{"id":17837059,"url":"https://github.com/sammyhenningsson/hal_presenter","last_synced_at":"2025-03-19T21:30:32.812Z","repository":{"id":56875705,"uuid":"92093997","full_name":"sammyhenningsson/hal_presenter","owner":"sammyhenningsson","description":"DSL for writing Presenters to serialize JSON HAL","archived":false,"fork":false,"pushed_at":"2022-01-31T20:39:25.000Z","size":175,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2024-04-24T14:37:21.301Z","etag":null,"topics":["curie","hal","hypermedia-api","ruby","serialization"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sammyhenningsson.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-05-22T19:56:49.000Z","updated_at":"2022-01-31T20:39:28.000Z","dependencies_parsed_at":"2022-08-20T10:11:16.347Z","dependency_job_id":null,"html_url":"https://github.com/sammyhenningsson/hal_presenter","commit_stats":null,"previous_names":["sammyhenningsson/hal_decorator"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sammyhenningsson%2Fhal_presenter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sammyhenningsson%2Fhal_presenter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sammyhenningsson%2Fhal_presenter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sammyhenningsson%2Fhal_presenter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sammyhenningsson","download_url":"https://codeload.github.com/sammyhenningsson/hal_presenter/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244022639,"owners_count":20385134,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["curie","hal","hypermedia-api","ruby","serialization"],"created_at":"2024-10-27T20:45:17.496Z","updated_at":"2025-03-19T21:30:32.554Z","avatar_url":"https://github.com/sammyhenningsson.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# HALPresenter\n[![Gem Version](https://badge.fury.io/rb/hal_presenter.svg)](https://badge.fury.io/rb/hal_presenter)\n\nHALPresenter is a DSL for creating serializers conforming to [JSON HAL](http://stateless.co/hal_specification.html). This DSL is highly influenced by ActiveModelSerializers.\nCheck out [this benchmark](https://gist.github.com/sammyhenningsson/890f7e4d6967883666851eb6aab92adb) for a comparison to other serializers.  \nSo, generating some json from an object, whats the big deal? Well if your API is not driven by hypermedia and your payloads most of the time just looks the same, then this might be overkill. But if you do have dynamic payloads (e.g the payload attributes and links depend on the context) then this gem greatly simplifies serialization and puts all the serialization logic in one place.\nThis documentation might be a bit long and dull, but skim through it and check out the examples. I think you'll get the hang of it.  \n\n## Installation\n```sh\ngem install hal_presenter\n```\nWith Gemfile:\n\n```sh\ngem 'hal_presenter'\n```\n\nAnd then execute:\n\n```sh\n$ bundle\n```\n\n## Intro\nLets start with an example. Say you have your typical blog and you want to serialize post resources. Posts have some text, an author and possibly some comments. Only the author of the post may edit or delete it. A serializer could then be written as:\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  model Post\n  \n  attribute :text\n  attribute :characters do\n    resource.text.size\n  end\n  \n  link :self do\n    \"/posts/#{resource.id}\"\n  end\n  \n  link :author do\n    \"/users/#{resource.author.id}\"\n  end\n  \n  link :edit do\n    \"/posts/#{resource.id}/edit\" if resource.author.id == options[:current_user]\n  end\n  \n  link :delete do\n    \"/posts/#{resource.id}\" if resource.author.id == options[:current_user]\n  end\n  \n  embed :recent_comments\nend\n\n```\nInstances of `Post` can now be serialized with `HALPresenter.to_hal(post, current_user: some_user)` which will produce the following (assuming the current user is the author of the post. Else the edit/delete links would not be present):\n\n``` ruby\n{\n  \"text\": \"some very important stuff\",\n  \"characters\": 25,\n    \"_links\": {\n    \"self\": {\n      \"href\": \"/posts/5\"\n    },\n    \"author\": {\n      \"href\": \"/users/8\"\n    },\n    \"edit\": {\n      \"href\": \"/posts/5/edit\"\n    },\n    \"delete\": {\n      \"href\": \"/posts/5\"\n    }\n  },\n  \"_embedded\": {\n    \"recent_comments\": {\n      \"count\": 2,\n      \"_links\": {\n        \"self\": {\n          \"href\": \"/posts/5/recent_comments\"\n        }\n      },\n      \"_embedded\": {\n        \"comments\": [\n          {\n            \"comment\": \"lorem ipsum\",\n            \"_links\": {\n              \"self\": {\n                \"href\": \"/posts/5/comment/1\"\n              }\n            }\n          },\n          {\n            \"comment\": \"dolor sit\",\n            \"_links\": {\n              \"self\": {\n                \"href\": \"/posts/5/comment/2\"\n              }\n            }\n          }\n        ]\n      }\n    }\n  }\n}\n```\n_Note_: In the output above, `recent_comments` is a collection and collections have their items embedded. Since this collection is\nembedded in the post resource that's why we get two levels of embedded resources.\n\n\n## Defining a Serializer\nSerializers are defined by extending `HALPresenter` in the begining of the class declaration. This will add the following class methods:\n- [`::model(clazz)`](#model)\n- [`::attribute(name, value = nil, embed_depth: nil, \u0026block)`](#attribute)\n- [`::link(rel, value = nil, **options, \u0026block)`](#link)\n- [`::curie(rel, value = nil, embed_depth: nil, \u0026block)`](#curie)\n- [`::profile(value = nil, **kwargs, \u0026block)`](#profile)\n- [`::policy(clazz)`](#policy)\n- [`::namespace(curie, \u0026block)`](#namespace)\n- [`::embed(name, value = nil, embed_depth: nil, presenter_class: nil, \u0026block)`](#embed)\n- [`::collection`](#collection)\n- [`::to_hal(resource = nil, **options)`](#to_hal)\n- [`::to_collection(resources = [], **options)`](#to_collection)\n- [`::post_serialize(\u0026block)`](#post_serialize)\n- [`::from_hal(payload, resource = nil)`](#from_hal)\n\n### ::model\nThe `model` class method is used to register the resource Class that this serializer handles. (There's no Rails magic that automagically maps models to serializers.)\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  model Post\nend\n```\nThis make it possible to serialize `Post` instances with `HALPresenter.to_hal`. HALPresenter will then lookup the right presenter and delegate the serialization to it \n(which in the case above would be `PostSerializer`).\n``` ruby\npost = Post.new(*args)\nHALPresenter.to_hal(post)\n```\nIf a model does not have it's own presenter but one of its superclasses does, then that will be used.\n``` ruby\nclass SubPost \u003c Post; end\nsub_post = SubPost.new(*args)\nHALPresenter.to_hal(sub_post) # will lookup PostSerializer since there isn't a specific one for SubPost\n```\n\nUsing the `model` class method is not required for serialization. The serializer can also be called directly.\n``` ruby\nPostSerializer.to_hal(post)\n```\nThe serializer class may also be specified as an option, using the `:presenter` key.\n``` ruby\nHALPresenter.to_hal(post, {presenter: PostSerializer})\n```\nEven though the `model` class method is optional, it is very useful if the serializer should be selected dynamically and when the serializer is used for deserialization.\n\n\n### ::attribute\nThe `attribute` class method specifies an attribute (property) to be serialized. The first argument, `name`, is required and specifies the name of the attribute. When `::attribute` is called with only one argument, the resources being serialized are expected to respond to that argument and the returned value is what ends up in the payload.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  attribute :title\nend\n\npost = OpenStruct.new(title: \"hello\")\nPostSerializer.to_hal(post)   # =\u003e {\"title\": \"hello\"}\n```\nIf `::attribute` is called with two arguments, then the second arguments is what ends up in the payload.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  attribute :title, \"world\"\nend\n\npost = OpenStruct.new(title: \"ignored\")\nPostSerializer.to_hal(post)   # =\u003e {\"title\": \"world\"}\n```\nWhen a block is passed to `::attribute`, then the return value of that block is what ends up in the payload.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  attribute :title do\n    resource.title.upcase\n  end\nend\n\npost = OpenStruct.new(title: \"hello\")\nPostSerializer.to_hal(post)   # =\u003e {\"title\": \"HELLO\"}\n```\nNotice that the object being serialized (`post` in the above example) is accessible inside the block by the `resource` method.  \nThe keyword argument `:embed_depth` may be specified to set a max allowed nesting depth for the corresponding attribute to be serialized. See [`embed_depth`](#keyword-argument-embed_depth-passed-to-attribute-link-curie-and-embed).  \n\n### ::link\nThe `link` class method specifies a link to be added to the _\\_links_ property. The first argument, `rel`, is required. `::link` must be called with either a second argument (`value`) or a block.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  link :self, '/posts/1'\nend\n\nPostSerializer.to_hal   # =\u003e {\"_links\": {\"self\": {\"href\": \"/posts/1\"}}}\n```\nWhen a block is passed to `::link`, the block must evaluate to either `nil`, a String containing the href or a hash with symbol keys.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n\n  link :self do\n    \"/posts/#{resource.id}\"\n  end\nend\n\npost = OpenStruct.new(id: 5)\nPostSerializer.to_hal(post)   # =\u003e {\"_links\": {\"self\": {\"href\": \"/posts/5\"}}}\n```\nWhen the block returns a hash the following keys will be used `:href`, `:type`, `:deprecation`, `:profile`, `:title` and `:templated`.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n\n  link :other do\n    {\n      href: \"/posts/#{resource.id}\",\n      title: resource.title,\n      type: resource.type,\n    }\n  end\nend\n\npost = OpenStruct.new(id: 5, title: 'Foo', type: 'bar')\nPostSerializer.to_hal(post)   # =\u003e {\n                              #      \"_links\": {\n                              #        \"other\": {\n                              #          \"href\": \"/posts/5\",\n                              #          \"title\": \"Foo\",\n                              #          \"type\": \"bar\"\n                              #        }\n                              #      }\n                              #    }\n```\n\nMultiple links can have the same relation. If so, they will be serialized as an array.\n```ruby\nclass PostSerializer\n  extend HALPresenter\n\n  link :item, \"/foo\"\n\n  link :item do\n    \"/bar\"\n  end\nend\n\nPostSerializer.to_hal   # =\u003e\n                        # {\n                        #   \"_links\": {\n                        #     \"item\": [\n                        #       {\n                        #         \"href\": \"/foo\"\n                        #       },\n                        #       {\n                        #         \"href\": \"/bar\"\n                        #       }\n                        #     ]\n                        #   }\n                        # }\n```\n\nThe following options may be given to `::link`:\n- `embed_depth` - sets a max allowed nesting depth for the corresponding link to be serialized. See [`embed_depth`](#keyword-argument-embed_depth-passed-to-attribute-link-curie-and-embed).\n- `curie` - prepends a curie to the rel.\n- `title` - a string used for labelling the link (e.g. in a user interface).\n- `type` - the media type of the resource returned after following this link.\n- `deprecation` - a URL providing information about the deprecation of this link.\n- `profile` - a URI that hints about the profile of the target resource.  \n\n### ::curie\nThe `curie` class method specifies a curie to be added to the _curies_ list. The first argument, `rel`, is required. `::curie` must be called with either a second argument (`value`) or a block.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n\n  curie :doc, '/api/docs/{rel}'\n  link :'doc:user', '/users/5'\nend\n\nPostSerializer.to_hal   # =\u003e {\"_links\":{\"doc:user\":{\"href\":\"/users/5\"},\"curies\":[{\"name\":\"doc\",\"href\":\"/api/docs/{rel}\",\"templated\":true}]}}\n```\nWhen a block is passed to `::curie`, the return value of that block is what ends up as the href of the curie.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n\n  curie :doc do\n  '/api/docs/{rel}'\n  end\n  link :'doc:user', '/users/5'\nend\n\npost = OpenStruct.new(id: 5)\nPostSerializer.to_hal(post)   # =\u003e {\"_links\":{\"doc:user\":{\"href\":\"/users/5\"},\"curies\":[{\"name\":\"doc\",\"href\":\"/api/docs/{rel}\",\"templated\":true}]}}\n```\nThe keyword argument `:embed_depth` may be specified to set a max allowed nesting depth for the corresponding curie to be serialized. See [`embed_depth`](#keyword-argument-embed_depth-passed-to-attribute-link-curie-and-embed).  \nWhen a resource is embedded in another resource all curies are added to the root resource. This ensures that each curie only appear once in the output.\nNote that curies may get renamed if there are conflicts between them. See example in [`::embed`](#embed) for more details.\n\n### ::profile\nThe `profile` class method is used to specify a mediatype profile that the serializer is conforming to. This is optional and does not change anything about how things get serialized.\nThis method exist only to specify that a given serializer will produce content with semantic meaning described in the specified mediatype profile.  \nThe profile is retrieved with the class method `semantic_profile`.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  profile \"foobar\"\nend\n\nPostSerializer.semantic_profile     # =\u003e \"foobar\"\n\n# Or with a block\n\nclass PostSerializer\n  extend HALPresenter\n  profile do\n    \"foo#{options[:foo]}\"\n  end\nend\n\nPostSerializer.semantic_profile(foo: 'bar')     # =\u003e \"foobar\"\n```\n\n### ::policy\nThe `policy` class method is used to register a policy class that should be used during serialization. The purpose of using a policy class is to specify rules about which properties should be serialized (depending on the context). E.g hide some attributes and/or links if `current_user` is not an admin. If a policy is specified, then by default no properties will be serialized unless the policy explicitly allows them to be serialized.  \nUsing polices is not required, but its a nice way to structure rules about what should be shown and what actions (links) are possible to perform on a resource. The latter is usually tightly coupled with authorization in controllers. This means we can create polices with a bunch of rules and use the same policy in both serialization and in controllers. This plays nicely with gems like [Pundit](https://github.com/elabs/pundit).\nInstances of the class registered with this method needs to respond to the following methods:\n- `initialize(current_user, resource, options = {})`\n- `attribute?(name)`\n- `link?(rel)`\n- `embed?(name)`\n\nAdditional methods will be needed for authorization in controller. Such as `create?`, `update?` etc when using Pundit.\nA policy instance will be instantiated with the resource being serialized and the option `:current_user` passed to `::to_hal`. For each attribute being serialized a call to `policy_instance.attribute?(name)` will be made. If that call returns `true` then the attribute will be serialized. Else it will not end up in the serialized payload. Same goes for links and embedded resources. Curies are ignored by policies and always serialized.\nUsing the following Policy would discard everything except the _title_ attribute, the _self_ link and only embedded resources if `current_user` is an admin user.\n``` ruby\nclass SomePolicy\n  attr_reader :current_user, :resource, :options\n\n  def initialize(current_user, resource, options = {})\n    @current_user = current_user\n    @resource = resource\n    @options = options\n  end\n\n  def attribute?(name)\n    name.to_s == 'title'\n  end\n\n  def link?(rel)\n    rel == :self\n  end\n\n  def embed?(_name)\n    return false unless current_user\n    current_user.admin?\n  end\nend\n\n```\nThis gem includes a DSL that simplifies creating policies. See [`HALPresenter::Policy::DSL`](#policy-dsl).\n\n### ::namespace\nMultiple links and embedded resources may be grouped together inside a curie namespace. This is done by wrapping them inside a block passed to `::namespace`.\n``` ruby\nclass DogSerializer\n  extend HALPresenter\n  attribute :name\n  link :self do\n    \"/dogs/#{resource.name}\"\n  end\nend\n\nclass PetSerializer\n  extend HALPresenter\n  namespace :foo do\n    link :owner do\n      \"/users/#{resource.owner_id}\"\n    end\n    embed :animal, presenter_class: DogSerializer\n  end\n  curie :foo do\n    '/some_link/{rel}'\n  end\nend\n\ndog = OpenStruct.new(id: 3, name: 'Milo')\npet = OpenStruct.new(owner_id: 5, animal: dog)\n\nPetSerializer.to_hal(pet)   # =\u003e {\"_links\":{\"foo:owner\":{\"href\":\"/users/5\"},\"curies\":[{\"name\":\"foo\",\"href\":\"/some_link/{rel}\",\"templated\":true}]},\"_embedded\":{\"foo:animal\":{\"name\":\"Milo\",\"_links\":{\"self\":{\"href\":\"/dogs/Milo\"}}}}}\n```\n\n### ::embed\nThe `embed` class method specifies a nested resource to be embedded. The first argument, `name`, is required. When `::embed` is called with only one argument, the resource being serialized is expected to respond to the value of that argument and the returned value is what ends up in the payload. The keyword argument `presenter_class` specifies the serializer to be used for serializing the embedded resource.\n``` ruby\nclass UserSerializer\n  extend HALPresenter\n  attribute :name\nend\n\nclass PostSerializer\n  extend HALPresenter\n  embed :author, presenter_class: UserSerializer\nend\n\nuser = OpenStruct.new(name: \"bengt\")\npost = OpenStruct.new(title: \"hello\", author: user)\nPostSerializer.to_hal(post)   # =\u003e {\"_embedded\":{\"author\":{\"name\":\"bengt\"}}}\n```\nIf `::embed` is called with two arguments, then the second arguments is embedded.\n``` ruby\nclass UserSerializer\n  extend HALPresenter\n  attribute :name\nend\n\nclass PostSerializer\n  extend HALPresenter\n  embed :author, OpenStruct.new(name: \"bengt\"), presenter_class: UserSerializer\nend\n\npost = OpenStruct.new(title: \"hello\")\nPostSerializer.to_hal(post)   # =\u003e {\"_embedded\":{\"author\":{\"name\":\"bengt\"}}}\n```\nWhen a block is passed to `::embed`, then the return value of that block is embedded.\n``` ruby\nclass UserSerializer\n  extend HALPresenter\n  attribute :name\nend\n\nclass PostSerializer\n  extend HALPresenter\n  embed :author, presenter_class: UserSerializer do\n    OpenStruct.new(name: \"bengt\")\n  end\nend\n\npost = OpenStruct.new(title: \"hello\")\nPostSerializer.to_hal(post)   # =\u003e {\"_embedded\":{\"author\":{\"name\":\"bengt\"}}}\n```\nThe keyword argument `:embed_depth` may be specified to set a max allowed nesting depth for the corresponding resource to be embedded. See [`embed_depth`](#keyword-argument-embed_depth-passed-to-attribute-link-curie-and-embed).  \n If the resource to be embedded has a registered Serializer then `presenter_class` is not needed.\n``` ruby\nclass User\n  def name; \"bengt\"; end\nend\n\nclass UserSerializer\n  extend HALPresenter\n  model User\n  attribute :name\nend\n\nclass PostSerializer\n  extend HALPresenter\n  embed :author\nend\n\npost = OpenStruct.new(title: \"hello\", author: User.new)\nPostSerializer.to_hal(post)   # =\u003e {\"_embedded\":{\"author\":{\"name\":\"bengt\"}}}\n```\nIf the embedded resource has a curie then it will be added to the root resource rather than the embedded resource. If there is a curie name conflict between the root resource and any embedded resources,\none or many of them will get renamed.\n``` ruby\nclass FooSerializer\n  extend HALPresenter\n  attribute :info\n  link :foo, curie: :doc do\n    '/foo'\n  end\n  curie :doc do\n    '/doc/{rel}'\n  end\nend\n\nclass BarSerializer\n  extend HALPresenter\n  attribute :info\n  link :bar, curie: :doc do\n    '/bar'\n  end\n  curie :doc do\n    '/conflicting/{rel}'\n  end\nend\n\nclass PostSerializer\n  extend HALPresenter\n  curie :doc do\n    '/doc/{rel}'\n  end\n  embed :foo, presenter_class: FooSerializer\n  embed :bar, presenter_class: BarSerializer\nend\n\npost = OpenStruct.new(\n  foo: OpenStruct.new(info: \"doc means the same thing for foo\"),\n  bar: OpenStruct.new(info: \"doc conflicts with Foo and Post. It must be renamed!\")\n)\nPostSerializer.to_hal(post)   # =\u003e\n                              # {\n                              #   \"_links\": {\n                              #     \"curies\": [\n                              #       {\n                              #         \"name\": \"doc\",\n                              #         \"href\": \"/doc/{rel}\",\n                              #         \"templated\": true\n                              #       },\n                              #       {\n                              #         \"name\": \"doc0\",\n                              #         \"href\": \"/conflicting/{rel}\",\n                              #         \"templated\": true\n                              #       }\n                              #     ]\n                              #   },\n                              #   \"_embedded\": {\n                              #     \"foo\": {\n                              #       \"info\": \"doc means the same thing for foo\",\n                              #       \"_links\": {\n                              #         \"doc:foo\": {\n                              #           \"href\": \"/foo\"\n                              #         }\n                              #       }\n                              #     },\n                              #     \"bar\": {\n                              #       \"info\": \"doc conflicts with Foo and Post. It must be renamed!\",\n                              #       \"_links\": {\n                              #         \"doc0:bar\": {\n                              #           \"href\": \"/bar\"\n                              #         }\n                              #       }\n                              #     }\n                              #   }\n                              # }\n```\nNote that the curie in `FooSerializer` has the same href as the curie in `PostSerializer`, thus it remains unaffected. The curie in `BarSerializer` however has another href and needs to be renamed. The new name is `doc0`. Any rels (links and embedded) refering to the renamed curie will be updated with the new curie name (e.g. `doc0:bar`).  \n\n### collection\nThe `collection` class method is used to make a serializer capable of serializing an array of resources. Serializing collections may of course be done with separate serializer, but should we want to use the same serializer class for both then `::collection` will make that work. The method takes a required keyword paramter named `:of`, which will be used as the key in the corresponding _\\_embedded_ property. Each entry in the array given to `::to_collection` will then be serialized with this serializer.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  attribute :id\n  attribute :title\n  collection of: 'posts'\nend\n\nlist = (1..2).map do |i|\n  OpenStruct.new(id: i, title: \"hello#{i}\")\nend\n\nPostSerializer.to_collection(list)   # =\u003e {\"_embedded\":{\"posts\":[{\"id\":1,\"title\":\"hello1\"},{\"id\":2,\"title\":\"hello2\"}]}}\n```\nThe `collection` class method takes an optional block. The purpose of this block is to be able to set attributes, links and embedded resources on the serialized collection.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  attribute :id\n  attribute :title\n  collection of: 'posts' do\n    attribute(:number_of_posts) { resources.count }\n    link :self do\n      format \"/posts%s\", (options[:page] \u0026\u0026 \"?page=#{options[:page]}\")\n    end\n    link :next do\n      \"/posts?page=#{options[:next]}\" if options[:next]\n    end\n  end\nend\n\nlist = (1..2).map do |i|\n  OpenStruct.new(id: i, title: \"hello#{i}\")\nend\n\nPostSerializer.to_collection(list, page: 1, next: 2)   # =\u003e {\"number_of_posts\":2,\"_links\":{\"self\":{\"href\":\"/posts?page=1\"},\"next\":{\"href\":\"/posts?page=2\"}},\"_embedded\":{\"posts\":[{\"id\":1,\"title\":\"hello1\"},{\"id\":2,\"title\":\"hello2\"}]}}\"\n```\nThe response above with some newlines.\n```sh\n{\n  \"number_of_posts\": 2,\n  \"_links\": {\n    \"self\": {\n      \"href\": \"/posts?page=1\"\n    },\n    \"next\": {\n      \"href\": \"/posts?page=2\"\n    }\n  },\n  \"_embedded\": {\n    \"posts\": [\n      {\n        \"id\": 1,\n        \"title\": \"hello1\"\n      },\n      {\n        \"id\": 2,\n        \"title\": \"hello2\"\n      }\n    ]\n  }\n}\n```\nNote: the block given to the `:number_of_posts` attribute is using the method `resources`. This is just and alias for `resource` which looks better inside collections. \n\n#### Keyword argument `:embed_depth` passed to `::attribute`, `::link`, `::curie` and `::embed`\nThe `:embed_depth` keyword arguments specifies for which levels of embedding the correponding property should be serialized.\n - `nil`: The property it is always serialized.\n - `0`: The property is only serialized when the resource is not embedded.\n - `1`: The property is serialized when it's embedded at most 1 level deep.\n - etc..\n\nConsider the following payload representing a post resource:\n```sh\n{\n  \"id\": 1,\n  \"message\": \"lorem ipsum..\",\n  \"_links\": {\n    \"self\": {\n      \"href\": \"/posts/1\"\n    },\n  },\n  \"_embedded\": {\n    \"comments\": [\n      {\n        \"id\": 1,\n        \"comment\": \"hello1\"\n        \"_embedded\": {\n          \"user\": {\n            \"id\": 2,\n            \"name\": \"foo\",\n            \"_links\": {\n              \"self\": {\n                \"href\": \"/users/2\"\n              }\n            }\n          }\n        }\n      },\n      {\n        \"id\": 2,\n        \"comment\": \"hello2\"\n        \"_embedded\": {\n          \"user\": {\n            \"id\": 3,\n            \"name\": \"foo\",\n            \"_links\": {\n              \"self\": {\n                \"href\": \"/users/3\"\n              }\n            }\n          }\n        }\n      }\n    ]\n  }\n}\n```\nHere the post attributes `id`, `message` as well as the _self_ link and the embedded comments all have a depth of 0. The properties of each embedded comment (attributes `id`, `comment` and embedded user) have a depth of 1. The properties of the user resources (embedded in the comments, which in turn are embedded in the post resource) have a depth of 2.  \nWhen a collection is serialized (using `::to_collection`) the embedded resources will have the same depth as attributes and links on collection (e.g. depth = 0 unless the collection itself is embedded).  \nThe purpose of specifying `embed_depth` is to be able skip serializing properties when embeddeed.  \nFor example, when you serialize a collection of resources, perhaps you would like for each resource in that collection to only serialize a few properties, making it kind of like a \"preview\" of each resource.\n\n#### blocks passed to `::attribute`, `::link`, `::curie`, `::embed` and `::collection`\nBlocks passes to `::attribute`, `::link`, `::curie` and `::embed` have access to the resource being serialized throught the `resource` method. These blocks also have access to an optional `options` hash that can be passed to `::to_hal`.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  attribute :title do\n    \"#{resource.id} -- #{resource.title} -- #{options[:extra]}\"\n  end\nend\n\npost = OpenStruct.new(id: 5, title: \"hello\")\nPostSerializer.to_hal(post, extra: 'world')   # =\u003e {\"title\": \"5 -- hello -- world\"}\n```\nThese blocks also have access to the scope where the block was created (e.g. the Serializer class)\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  def self.prefix; \"--\u003e\"; end\n  attribute :title do\n    \"#{prefix} #{resource.title}\"\n  end\nend\n\npost = OpenStruct.new(id: 5, title: \"hello\")\nPostSerializer.to_hal(post)   # =\u003e {\"title\": \"--\u003e hello\"}\n```\nNote: this does not mean that `self` inside the block is the serializer class. The access to the serializer class methods is done by delegation.  \nIf the block passed to `::attribute` evaluates to `nil` then the serialized value will be `null`. If the block passed to `::link`, `::curie` or `::embed` evaluates to `nil`,\nthen the corresponding property will not be serialized.\n``` ruby\nclass PostSerializer\n  extend HALPresenter\n  attribute :title\n  attribute :foo { nil }\n  link :self { \"/posts/#{resource.id}\" }\n  link :edit do\n    \"/posts/#{resource.id}\" if resource.author_id == options[:current_user].id\n  end\nend\n\nuser = OpenStruct.new(id: 5)\npost = OpenStruct.new(id: 1, title: \"hello\", author_id: 2)\nPostSerializer.to_hal(post, current_user: user)   # =\u003e {\"title\":\"hello\",\"foo\":null,\"_links\":{\"self\":{\"href\":\"/posts/1\"}}}\n\nuser = OpenStruct.new(id: 2)\nPostSerializer.to_hal(post, current_user: user)   # =\u003e \"{\"title\":\"hello\",\"foo\":null,\"_links\":{\"self\":{\"href\":\"/posts/1\"},\"edit\":{\"href\":\"/posts/1\"}}}\"\n```\n\n\n### ::to_hal\nSee examples in [`::attribute`](#attribute), [`::link`](#link), [`::curie`](#curie) and [`::embed`](#embed).\n\n### ::to_collection\nSee examples in [`::collection`](#collection)\n\n### ::post_serialize\nThe `::post_serialize` class method can used to run a hook after each serialization. This method must be called with a block taking one parameter (which will be a Hash of the serialized result). This can be convenient when dynamic properties needs to be added to the serialized payload. As an example say that you have a Form class and a FormSerializer that should be used to serialize different kinds of forms.\n```ruby\n  class Field\n    attr_reader :name, :type, :value\n\n    def initialize(name, params = {})\n      @name = name\n      @type = params[:type]\n      @has_value = params.key? :value\n      @value = params[:value]\n    end\n\n    def has_value?\n      @has_value\n    end\n  end\n\n  class Form\n    attr_accessor :resource, :name, :title, :href, :method, :type, :self_link, :fields\n\n    def initialize(params = {})\n      @name = params[:name]\n      @title = params[:title]\n      @method = params[:method] || :post\n      @type = params[:type] || 'application/json'\n      @fields = (params[:fields] || {}).map { |name, args| Field.new(name, args) }\n    end\n  end\n  \n  class FormSerializer\n    extend HALPresenter\n\n    model Form\n    profile 'shaf-form'\n\n    attribute :method do\n      (resource\u0026.method || 'POST').to_s.upcase\n    end\n\n    attribute :name do\n      resource\u0026.name\n    end\n\n    attribute :title do\n      resource\u0026.title\n    end\n\n    attribute :href do\n      resource\u0026.href\n    end\n\n    attribute :type do\n      resource\u0026.type\n    end\n\n    link :self do\n      resource\u0026.self_link\n    end\n\n    link :profile,\n      \"https://gist.githubusercontent.com/sammyhenningsson/39c8aafeaf60192b082762cbf3e08d57/raw/shaf-form.md\"\n\n    post_serialize do |hash|\n      fields = resource\u0026.fields\n      break if fields.nil? || fields.empty?\n      hash[:fields] = fields.map do |field|\n        { name: field.name, type: field.type }.tap do |f|\n          f[:value] = field.value if field.has_value?\n        end\n      end\n    end\n  end\n```\nNow this setup can be used to serialize different kinds of forms with a single serializer.\n```ruby\ncommon_fields = {\n  email: { type: \"string\"},\n  password: { type: \"string\"}\n}\n\ncreate_form = Form.new(name: 'create-user', title: 'Create User', fields: common_fields.merge({username: { type: \"string\"}}))\ncreate_form.href = '/users'\n\nedit_form = Form.new(name: 'edit-user', title: 'Update User', method: :put, fields: common_fields)\nedit_form.href = '/users/5/edit'\n\nFormSerializer.to_hal(create_form)\nFormSerializer.to_hal(edit_form)\n```\nThis would give the following:\n```ruby\n{\n    \"href\": \"/users\",\n    \"method\": \"POST\",\n    \"name\": \"create-user\",\n    \"title\": \"Create User\",\n    \"type\": \"application/json\",\n    \"fields\": [\n        {\n            \"name\": \"email\",\n            \"type\": \"string\"\n        },\n        {\n            \"name\": \"password\",\n            \"type\": \"string\"\n        },\n        {\n            \"name\": \"username\",\n            \"type\": \"string\"\n        }\n    ]\n}\n\n{\n    \"href\": \"/users/5/edit\",\n    \"method\": \"PUT\",\n    \"name\": \"edit-user\",\n    \"title\": \"Update User\",\n    \"type\": \"application/json\",\n    \"fields\": [\n        {\n            \"name\": \"email\",\n            \"type\": \"string\"\n        },\n        {\n            \"name\": \"password\",\n            \"type\": \"string\"\n        }\n    ]\n}\n\n```\n\n### from_hal\nThe class method `from_hal` is used to deserialize a payload into a model instance. If there are links in the payload they will be discarded. Fields in the payload that\ndoes not have a corresponding attribute or embed in the serializer will be ignored.\n\n```ruby\nclass User\n  attr_accessor :name\nend\n\nclass Post\n  attr_accessor :title, :author\nend\n\nclass UserSerializer\n  extend HALPresenter\n  model User\n  attribute :name\n  link :self, '/user'\nend\n\nclass PostSerializer\n  extend HALPresenter\n  model Post\n  attribute :title\n  link :self, '/post'\n  embed :author, presenter_class: UserSerializer\nend\n\nuser = User.new\nuser.name = \"bengt\"\n\npost = Post.new\npost.title= \"hello\"\npost.author = user\n\npayload = PostSerializer.to_hal(post)   # =\u003e {\"title\":\"hello\",\"_links\":{\"self\":{\"href\":\"/post\"}},\"_embedded\":{\"author\":{\"name\":\"bengt\",\"_links\":{\"self\":{\"href\":\"/user\"}}}}}\"\n\npost = PostSerializer.from_hal(payload)\npost.title                               # =\u003e \"hello\"\npost.author.name                         # =\u003e \"bengt\"\n\n```\nInstances are created by calling `::new` on the class registered by `::model` without any arguments. Then each attribute is set with *`#attribute_name=`* (e.g.\n`post.title = 'hello'`)\nThus, all models used for deserialization must respond to *`attribute_name=`* for all attributes used in the serializer.  \nIf the model can't be created without arguments (or if the instance already exit), then the instance can be passed to `::from_hal`.\n```ruby\nclass User\n  attr_accessor :name\n\n  def initialize(name)\n    @name = name\n  end\nend\n\nclass Post\n  attr_accessor :title, :author\n\n  def initialize(title, author)\n    @title = title\n    @author = author\n  end\nend\n\nclass UserSerializer\n  extend HALPresenter\n  model User\n  attribute :name\n  link :self, '/user'\nend\n\nclass PostSerializer\n  extend HALPresenter\n  model Post\n  attribute :title\n  link :self, '/post'\n  embed :author, presenter_class: UserSerializer\nend\n\npayload = JSON.generate(\n  {\n    \"title\": \"hello\",\n    \"_embedded\": {\n      \"author\": {\n        \"name\": \"bengt\"\n      }\n    }\n  }\n)\n\nuser = User.new('will_be_overwritten')\npost = Post.new('will_be_overwritten', user)\n\npost = PostSerializer.from_hal(payload, post)\npost.title                               # =\u003e \"hello\"\npost.author.name                         # =\u003e \"bengt\"\n```\nCollections can be deserialized into an array as long as the serializer has a collection. In this case the model instance cannot be passed as an argument\nso it must be possbile to create new instances with _ModelName_.new (whithout any arguments).\n```ruby\nclass User\n  attr_accessor :name\nend\n\nclass UserSerializer\n  extend HALPresenter\n  model User\n  attribute :name\n  link :self, '/user'\n  collection of: 'users'\nend\n\nusers =  (1..5).map do |i|\n  {\n    name: \"user#{i}\",\n    foo: \"ignored\"\n  }\nend\n\npayload = JSON.generate(\n  {\n    _embedded: {\n      users: users\n    }\n  }\n)\n\nusers = UserSerializer.from_hal(payload)\nusers.class                              # =\u003e Array\nusers.first.class                        # =\u003e User\nusers.map(\u0026:name)                        # =\u003e [\"user1\", \"user2\", \"user3\", \"user4\", \"user5\"]                         \n```\n\n## Inheritance\nSerializers may use inheritance and should work just as expected \n```ruby\nclass BaseSerializer\n  extend HALPresenter\n\n  attribute :base, \"will_be_overwritten\"\n  attribute :title\n\n  link :self do\n    resource_path(resource.id)\n  end\nend\n\nclass PostSerializer \u003c BaseSerializer\n  attribute :base, \"child_attribute\"\n  \n  def self.resource_path(id)\n    \"/posts/#{id}\"\n  end\nend\n\npost = OpenStruct.new(id: 5, title: 'hello')\n\nPostSerializer.to_hal(post)   # =\u003e {\"title\": \"hello\", \"base\": \"child_attribute\", \"_links\": {\"self\": {\"href\": \"/posts/5\"}}}\n```\n\n## Config\n### HALPresenter.base_href\nThis module method can be used to specify a base url that will get prepended to links hrefs.\n```ruby\nHALPresenter.base_href = 'https://localhost:3000/'\n\nclass PostSerializer\n  extend HALPresenter\n  link :self, '/posts/1'\nend\n\nPostSerializer.to_hal   # =\u003e {\"_links\": {\"self\": {\"href\": \"https://localhost:3000/posts/1\"}}}\n```\n\n### HALPresenter.paginate\nSetting `HALPresenter.paginate = true` will add next/prev links for collections when possible. Requirements for this is:\n- The resource being serialized is a paginated collection (Kaminari, will_paginate and Sequel are supported)\n- The serializer being used has a collection block which declares a self link\n\n## Policy DSL\nHALPresenter includes a DSL for creating polices. By including `HALPresenter::Policy::DSL` into your policy class you get the following class methods:\n- `::attribute(*names, \u0026block)`\n- `::link(*rels, \u0026block)`\n- `::embed(*names, \u0026block)`\n\nThese methods all work the same way and creates one or more rules for each `name` argument (`rel` for links). If no block is given then the corresponding attribute/link/embedded resource will always be serialized. If the block evaluates to `true` then the attribute/link/embedded resource will be serialized. Otherwise it will not be serialized. The block has access to the current user, the resource that is being serialized, as well as any options passed to `::to_hal` from the methods `current_user`, `resource` resp. `options`.\n```ruby\nclass UserPolicy\n  include HALPresenter::Policy::DSL\n\n  attribute :first_name, :last_name\n\n  attribute :email do\n    # show name and email attributes if user is logged in\n    !current_user.nil?\n  end\n  \n  attribute :ssn do\n    # Only show ssn if the resource belongs to current_user\n    current_user \u0026\u0026 resource.user.id == current_user.id\n  end\n  \n  link :self\n  \n  link :edit do\n    edit?\n  end\n  \n  embed :posts do\n    current_user \u0026\u0026 !current_user.posts.empty?\n  end\n  \n  def edit?\n    current_user \u0026\u0026 resource.user.id == current_user.id\n  end\n```\nNotice the instance method `#edit?` which is typically used by [Pundit](https://github.com/elabs/pundit). That method is called from the block belonging to the rule for the edit link. This means that we can use the same policy class both for serialization and for authorization (and have all the rules in one place). This is great since we should only provide links to actions that are possible (authorized) and we don't want to sync this between controller code and serialization code.\n\nPolicies can be inherited, so probably it makes sense to have a base policy that other policies can inherit from. Like:\n```ruby\nclass BasePolicy\n  include HALPresenter::Policy::DSL\n\n  def authenticated?\n    !!current_user\n  end\nend\n\nclass CommentPolicy \u003c BasePolicy\n  link :post\n\n  link :comment do\n    authenticated?\n  end\nend\n```\n\nSometimes policies may depend on other policies. For thoses cases we can delegate to another policy. Like:\n```ruby\nclass CommentPolicy \u003c BasePolicy\n  def read?\n    delegate_to PostPolicy, :read?, resource: resource.post\n  end\nend\n```\nBesides `#delegate_to(policy_class, method, resource: nil, args: nil, **opts)`, there is also:\n - `#delegate_attribute(policy_class, attr, **opts)`\n - `#delegate_link(policy_class, rel, **opts)`\n - `#delegate_embed(policy_class, rel, **opts)`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsammyhenningsson%2Fhal_presenter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsammyhenningsson%2Fhal_presenter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsammyhenningsson%2Fhal_presenter/lists"}