Repository: plexus/yaks Branch: master Commit: 75c41e5d9c56 Files: 174 Total size: 364.3 KB Directory structure: gitextract_hj5iupwf/ ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── ADDING_FORMATS.md ├── CHANGELOG.md ├── COOKBOOK.md ├── DEVELOPERS.md ├── FORMATS.org ├── Gemfile ├── IDENTIFIERS.md ├── LICENSE ├── Rakefile ├── bench/ │ ├── bench.rb │ └── bench_1000.rb ├── code_of_conduct.md ├── notes.org ├── shared/ │ ├── rake_tasks.rb │ └── rspec_config.rb ├── yaks/ │ ├── .rspec │ ├── README.md │ ├── Rakefile │ ├── ataru_setup.rb │ ├── find_missing_tests.rb │ ├── lib/ │ │ ├── yaks/ │ │ │ ├── behaviour/ │ │ │ │ └── optional_includes.rb │ │ │ ├── breaking_changes.rb │ │ │ ├── builder.rb │ │ │ ├── changelog.rb │ │ │ ├── collection_mapper.rb │ │ │ ├── collection_resource.rb │ │ │ ├── config.rb │ │ │ ├── configurable.rb │ │ │ ├── default_policy.rb │ │ │ ├── errors.rb │ │ │ ├── format/ │ │ │ │ ├── collection_json.rb │ │ │ │ ├── hal.rb │ │ │ │ ├── halo.rb │ │ │ │ └── json_api.rb │ │ │ ├── format.rb │ │ │ ├── fp/ │ │ │ │ └── callable.rb │ │ │ ├── html5_forms.rb │ │ │ ├── mapper/ │ │ │ │ ├── association.rb │ │ │ │ ├── association_mapper.rb │ │ │ │ ├── attribute.rb │ │ │ │ ├── config.rb │ │ │ │ ├── form/ │ │ │ │ │ ├── config.rb │ │ │ │ │ ├── dynamic_field.rb │ │ │ │ │ ├── field/ │ │ │ │ │ │ └── option.rb │ │ │ │ │ ├── field.rb │ │ │ │ │ ├── fieldset.rb │ │ │ │ │ └── legend.rb │ │ │ │ ├── form.rb │ │ │ │ ├── has_many.rb │ │ │ │ ├── has_one.rb │ │ │ │ └── link.rb │ │ │ ├── mapper.rb │ │ │ ├── null_resource.rb │ │ │ ├── pipeline.rb │ │ │ ├── primitivize.rb │ │ │ ├── reader/ │ │ │ │ ├── hal.rb │ │ │ │ └── json_api.rb │ │ │ ├── resource/ │ │ │ │ ├── form/ │ │ │ │ │ ├── field/ │ │ │ │ │ │ └── option.rb │ │ │ │ │ ├── field.rb │ │ │ │ │ ├── fieldset.rb │ │ │ │ │ └── legend.rb │ │ │ │ ├── form.rb │ │ │ │ ├── has_fields.rb │ │ │ │ └── link.rb │ │ │ ├── resource.rb │ │ │ ├── runner.rb │ │ │ ├── serializer.rb │ │ │ ├── util.rb │ │ │ └── version.rb │ │ └── yaks.rb │ ├── spec/ │ │ ├── acceptance/ │ │ │ ├── acceptance_spec.rb │ │ │ ├── json_shared_examples.rb │ │ │ └── models.rb │ │ ├── fixture_helpers.rb │ │ ├── integration/ │ │ │ ├── dynamic_form_fields_spec.rb │ │ │ ├── fieldset_spec.rb │ │ │ └── map_to_resource_spec.rb │ │ ├── json/ │ │ │ ├── confucius.collection_json.json │ │ │ ├── confucius.hal.json │ │ │ ├── confucius.halo.json │ │ │ ├── confucius.json_api.json │ │ │ ├── john.hal.json │ │ │ ├── list_of_quotes.collection_json.json │ │ │ ├── list_of_quotes.hal.json │ │ │ ├── list_of_quotes.json_api.json │ │ │ ├── plant_collection.collection.json │ │ │ ├── plant_collection.hal.json │ │ │ └── youtypeitwepostit.collection_json.json │ │ ├── sanity_spec.rb │ │ ├── spec_helper.rb │ │ ├── support/ │ │ │ ├── classes_for_policy_testing.rb │ │ │ ├── deep_eql.rb │ │ │ ├── fixtures.rb │ │ │ ├── friends_mapper.rb │ │ │ ├── models.rb │ │ │ ├── pet_mapper.rb │ │ │ ├── pet_peeve_mapper.rb │ │ │ ├── shared_contexts.rb │ │ │ └── youtypeit_models_mappers.rb │ │ ├── unit/ │ │ │ └── yaks/ │ │ │ ├── behaviour/ │ │ │ │ └── optional_includes_spec.rb │ │ │ ├── builder_spec.rb │ │ │ ├── collection_mapper_spec.rb │ │ │ ├── collection_resource_spec.rb │ │ │ ├── config_spec.rb │ │ │ ├── configurable_spec.rb │ │ │ ├── default_policy/ │ │ │ │ ├── derive_mapper_from_collection_spec.rb │ │ │ │ ├── derive_mapper_from_item_spec.rb │ │ │ │ └── derive_mapper_from_object_spec.rb │ │ │ ├── default_policy_spec.rb │ │ │ ├── format/ │ │ │ │ ├── collection_json_spec.rb │ │ │ │ ├── hal_spec.rb │ │ │ │ ├── halo_spec.rb │ │ │ │ ├── html_spec.rb │ │ │ │ └── json_api_spec.rb │ │ │ ├── format_spec.rb │ │ │ ├── fp/ │ │ │ │ └── callable_spec.rb │ │ │ ├── mapper/ │ │ │ │ ├── association_mapper_spec.rb │ │ │ │ ├── association_spec.rb │ │ │ │ ├── attribute_spec.rb │ │ │ │ ├── config_spec.rb │ │ │ │ ├── form/ │ │ │ │ │ ├── config_spec.rb │ │ │ │ │ ├── dynamic_field_spec.rb │ │ │ │ │ ├── field/ │ │ │ │ │ │ └── option_spec.rb │ │ │ │ │ ├── field_spec.rb │ │ │ │ │ ├── fieldset_spec.rb │ │ │ │ │ └── legend_spec.rb │ │ │ │ ├── form_spec.rb │ │ │ │ ├── has_many_spec.rb │ │ │ │ ├── has_one_spec.rb │ │ │ │ └── link_spec.rb │ │ │ ├── mapper_spec.rb │ │ │ ├── null_resource_spec.rb │ │ │ ├── pipeline_spec.rb │ │ │ ├── primitivize_spec.rb │ │ │ ├── resource/ │ │ │ │ ├── form/ │ │ │ │ │ ├── field_spec.rb │ │ │ │ │ ├── fieldset_spec.rb │ │ │ │ │ └── legend_spec.rb │ │ │ │ ├── form_spec.rb │ │ │ │ ├── has_fields_spec.rb │ │ │ │ └── link_spec.rb │ │ │ ├── resource_spec.rb │ │ │ ├── runner_spec.rb │ │ │ ├── serializer_spec.rb │ │ │ └── util_spec.rb │ │ └── yaml/ │ │ ├── confucius.yaml │ │ ├── list_of_quotes.yaml │ │ └── youtypeitwepostit.yaml │ └── yaks.gemspec ├── yaks-html/ │ ├── README.md │ ├── Rakefile │ ├── lib/ │ │ ├── yaks/ │ │ │ └── format/ │ │ │ ├── html.rb │ │ │ └── template.html │ │ ├── yaks-html/ │ │ │ └── rspec.rb │ │ └── yaks-html.rb │ ├── spec/ │ │ ├── smoke_test_spec.rb │ │ ├── spec_helper.rb │ │ └── support/ │ │ └── test_app.rb │ └── yaks-html.gemspec ├── yaks-sinatra/ │ ├── .rspec │ ├── README.md │ ├── Rakefile │ ├── lib/ │ │ └── yaks-sinatra.rb │ ├── spec/ │ │ ├── integration/ │ │ │ ├── classic_app.rb │ │ │ ├── classic_spec.rb │ │ │ └── modular_spec.rb │ │ ├── integration_helper.rb │ │ └── spec_helper.rb │ └── yaks-sinatra.gemspec └── yaks-transit/ ├── README.md ├── lib/ │ └── yaks-transit.rb └── yaks-transit.gemspec ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ pkg .bundle coverage Gemfile.lock *~ .yardoc doc FORMATS.html scratch .ruby-version .ruby-gemset Jarfile.lock ================================================ FILE: .rubocop.yml ================================================ AllCops: Exclude: - 'pkg/**/*' - 'vendor/**/*' - 'bench/**/*' Lint/AmbiguousRegexpLiteral: Enabled: false Lint/AmbiguousOperator: Enabled: false Lint/AssignmentInCondition: Enabled: false Metrics/AbcSize: Enabled: false Metrics/ClassLength: Enabled: false Metrics/CyclomaticComplexity: Enabled: false # FIXME: lower by fixing the biggest offenders Metrics/LineLength: Max: 184 Metrics/MethodLength: Enabled: false Metrics/PerceivedComplexity: Enabled: false # def_delegators’ first symbol is the target, the rest are calls Style/AlignParameters: Enabled: false # FIXME: should this be normalised to `EnforcedStyle: semantic`? Style/BlockDelimiters: Enabled: false # including parametrised modules looks much better without this alignment Style/ClosingParenthesisIndentation: Enabled: false Style/ConstantName: Exclude: - yaks/lib/yaks/breaking_changes.rb Style/Documentation: Enabled: false Style/EmptyLineBetweenDefs: AllowAdjacentOneLineDefs: true Style/FileName: Exclude: - yaks-html/lib/yaks-html.rb - yaks-sinatra/lib/yaks-sinatra.rb - yaks-transit/lib/yaks-transit.rb # including parametrised modules looks much better without this indentation Style/FirstParameterIndentation: Enabled: false Style/FormatString: EnforcedStyle: percent Style/GlobalVars: Exclude: - bench/bench.rb - bench/bench_1000.rb Style/HashSyntax: Exclude: - Rakefile # some arrays need deeper indenting for readability Style/IndentArray: Enabled: false Style/IndentationWidth: Exclude: - yaks/lib/yaks/breaking_changes.rb # the codebase uses -> consistently Style/Lambda: Enabled: false # FIXME: figure out why fixing this blows tests up Style/MethodCallParentheses: Enabled: false Style/ModuleFunction: Exclude: - yaks/lib/yaks/util.rb Style/MultilineBlockChain: Enabled: false Style/PercentLiteralDelimiters: Exclude: - yaks/lib/yaks/breaking_changes.rb - yaks/spec/unit/yaks/config_spec.rb PreferredDelimiters: '%i': '[]' '%w': '[]' '%W': '[]' Style/PerlBackrefs: Enabled: false Style/Semicolon: AllowAsExpressionSeparator: true Style/SignalException: EnforcedStyle: only_raise Style/SpaceBeforeSemicolon: Enabled: false # FIXME: this should be enforced to either space or no_space Style/SpaceBeforeBlockBraces: Enabled: false # FIXME: this should be enforced to either space or no_space Style/SpaceInsideBlockBraces: Enabled: false # FIXME: make a call whether to fix this one or not Style/SpaceInsideBrackets: Enabled: false Style/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space Style/SpaceInsideStringInterpolation: Enabled: false # FIXME: enforce either single_quotes (fewer fixes) or double_quotes Style/StringLiterals: Enabled: false Style/UnneededPercentQ: Exclude: - yaks/lib/yaks/breaking_changes.rb Style/TrailingComma: Enabled: false # Allow redundant braces in foo.bar({"qux" => "quz"}), when writing # e.g. JSON tests this can be more explicit and clear Style/BracesAroundHashParameters: Exclude: - yaks/spec/**/* Style/SpaceInsideStringInterpolation: Enabled: false Style/LambdaCall: Enabled: false ================================================ FILE: .travis.yml ================================================ language: ruby rvm: - 2.2 - 2.3.4 - 2.4.2 - jruby - jruby-head - ruby-head cache: bundler sudo: false script: bundle exec rake $TASK env: - TASK=yaks:rspec - TASK=yaks-html:rspec - TASK=yaks-sinatra:rspec matrix: allow_failures: - rvm: ruby-head - rvm: jruby-head - rvm: rbx - env: TASK=mutant fast_finish: true include: - rvm: rbx env: TASK="yaks:rspec --trace" - rvm: rbx env: TASK="yaks-html:rspec --trace" - rvm: rbx env: TASK="yaks-sinatra:rspec --trace" - rvm: 2.2 env: TASK=ataru - rvm: 2.2 env: TASK=mutant ================================================ FILE: ADDING_FORMATS.md ================================================ # Adding Extra Output Formats to Yaks Individual output formats are each handled by a dedicated `Yaks::Serializer` class. These take a `Yaks::Resource` as input, and turn it into the requested output format. A `Yaks::Resource` is created by "mapping" domain models by a `Yaks::Mapper`. In a `Yaks::Mapper` subclass a DSL is available to specify how to extract different types of information, for example attributes or links, and store them in a generalized way in a `Resource`. Different formats have different features. Simple formats might just represent attributes, links, and subresources, other formats have queries, forms, or RDF identifiers. If a format represents data of a different nature, then the first step is to decide on a good and straightforward syntax to specify how to derive this data. This can then be stored in a `Yaks::Resource`, and formats that support it can use it, other formats can ignore it. This is already the case, JSON-API ignores links for example. So adding an output format is generally straightforward, as long as the information that the output format supports is already available in `Yaks::Resource`. In that case adding a `Yaks::Serializer::YourFormat` is all that is needed. If the format has features that are not yet available then syntax needs to be added for those features. The guiding idea there is to try and find more than one format with the given feature, to make sure the intermediate abstraction is general and not tied to the specifics and vocabulary of a single format. ================================================ FILE: CHANGELOG.md ================================================ ### master [all changes](http://github.com/plexus/yaks/compare/v0.13.0...master) ### v0.13.0 / 2017-11-13 [all changes](http://github.com/plexus/yaks/compare/v0.12.0...v0.13.0) * Maintenance release to upgrade dependencies ### v0.12.0 / 2016-03-28 [all changes](http://github.com/plexus/yaks/compare/v0.11.0...v0.12.0) * `attribute` can now take an `if` parameter, just like links and forms ### v0.11.0 / 2015-07-09 [all changes](http://github.com/plexus/yaks/compare/v0.10.0...v0.11.0) * Updated JSON-API to match 1.0 format. ([Yohan Robert](https://github.com/groyoh) and [Janko Marohnić](https://github.com/janko-m)) * Added Yaks::Behaviour::OptionalIncludes, to support JSON-API style optional associations ([Janko Marohnić](https://github.com/janko-m)) * Renamed Sinatra::Yaks to Yaks::Sinatra ([Matt Patterson](https://github.com/fidothe)) * Correctly handle 'charset' in Yaks::Sinatra ([Matt Patterson](https://github.com/fidothe)) * Fix rendering of checkboxes in yaks-html * Add integration for testing through RSpec/Capybara/Rack::Test ### v0.10.0 / 2015-05-19 [all changes](http://github.com/plexus/yaks/compare/v0.9.0...v0.10.0) * Updated JSON-API Reader to handle collections * Further changes to bring JSONAPI formatting more in line with 1.0 format - Changed `linked` to `included` - Change format of `links` to include 'linkages' - `included` no longer contains duplicates - Render top level collection links - Don't include "rel" in links output * yaks-html: Make nested resources expand/collapse, various small improvements * yaks-html: render "rel" attributes, making the HTML output more suitable for use in integration tests * In mapper/form declarations: make methods that take a lambda to also accept a block * Instead of representing form fieldset legends as form fields, give them their own class * Introduce `Yaks::Form#fields_flat`, an enumerator to traverse all input fields in a form linearly, e.g. for validation. Will skip over legend elements * Introduce `Yaks::Form#map_fields`, a way to map over fields and fieldsets recursively, yielding a new nested object * Bug fix: `json_serializer` configuration method not working as intended * Improved mapper lookup to deal with model inside namespace * Introduce `mapper_for`, a configuration option for setting up one-off mapper derivation rules * Reify Form::Legend, making it easier to handle form objects with field sets * Improve test coverage. We are now at 97.30% mutation coverage * Improve documentation. Code examples in the README are now verified with Ataru * Make code warning-free ### v0.9.0 / 2015-03-17 Make dynamic form fields respect the order in which they were declared in the form relative to other form fields. Some changes to bring JSONAPI formatting more in line with 1.0 format - Top level key must be named 'data' rather than the resource type - The resource name myst be included in a 'type' attribute Started a Reader for JSONAPI, which can build a resource from JSONAPI input. Add if: options to Form::Field, Form::Fieldset, and Form::Field option, just as on links, associations, and forms. Allow form field details to be expressed in a block, and allow Configurable "setters" to take a block instead of a direct argument. ``` ruby text :first_name, label: 'views.checkout.first_name', required: true, value: ->{ customer_attribute(:first_name) } ``` becomes ``` text :first_name do required true label 'views.checkout.first_name' value { customer_attribute(:first_name) } end ``` This makes the DSL more consistent, since e.g. `label` could already be set in this way, but not `value` or `required`. Prevent `:if` on a form field to be rendered as a form element attribute. ### v0.8.3 / 2015-03-09 The default policy for resolving mappers will now look up superclass names of the object being serialized, so you can define a single mapper to handle a class hierarchy. ### v0.8.2 / 2015-03-02 Various improvements to the HTML formatter - use the form name as a title if there's no title - remove the link styling on rels to indicate they are purely identifiers - link IANA registered rels (indicated by using a symbol) to the IANA list - style the hierarchy in a cleaner way by using a gray left border rather than complete boxes - Add a header that shows the current request method/path - Add a footer that shows the yaks version - show the name/value of hidden form fields - get rid of the all the border-radius, try a new color scheme ### v0.8.1 / 2015-02-20 Add `disabled` as a possible attribute of a select option, so you can render form select controls with disabled options. ### v0.8.0 / 2015-02-18 Allow to use procs for dynamic values in "option" form elements (as used inside a "select"). This makes the form API more consistent. Add an `:if` option to links, to only render them upon a certain condition. Add an `:if` option to forms, and a corresponding `condition` method (it's tricky to have a method called `if`), to only render them upon a certain condition. Add an `:if` option to associtions, to only render them upon a certain condition. ### 0.8.0.beta2 / 2015-01-14 In form select fields, allow the attributes of options to be generated dynamically by passing procs, in line with other form related attributes ### 0.8.0.beta1 / 2015-01-09 Improved form support, HTML form rendering, CJ support. ### 0.8.0.alpha / 2014-12-17 Improved Collection+JSON support, dynamically generated form fields. #### Collection+JSON Carles Jove i Buxeda has done some great work to improve support for Collection+JSON, GET forms are now rendered as CJ queries. #### Dynamic Form Fields A new introduction are "dynamic" form fields. Up to now it was hard to generate forms based on the object being serialized. Now it's possible to add dynamic sections to a Form definition. These will be evaluated at map-time, they receive the object being mapped, and inside the syntax for defining form fields can be used. ``` form :checkout do text :name text :lastname dynamic do |object| object.shipping_options.each do |shipping| radio shipping.type_name, title: shipping.description end end end ``` #### Fieldset and Legend Support for the fieldset element type has been added, which works as you would expect ``` form :foo do fieldset do legend "Hello" text :field_1 end end ``` #### Remove links A link defined in a mapper can be removed in a derived mapper. This is useful when you have a base mapper defining for example 'self' or 'profile' links, but for some derived mappers you don't want these in the output. ``` class BaseMapper link :self, "/api/{mapper_name}/{id}" end class FooMapper < BaseMapper link :self, remove: true end ``` #### Deprecations Internally there the DSL/Config mechanisms have been made more consistent. Yaks::Config is now immutable, much like Yaks::Mapper::Config. Attributes-based classes no long have arity-based hybrid getter/setters. Instead use `with(attr: val)` to set a value. Because of this work, two methods on Yaks::Config are considered deprecated. You will get a warning when using the old name. * json_serializer, use serializer(:json, &...) * namespace, use mapper_namespace #### Experimental read/write support Some work has happened on read/write support, but this is not considered stable yet. ### 0.7.7 / 2014-12-02 General extension and improvements to form handling. Add top level links in Collection+JSON (Carles Jove i Buxeda) The mapper DSL method "control" has been renamed to "form". There is a deprecated alias available. Add Yaks::Resource#find_form for querying a resource for an embedded form by name. Introduce yaks.map() so you can only call the mapping step without running the whole pipeline. ### 0.7.6 / 2014-11-18 Much expanded form support, simplified link DSL, pretty-print objects to Ruby code. Breaking change: using a symbol instead of link template no longer works, use a lambda. link :foo, :bar Becomes link :foo, ->{ bar } Strictly speaking the equivalent version would be `link :foo, ->{ load_attribute(:bar) }`. Depending on if `bar` is implemented on the mapper or is an attribute of the object, this would simplify to `link :foo, ->{ bar }` or `link :foo, ->{ object.bar }` respectively. The form control DSL has been expanded, instead of `field type: 'text'` and similar there are now aliases, e.g. `text :name, value: 'foo'`. All attributes on the form control itself, and on fields, now optionally take a lambda (any `#to_proc`-able) for dynamic content. e.g. control :add_product do method 'POST' action ->{ '/cart/#{cart.id}/line_items' } hidden :product_id, value: -> { product.id } number :quantity, value: 0 end As with lambdas used for links, in case of a zero-arity lambda these evaluate with `self` being the mapper. If the lambda takes an argument the argument will be the mapper, and the lambda is evaluated as a closure. The `href` attribute of a control has been renamed `action`, in line with the attribute name in HTML. An alias is available but will output a deprecation warning. The Yaks::Resource#pp method has been lifted into Attributes so it's available on most immutable Yaks objects. It has also been adapted to produce, in most cases, output that is valid Ruby code. ### 0.7.5 / 2014-11-17 Add the :replace option to link specifications. When used on a link when another link of the same rel was specified previously, then the current link will replace the one (and any other) that was specified earlier. Use case: class BaseMapper < Yaks::Mapper link :self, '/api/{mapper_name}/{id}' end class CartMapper < BaseMapper link :self, '/api/cart', :replace => true end ### 0.7.4 / 2014-11-17 Fix a regression in around hooks introduced in 0.7.0. Improve pretty printing (Yaks::Resource#pp) ### 0.7.3 / 2014-11-11 yaks-sinatra: Allow passing extra Yaks options to the helper method ### 0.7.2 / 2014-11-10 Allow controls to use the same expansion mechanisms that are available in links, i.e. URI templates, symbol referring to a method. Added procs to that list as well. ### 0.7.1 / 2014-11-10 Bugfix in CollectionMapper. ### 0.7.0 / 2014-11-10 #### Introduces yaks-sinatra For easier Sinatra integration. See the respective README for more info. #### Move the rel of subresource into a resource itself Before the subresources in a Yaks::Resource were stored in a hash, keyed by rel. Now the rel is stored as a property of the resource, and the subresources are a simple array. This opens the door to formats that support multiple rels on a resource, and simplifies things as a preparatory step towards bi-directional mapping. This change is mostly transparent to the user, but when implementing custom output formats or doing testing on the resulting Resource instances, you might have to update your code. #### Pass the rack env to steps and hooks Yaks is a pipeline where each step implements the `call` method. Before `call` always received one argument, the previous transformation step's result. Now it receives the Rack env as a second argument. This also applies to before/after/around hooks, although if they are specified as ruby blocks then no change is needed, the second argument will be ignored. #### Handle URI instances After formatting for a JSON output format (e.g. HAL), but before actually serializing to JSON, all data needs to be of a type that has a JSON equivalent, or needs to be handled explicitly with a conversion (known as "primitivizing"). instances of `URI` have been added to this list, they will automatically be represented as JSON strings. ### 0.6.2 / 2014-11-05 Improvements to yaks-html: render form controls, make output prettier. ### 0.6.1 / 2014-10-30 Make sure Resource, NullResource, and CollectionResource have identical public APIs. Create a base Yaks::Error class, and derived classes for specific error categories. This should make it easier to handle errors originating in Yaks. Note that not all code makes use of these yet, so you might still get a StandardError in some cases. ### 0.6.0 / 2014-10-30 v0.6.0 saw some big internal overhaul to make things cleaner and more consistent. It also introduced some new features. #### Form controls We already had templated links which form a limited way of generating parameterized requests. Form controls are more like full HTML forms, e.g. ``` ruby class UserMapper < Yaks::Mapper control :create do href "/foo" method "POST" content_type "application/x-www-form-urlencoded" field :first_name, label: "First name" field :last_name, label: "Last name" end end ``` These are also called actions in some formats. At the moment only one format renders these, a new format called HALO which is en extension of HAL, loosely based on an example by Mike Kelly on how HAL could be extended for this purpose. #### Introduce a HTML output format Provided as a separate gem, `yaks-html` allows Yaks to generate a version of your API that can be browsed from any web browser. This is still very rough around the edges. ### 0.5.0 / 2014-09-18 * Yaks now serializes (returns a string), instead of returning a data structure. This is a preparatory step for supporting non-JSON formats. To get the old behavior back, do this ``` ruby yaks = Yaks.new do skip :serialize end ``` * The old `after` hook has been removed, instead there are now generic hooks for all steps: `before`, `after`, `around`, `skip`; `:map`, `:format`, `:primitivize`, `:serialize`. * By default Yaks uses `JSON.pretty_generate` as a JSON unparser. To use something else, for example `Oj.dump`, do this ``` ruby yaks = Yaks.new do json_serializer &Oj.method(:dump) end ``` * Mapping a non-empty collection will try to infer the type, and hence rel of the nested items, based on the first object in the collection. This is only relevant for formats like HAL that don't have a top-level collection representation, and only matters when mapping a collection at the top level, not when mapping a collection from an association. * Collection+JSON uses a link's "title" attribute to output a link's "name", to better correspond with other formats * When registering a custom format (Yaks::Format subclass), the signature has changed ``` ruby # 0.4.3 Format.register self, :collection_json, 'application/vnd.collection+json' # 0.5.0 register :collection_json, :json, 'application/vnd.collection+json' ``` * `yaks.call` is now the preferred interface, rather than `yaks.serialize`, although there are no plans yet to remove the alias. * The result of a call to `Yaks.new` now responds to `to_proc`, so you can treat it as a Proc/Symbol, e.g. `some_method &yaks` * Improved YARD documentation * 100% mutation coverage :trumpet: :tada: ### 0.4.3 / 2014-08-25 * when specifying a rel_template, instead of allowing for {src} and {dest} fields, now a single {rel} field is expected, which corresponds more with typical usage. ```ruby Yaks.new do rel_template 'http://my-api/docs/relationships/{rel}' end ``` * Yaks::Serializer has been renamed to Yaks::Format * Yaks::Mapper#{map_attributes,map_links,map_subresource} signature has changed, they now are responsible for adding themselves to a resource instance. ```ruby class FooMapper < Yaks::Mapper def map_attributes(resource) resource.update_attributes(:example => 'attribute') end end ``` * Conditionally turn associations into links ```ruby class ShowMapper < Yaks::Mapper has_many :events, href: '/show/{id}/events', link_if: ->{ events.count > 50 } end ``` * Reify `Yaks::Mapper::Attribute` * Remove `Yaks::Mapper#filter`, instead override `#attributes` or `#associations` to filter things out, for example: ```ruby class SongMapper attributes :title, :duration, :lyrics has_one :artist has_one :album def minimal? env['HTTP_PREFER'] =~ /minimal/ end def attributes if minimal? super.reject {|attr| attr.name.equal? :lyrics } # These are instances of Yaks::Mapper::Attribute else super end end def associations return [] if minimal? super end end ``` * Give Attribute, Link, Association a common interface : `add_to_resource(resource, mapper, context)` * Add persistent update methods to `Yaks::Resource` ### v0.4.2 / 2014-06-24 * JSON-API: render self links as href attributes * HAL: render has_one returning nil as null, not as {} * Keep track of the mapper stack, useful for figuring out if mapping the top level response or not, or for accessing parent * Change Serializer.new(resource, options).serialize to Serializer.new(options).call(resource) for cosistency of "pipeline" interface * Make Yaks::CollectionMapper#collection overridable for pagination * Don't render links from custom link methods (link :foo, :method_that_generates_url) that return nil ### v0.4.1 / 2014-06-18 * Change how env is passed to yaks.serialize to match docs * Fix JSON-API bug (#18 reported by Nicolas Blanco) * Don't pluralize has_one association names in JSON-API ### v0.4.0 / 2014-06-17 * Introduce after {} post-processing hook * Streamline interfaces and variable names, especially the use of `call` * Improve deriving mappers automatically, even with Rails style autoloading * Give CollectionResource a members_rel, for HAL-like formats with no top-level collection concept * Switch back to using `src` and `dest` as the rel-template keys, instead of `association_name` * deprecate `mapper_namespace` in favor of `namespace` ### v0.4.0.rc1 / 2014-06-11 * Introduce Yaks.new as the main public interface * Fix JsonApiSerializer and make it compliant with current spec * Remove Hamster dependency, Yaks new uses plain old Ruby arrays and hashes * Remove `RelRegistry` and `ProfileRegistry` in favor of a simpler explicit syntax + policy based fallback * Add more policy derivation hooks, plus make `DefaultPolicy` template for rel urls configurable * Optionally take a Rack env hash, pass it around so mappers can inspect it * Honor the HTTP Accept header if it is present in the rack env * Add map_to_primitive configuration option ### v0.3.0 / 2014-05-15 * Allow partial expansion of templates, expand certain fields, leave others as URI template in the result. ### v0.2.0 / 2014-03-31 * links can now take a simple for a template to compute a link just like an attribute ### v0.1.0 / 2014-03-07 ### v0.0.0 / 2013-12-09 ================================================ FILE: COOKBOOK.md ================================================ # Yaks Cookbook ## Represent Date/Time objects as iso8601 ``` ruby $yaks = Yaks.new do map_to_primitive Date, Time, DateTime, ActiveSupport::TimeWithZone, &:iso8601 end ``` ## Make Yaks' HTML format play nice with CSRF protection Minimum version when using Rack::Protection ``` ruby $yaks = Yaks.new do after :format, :add_csrf_token do |result, env| next result unless result.is_a?(Hexp::Node) && env.key?('rack.session') session = env['rack.session'] session[:csrf] ||= SecureRandom.hex(32) token = session[:csrf] result.replace 'form' do |form| form.append(H[:input, type: :hidden, name: 'authenticity_token', value: token]) end end end ``` Version that covers all cases when using a Rack::Protection protected API mounted inside a Rails app. ``` ruby $yaks = Yaks.new do after :format, :add_csrf_token do |result, env| next result unless result.is_a?(Hexp::Node) && env.key?('rack.session') # Rails uses '_csrf_token' as a key. Rack::Protection uses # :csrf, but will detect and use '_csrf_token' if :csrf is # absent. This works fine as long as a call to Rails is made # before a call to the API is made. When using the HTML # rendering of the API on an empty session and afterwards # switching to Rails though, the '_csrf_token' and :csrf # values will differ, causing Rack::Protection to reject # valid API calls. Hence this little dance to prevent that. session = env['rack.session'] session[:csrf] ||= session['_csrf_token'] || SecureRandom.hex(32) session['_csrf_token'] ||= session[:csrf] token = session[:csrf] result.replace 'form' do |form| form.append(H[:input, type: :hidden, name: 'authenticity_token', value: token]) end end ``` ## Make Yaks' HTML format work with PUT/DELETE/etc. If you're using `Rack::MethodOverride` or something similar, you could drop this in your Yaks config to convert forms so they will work in a browser. ``` ruby after :format, :html_form_methods do |result, env| next result unless result.is_a?(Hexp::Node) result.replace('[method="PUT"],[method="DELETE"],[method="PATCH"]') do |form| form .append(H[:input, type: "hidden", name: "_method", value: form[:method]]) .attr("method", "POST") end end ``` ## Implement Pagination In a hypermedia API the typical way to provide pagination is by adding "previous" and "next" links on a collection. You can do this by implementing your own CollectionMapper ``` module Mappers class CollectionMapper < Yaks::CollectionMapper PAGE_SIZE = 50 link :previous, -> { previous_link } link :next, -> { next_link } def params Rack::Request.new(env).params end def offset params.fetch('offset') { 0 }.to_i end alias full_collection collection def collection # You can implement more efficient page slicing based on DB # layer you're using full_collection.drop(offset).take(PAGE_SIZE) end def count full_collection.count end def previous_link if offset > 0 URITemplate.new("#{env['PATH_INFO']}{?offset}").expand(offset: [offset - PAGE_SIZE, 0].max) end end def next_link if offset + page_size < count URITemplate.new("#{env['PATH_INFO']}{?offset}").expand(offset: offset + PAGE_SIZE) end end end end ``` You can pass this mapper explicitly when calling yaks: `yaks.call(collection, mapper: MyCollectionMapper)`, or leverage the default policy which gives you several options for hooking into mapper resolution. * When implementing a `CollectionMapper` inside your configured mapper namespace, or at the top level if no namespace is confgured, Yaks will use that instead of its vanilla collection mapper * If you're serializing collections of a specific type, you can implement a specific mapper for that. E.g. if you want paging for hypothetical `DatabaseQuerySet`, you can implement a `DatabaseQuerySetMapper` * You can make a `PagedCollection` decorator class, and provide a `PagedCollectionMapper`. This is a great pattern because you can put more of the paging logic inside that object, and override it in subclasses, e.g. to date per month, offset, page, etc. ================================================ FILE: DEVELOPERS.md ================================================ # Yaks Dev Docs This document is for when you want to hack on Yaks itself, or better understand its internals. To simply use it, consult the README. ## Attribs You'll find that most classes in Yaks include an instance of `Attribs`, for example ``` ruby class Yaks::Resource::Link include Attribs.new(:rel, :uri, options: {}) end ``` You can think of this (as a starting point) as replacing `attr_reader`, by adding this line instances of `Link` will have getter methods for `rel`, `uri`, and `options`. But that's really just scratching the surface. `Attribs` relies on Anima, so you get the same things as using `include Anima.new` * a hash-based constructor * getters * equality checks * `to_h` ``` ruby link = Yaks::Resource::Link.new(rel: :self, uri: '/api/cart', options: {templated: false}) link.rel # => :self link.to_h # => {:rel=>:self, :uri=>"/api/cart", :options=>{:templated=>false}} link == Yaks::Resource::Link.new(link.to_h) # => true ``` These last two are important because they make these objects behave like "value objects". They are fully defined by their properties, not by their (object) identity. Note that there are no setters, these objects are immutable. There are some other things that `Attribs` adds that make it a pleasure to work with these objects. * default values * `with` method to create updates * `with_x` convenience methods * `pp` method for representing instances as valid Ruby code * `append_to` method * `to_h_compact` method You can include default values for properties in `Attribs.new(...)`, for example the options of a `Link` default to `{}`. `with` (see [this discussion](https://gist.github.com/plexus/42c6c9c63212182ee440) about why that name was chosen), will create a new object, with certain properties replaced. ``` ruby link2 = link.with(uri: '/foo/bar') link # => #false}> link2 # => #false}> ``` For each property `foo` there's also `with_foo`, so `x.with(foo: 'bar')` is the same as `x.with_foo('bar')` `pp` recursively turns nested `Attribs` based objects into nicely format, valid Ruby code. This is great for debugging, and very helpful when writing test cases. ``` ruby class FooMapper < Yaks::Mapper attributes :a, :b link :self, '/api/foo' has_many :baz form :bar do text :name text :age end end puts FooMapper.config.pp # -- output -- Yaks::Mapper::Config.new( attributes: [ Yaks::Mapper::Attribute.new(name: :a), Yaks::Mapper::Attribute.new(name: :b) ], links: [ Yaks::Mapper::Link.new(rel: :self, template: "/api/foo", options: {}) ], associations: [ Yaks::Mapper::HasMany.new(name: :baz, collection_mapper: nil) ], forms: [ Yaks::Mapper::Form.new( config: Yaks::Mapper::Form::Config.new( name: :bar, fields: [ Yaks::Mapper::Form::Field.new(name: :name, type: :text), Yaks::Mapper::Form::Field.new(name: :age, type: :text) ] ) ) ] ) ``` Because of the common case where new objects need to be added to a list, e.g. a new link, association, form, to the respective property, there's a `append_to` convenience method for that. ``` ruby config = Yaks::Mapper::Config.new config = config.append_to(:attributes, Yaks::Mapper::Attribute.new(name: :a)) config = config.append_to(:attributes, Yaks::Mapper::Attribute.new(name: :b)) puts config.pp # -- output -- Yaks::Mapper::Config.new( attributes: [ Yaks::Mapper::Attribute.new(name: {:name=>:a}), Yaks::Mapper::Attribute.new(name: {:name=>:b}) ] ) ``` Finally `to_h_compact` is similar to `to_h`, but won't output values that are the same as the defaults. So it's the minimal hash for which `foo == foo.class.new(foo.to_h_compact)` holds true. ## The Mapper DSL Now that we know that most objects in Yaks behave in a uniform way, we can leverage that to create the Yaks mapper DSL. As demonstrated in the [example above](#mapper_config_example), most methods like `link`, `has_many`, or `fieldset` simply instantiate an object of a certain type, and add it to a "config" object. For a form `text` input field, the config object is a `Form::Config`, held by the form instance. At the top-level where we have attributes, links, and associations, this config object is an instance of `Yaks::Mapper::Config` held by the mapper subclass. When configuring Yaks itself (through `Yaks.new do ...`), you are creating a `Yaks::Config`, etc. Because the objects created by the DSL all use `Attribs`, their constructor takes a Hash. For the DSL we often prefer positional arguments, however. E.g. `form :create` instead of `form name: :create`. To bridge this gap classes like `Form` implement a class method `create`, with the same signature as the DSL method. Because all these classes implement `create`, we can now generate the DSL methods in a generic way. This is where `Yaks::Configurable` comes in. ## Yaks::Configurable Here's how `Yaks::Mapper` starts ``` ruby module Yaks class Mapper extend Configurable def_add :link, create: Link, append_to: :links def_add :has_one, create: HasOne, append_to: :associations def_add :has_many, create: HasMany, append_to: :associations def_add :attribute, create: Attribute, append_to: :attributes def_add :form, create: Form, append_to: :forms def_set :type def_forward :attributes => :add_attributes def_forward :append_to ``` The `def_add` "macro"[[1](#macro_footnote)] provided by `Yaks::Configurable` will generate a method which * creates an instance of certain class by calling `KlassName.create(...)` * update `config` to a new config which has the instance appended to `config.links` For the case where a DSL method simply needs to overwrite a certain config attribute, use `def_set`. For more involved cases you can implement methods on the Config object that will "update" it in a specific way, returning the updated instance (remember these are all immutable). In that case you generate a DSL method which "forwards" to the config object, hence `def_forward` ## Builder In the case of `Yaks::Mapper`, the config object is stored on each mapper subclass. In other cases the configuration isn't class based though, but instance based. For example, both a `Yaks::Form` and a `Yaks::Form::Fieldset` both have a `Yaks::Form::Config` instance as an attribute. Creating form fields will add them to this config. The block passed to the `form` DSL method will be passed on to `Form.create`. Inside the block a very similar DSL is used as that on a Mapper, but we don't have a class level evaluation context. Instead we create a `Yaks::Builder` and use the `Yaks::Configurable` "macros" to declare how the DSL in this context functions. Finally we ask the builder to evaluate the block, updating the form's config. ``` ruby module Yaks::Mapper::Form ConfigBuilder = Builder.new(Config) do def_set :action, :title, :method, :media_type def_add :field, create: Field::Builder, append_to: :fields def_add :fieldset, create: Fieldset, append_to: :fields # ... end def self.create(*args, &block) args, options = extract_options(args) options[:name] = args.first if args.first.is_a? Symbol config = Config.new(options) config = ConfigBuilder.build(config, &block) new(config: config) end # ... end ``` The builder takes an initial config object, and then evaluates the block, keeping track of the updated config as it evaluates DSL methods. Finally you get the updated config object back. [[2](#state_monad_footnote)] ### footnotes [1] I strongly dislike calling all Ruby class-level methods "macros", especially when they have little to nothing in common with "real" (i.e. syntax tranforming read-time functions) macros. In this case what they achieve is very similar to what you would do with a "real" macro, so I'm rolling with it, adding sarcastic "quotes" to express my self-loathing in doing so. [2] You can think of the Builder as a state monad. I'm sure that helps. ================================================ FILE: FORMATS.org ================================================ #+TITLE:Comparison of Hypermedia Message Formats #+AUTHOR: Arne Brasseur #+email: arne@arnebrasseur.net #+INFOJS_OPT: view:info toc:nil #+BABEL: :session *ruby* :cache yes :results output graphics :exports both :tangle yes * Form Controls / Actions | Format | href | name/id | title/caption | method | media-type | fields | schema | string template | structured template | |-----------------+---------------------------------+----------+---------------+-----------+-----------------+----------------------------------+-------------+-----------------+---------------------| | HTML5 | action="" | | | method="" | enctype="" | yes | | | | | halo | "href" | json key | | "method" | "content-type" | | "schema" | "template" | | | siren | "href" | "name" | "title" | "method" | "type" | "fields" | | | | | mason | "href" | json key | "title" | "method" | depends on type | | "schemaUrl" | | "template" | | Collection+JSON | "href" (query)/current resource | | "prompt" | | | | | | | | Hydra | current resource | "@type" | | "method" | | "expects": {"supportedProperty"} | | | | | Format | Name | Title | Type | Value | |-----------------+------------+----------+---------+---------| | Siren | "name" | | "type" | "value" | | Collection+JSON | "name" | "prompt" | | "value" | | Hydra | "property" | | "range" | | ** HTML [[http://www.w3.org/TR/html5/forms.html][W3C: HTML5 Forms]] ** halo+json [[https://gist.github.com/mikekelly/893552][Gist: A sketch of application/halo+json and application/halo+xml]] #+BEGIN_SRC json { "_controls": { "widgetate": { "href": "/widget/{newID}", "method": "PUT", "content-type": "application/xml", "schema": null, "template": "\\n {{name}}\\n\\n \\n {{#blobs}}\\n \\n {{#first}}\\n true\\n {{/first}}\\n {{contents}}\\n \\n {{/blobs}}\\n \\n\\n {{#is_empty}}\\n This is an empty widget\\n {{/is_empty}}\\n\\n" } } } #+END_SRC ** Siren [[https://github.com/kevinswiber/siren][Siren Home page]] #+BEGIN_SRC json { "actions": [ { "name": "add-item", "title": "Add Item", "method": "POST", "href": "http://api.x.io/orders/42/items", "type": "application/x-www-form-urlencoded", "fields": [ { "name": "orderNumber", "type": "hidden", "value": "42" }, { "name": "productCode", "type": "text" }, { "name": "quantity", "type": "number" } ] } ] } #+END_SRC ** Mason #+BEGIN_SRC json { "@actions": { "is:delete-issue": { "type": "void", "href": "...", "method": "DELETE", "title": "Delete issue" } } } #+END_SRC #+BEGIN_SRC json { "@actions": { // JSON action with schema reference "is:project-create": { "type": "json", "href": "...", "title": "Create new project", "schemaUrl": "..." }, // JSON action with default template "is:update-project": { "type": "json", "href": "...", "title": "Update project details", "template": { "Code": "SHOP", "Title": "Webshop", "Description": "All issues related to the webshop." } } } } #+END_SRC https://github.com/JornWildt/Mason/blob/master/Documentation/Mason-draft-1.md#actions ** Collection+JSON CJ does not have a form-like representation. It does allow resources to contain a "template", which is really a list of form fields, and a client can use HTTP methods against the same endpoint to perform CRUD operations. In addition CJ provides "queries" for basic GET based operations. #+BEGIN_SRC json { "template" : { "data" : [ {"prompt" : STRING, "name" : STRING, "value" : VALUE}, {"prompt" : STRING, "name" : STRING, "value" : VALUE}, ... {"prompt" : STRING, "name" : STRING, "value" : VALUE} ] } } #+END_SRC #+BEGIN_SRC json { "queries" : [ { "href" : "http://example.org/search", "rel" : "search", "prompt" : "Enter search string", "data" : [ {"name" : "search", "value" : ""} ] } ] } #+END_SRC ** JSON-LD + Hydra Example taken from [[http://sookocheff.com/posts/2014-03-11-on-choosing-a-hypermedia-format/][this blog post]] JSON-LD itself does not have form like controls, only linking. Hydra introduces an "operation" property for this purpose. #+BEGIN_SRC json { "@context": [ "http://www.w3.org/ns/hydra/core", { "@vocab": "https://schema.org/", "image": { "@type": "@id" }, "friends": { "@type": "@id" } } ], "@id": "https://api.example.com/player/1234567890/friends", "operation": { "@type": "BefriendAction", "method": "POST", "expects": { "@id": "http://schema.org/Person", "supportedProperty": [ { "property": "name", "range": "Text" }, { "property": "alternateName", "range": "Text" }, { "property": "image", "range": "URL" } ] } } } #+END_SRC ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' gemspec path: 'yaks' gemspec path: 'yaks-html' gemspec path: 'yaks-sinatra' # Transit depends on Oj, which is not available for JRuby unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' gemspec path: 'yaks-transit' end if RUBY_VERSION < '2' gem 'mime-types', [ '>= 2.6.2', '< 3' ] end # gem 'mutant', github: 'mbj/mutant' # gem 'mutant-rspec', github: 'mbj/mutant' ================================================ FILE: IDENTIFIERS.md ================================================ # Identifiers In Yaks, and Hypermedia message formats in general, a number of different types of identifiers are used. Some are full URIs and correspond with well defined specs. Some are just short identifers that are easy to program with. Understanding these types of identifiers is key to creating a unifying model of a "Resource" that can be shared across output formats. We want to unify as much as possible across formats, without conflating things that are really not the same. This document reflects my current limited understanding of things, based on possibly incorrect assumptions. Feedback is more than welcome. ## rels As used in HTML and Atom, these identifiers say what the relationship is between a resource and another resource it links to. There is a [registry of names](http://www.iana.org/assignments/link-relations/link-relations.xhtml), e.g. self, next, profile, stylesheet. Custom rels need to be fully qualified URLs. Keep in mind that these are simply opaque identifiers, but by using a known protocol like http they can be used to point at documentation. Some examples ``` copyright stylesheet http://api.example.com/rel/author http://api.example.com/api-docs/relationships#comment custom_scheme:foo /order ``` The last example is a relative URL, which would have to be expanded against the source URL of the document it is mentioned in. In Yaks both links and subresources are specified with their rel(ationship). ```ruby class PersonMapper < Yaks::Mapper link :self, '/people/{id}' link 'http://api.example.com/rels#friends', '/people/{id}/friends' has_one :address, rel: 'http://api.example.com/rels#address' end ``` For subresources the rel can be omitted, in which case it will be inferred based on the rel_template: ```ruby $yaks = Yaks.new do rel_template 'http://api.example.com/rels/{dest}' end ``` Links and subresources are rendered keyed by rel in HAL and Collection+JSON. JSON-API renders `self` links as the `href` of a resource. ## profiles A specific IANA registered rel type is profile. > Profile: Identifying that a resource representation conforms to a certain profile, without affecting the non-profile semantics of the resource representation. Profile basically adds a layer of semantics on top of the hypermedia message format (e.g. HAL, Collection+JSON), which in turns defines semantics on top of a serialization format (JSON, XML, EDN). Loosely speaking it could be seen as the "type" or "class". For example if you know the profile of a resource, you might know you can expect to find a "name", "date_of_birth", or "post_body" field. ## "type" Despite the appealing rigor of having fully qualified URIs to identify things, sometimes you just want to call a person a `person`. In Yaks we call these short identifier the *type* for lack of a better word. In some cases, notably JSON-API, they are used literally in the output. More often they are used to derive full URIs based on a template. The type of a mapper is inferred from its class name, but can be set explicitly as well. ```ruby class CatMapper < Yaks::Mapper end # type = "cat" ``` ```ruby class CatMapper < Yaks::Mapper type 'feline' end # type => "feline" ``` ## rdf class RDF (Resource Description Framework) is a set of specifications for use in "semantic web" applications. RDF is based on "ontologies" that precisely define a "vocabulary" of "classes" and "predicates". An example class identifier for all Merlot wines could be > http://www.w3.org/TR/2004/REC-owl-guide-20040210/wine#Merlot (source [wikipedia](http://en.wikipedia.org/wiki/Resource_Description_Framework)) Not currently used by Yaks, but might become important when implementing support for JSON-LD or other RDF serialization formats. ## CURIES CURIES are "compact uris". The HAL format uses this so it can have the rigor of fully specified rels, with the ease of use of short-name "type" identifiers. The mechanism is similar to how one specifies and uses XML namespaces. From the HAL spec: ```json { "_links": { "self": { "href": "/orders" }, "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }], "next": { "href": "/orders?page=2" }, "ea:find": { "href": "/orders{?id}", "templated": true }, "ea:admin": [{ "href": "/admins/2", "title": "Fred" }, { "href": "/admins/5", "title": "Kate" }] } } ``` In this case "ea:find" is just a shorthand for "http://example.com/docs/rels/find". ================================================ FILE: LICENSE ================================================ Copyright (c) 2013-2014 Arne Brasseur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Rakefile ================================================ require 'yaks' require 'yaks-html' require 'yaks-sinatra' require 'yaks-transit' require 'rspec/core/rake_task' require 'rubocop/rake_task' require 'rubygems/package_task' require 'yard' def delegate_task(gem, task_name) task task_name do chdir gem.to_s do sh "rake", task_name.to_s end end end [:yaks, :"yaks-html", :"yaks-sinatra"].each do |gem| namespace gem do desc 'Run rspec' delegate_task gem, :rspec desc 'Build gem' delegate_task gem, :gem desc 'Generate YARD docs' delegate_task gem, :yard desc 'push gem to rubygems' task :push => "#{gem}:gem" do sh "gem push pkg/#{gem}-#{Yaks::VERSION}.gem" end end end desc "Tag current release and push to Github" task :tag do sh "git tag v#{Yaks::VERSION}" sh "git push --tags" end desc "Tag, build, and push all gems to rubygems.org" task :push_all => [ :tag, "yaks:gem", "yaks-html:gem", "yaks-sinatra:gem", "yaks:push", "yaks-html:push", "yaks-sinatra:push" ] task :push => :push_all desc "Run all the tests" task :rspec => ["yaks:rspec", "yaks-html:rspec", "yaks-sinatra:rspec"] desc 'Run mutation tests' delegate_task :yaks, :mutant desc "Start a console" task :console do require 'irb' require 'irb/completion' ARGV.clear IRB.start end task :ataru do require "ataru" Dir.chdir("yaks") Ataru::CLI::Application.start(["check", "README.md"]) end RuboCop::RakeTask.new do |task| task.options << '--display-cop-names' end task :default => [:rspec, :rubocop] ================================================ FILE: bench/bench.rb ================================================ require 'benchmark/ips' require 'yaks' require_relative '../spec/acceptance/models' require_relative '../spec/fixture_helpers' Benchmark.ips do |x| $yaks = Yaks.new input = FixtureHelpers.load_yaml_fixture 'confucius' x.report "Simple HAL mapping" do $yaks.serialize(input) end end ================================================ FILE: bench/bench_1000.rb ================================================ #!/usr/bin/env ruby require 'English' require 'benchmark/ips' require 'ruby-prof' require 'yaks' SIZE = 20 $timestamp = Time.now.utc.iso8601.gsub('-', '').gsub(':', '') $yaks = Yaks.new FlatModel = Struct.new(:field1, :field2) DeepModel = Struct.new(:field, :next) flat = SIZE.times.map do |i| FlatModel.new(i, 'x' * (i % 50)) end deep = nil SIZE.times do |i| deep = DeepModel.new(i, deep) end class FlatMapper < Yaks::Mapper attributes :field1, :field2 link :self, '/model/{field1}' end class DeepMapper < Yaks::Mapper attributes :field link :self, '/model/{field}' has_one :next, mapper: DeepMapper end def profile!(name) RubyProf.start yield results = RubyProf.stop File.open "/tmp/#{name}-#{$timestamp}.out.#{$PROCESS_ID}", 'w' do |file| RubyProf::CallTreePrinter.new(results).print(file) end end do_flat = ->(format) { -> { $yaks.serialize(flat, item_mapper: FlatMapper, format: format) } } do_deep = ->(format) { -> { $yaks.serialize(deep, mapper: DeepMapper, format: format) } } 10.times { do_flat[:hal][] } 10.times { do_deep[:hal][] } profile!('flat', &do_flat.(:hal)) profile!('deep', &do_deep.(:hal)) exit Benchmark.ips(10) do |job| Yaks::Format.names.each do |format| job.report "#{format} ; #{SIZE} objects in a list ; no nesting", &do_flat.(format) job.report "#{format} ; #{SIZE} objects nested", &do_deep.(format) end end ================================================ FILE: code_of_conduct.md ================================================ # Contributor Code of Conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) ================================================ FILE: notes.org ================================================ 0.4 * DONE Get rid of profile/rel registry, use policy instead * DONE pass around policy explicitly instead of through options * DONE introduce name/type separate from profile ** DONE mapper ** DONE Resource * DONE allow setting rel types directly on associations, with fallback to policy * DONE switch to hash-based init of Resource to make it more extensible * DONE add Resource#type * DONE not 100% happy yet about nameing of mapper#mapper_name / config#name. Maybe use `type` across the board? * DONE Fix JsonAPISerializer * top-level automatic links, e.g. for self and profile * make HAL plural/singular links configurable from the Yaks.new * make primitivize configuration instance based, not global * Have JsonApi add self links as href: attributes * Move examples to acceptance tests * Select mapper based on content type * move to 100% mutcov pre 0.5 * CURIES/namespaces Ticketsolve::Api::Yaks = ::Yaks.new do policy do def derive... end hal_options.plural_link '...' primitivize Date, Time do |o| o.iso8601 end rel_template "http://literature.example.com/rel/#{association_name}" link :self, "http://api.com/{key}/{id}" link :profile, "http://api.com/profile/{key}" # and/or derive_rel_from_association do |mapper, association| "http://literature.example.com/rel/#{association.name}" end end * DONE 59 lib/yaks/mapper.rb * DONE 92 lib/yaks/mapper/link.rb * DONE 37 lib/yaks/mapper/association.rb * DONE 3 lib/yaks/version.rb * DONE 13 lib/yaks/mapper/has_many.rb * DONE 9 lib/yaks/mapper/has_one.rb * DONE 79 lib/yaks/config.rb * 79 lib/yaks/mapper/config.rb * 73 lib/yaks.rb * DONE 72 lib/yaks/util.rb * DONE 65 lib/yaks/collection_resource.rb * 59 lib/yaks/json_api_serializer.rb * 59 lib/yaks/hal_serializer.rb * 43 lib/yaks/primitivize.rb * 37 lib/yaks/mapper/class_methods.rb * DONE 33 lib/yaks/collection_mapper.rb * 28 lib/yaks/null_resource.rb * DONE 27 lib/yaks/resource.rb * 25 lib/yaks/resource/link.rb * DONE 23 lib/yaks/fp.rb * 22 lib/yaks/serializer.rb * 15 lib/yaks/shared_options.rb * 15 lib/yaks/default_policy.rb * 10 lib/yaks/mapper/map_links.rb http://www.dragoart.com/tuts/4344/1/1/how-to-draw-a-yak.htm https://www.google.com/search?q=yak+head&num=20&source=lnms&tbm=isch&sa=X&ei=uVoAVbyWLcTiO-v0gYgF&ved=0CAcQ_AUoAQ&biw=1758&bih=923&dpr=1.09#imgdii=_&imgrc=CtgMobmQThXw_M%253A%3BzyYGcwgq1IAU_M%3Bhttps%253A%252F%252Fcallaocafeandmarket.files.wordpress.com%252F2014%252F02%252Fyak2.jpg%3Bhttps%253A%252F%252Fcallaocafeandmarket.wordpress.com%252Fweather%252F%3B563%3B539 https://www.google.com/search?q=hair+over+eyes&num=20&source=lnms&tbm=isch&sa=X&ei=p2sBVajOOIKvPdDjgPgL&ved=0CAcQ_AUoAQ&biw=1758&bih=923&dpr=1.09#imgdii=_&imgrc=la9hTGBvOniv9M%253A%3B_nu4vLbBOXzIeM%3Bhttp%253A%252F%252Fwww.rampantscotland.com%252Fdiary%252Fgraphics%252Fhighland_cow_baldernock_x3481d.jpg%3Bhttp%253A%252F%252Fwww.rampantscotland.com%252Fdiary%252Fdiary_photos_july11b.htm%3B514%3B496 https://www.google.com/search?q=hair+over+eyes&num=20&source=lnms&tbm=isch&sa=X&ei=p2sBVajOOIKvPdDjgPgL&ved=0CAcQ_AUoAQ&biw=1758&bih=923&dpr=1.09#imgdii=_&imgrc=RmzHL5NICZcanM%253A%3BwMr__Up6S7s_9M%3Bhttp%253A%252F%252Fwww.peakdistrictonline.co.uk%252Fimages%252Fwildlife%252Fanimals%252FHighland_Cattle_In_The_Peak_District_5.jpg%3Bhttp%253A%252F%252Fwww.peakdistrictonline.co.uk%252Ffarm-animals-highland-cattle-c101118.html%3B620%3B412 https://www.google.com/search?biw=1758&bih=923&tbm=isch&sa=1&q=yak+horns&oq=yak+horns&gs_l=img.3..0j0i24.118958.120138.0.120293.9.8.0.1.1.0.142.713.6j2.8.0.msedr...0...1c.1.62.img..0.9.701.rHB0v54VT9k#imgdii=_&imgrc=c8D4U_x1gob0YM%253A%3BzvdhmtL1g8r_UM%3Bhttp%253A%252F%252F4.bp.blogspot.com%252F_2Sr5OicZPHQ%252FTFT2H5ViKWI%252FAAAAAAAAAFs%252FfYlG-en9mFk%252Fs1600%252Fyak%252Band%252BDri.jpg%3Bhttp%253A%252F%252Fthewodakpa.blogspot.com%252F2010%252F07%252Ftibetan-yak_29.html%3B1080%3B672 https://www.google.com/search?biw=1758&bih=923&tbm=isch&sa=1&q=yak+horns&oq=yak+horns&gs_l=img.3..0j0i24.118958.120138.0.120293.9.8.0.1.1.0.142.713.6j2.8.0.msedr...0...1c.1.62.img..0.9.701.rHB0v54VT9k#imgdii=_&imgrc=_N83z2DYkcc7DM%253A%3BDV8K7BLZksSGrM%3Bhttp%253A%252F%252Fthumbs.dreamstime.com%252Fz%252Fyak-stuffed-animal-like-bull-23182676.jpg%3Bhttp%253A%252F%252Fwww.dreamstime.com%252Froyalty-free-stock-image-yak-stuffed-animal-like-bull-image23182676%3B1300%3B957 * JSON-LD / RDF ** Useful protocols used in RDF.rb - to_rdf, to_uri - RDF::URI pname, qname - RDF::Vocabulary::Term http://ruby-rdf.github.io/ https://github.com/ruby-rdf/json-ld/ http://schema.org/MusicEvent https://developers.google.com/structured-data/events/venues http://json-ld.org/playground/index.html http://www.iana.org/assignments/relation/ http://microformats.org/wiki/rel- and http://microformats.org/profile/ ================================================ FILE: shared/rake_tasks.rb ================================================ require 'yaks' require 'yaks-html' require 'rubygems/package_task' require 'rspec/core/rake_task' require 'yard' def mutant_task(_gem) require 'mutant' task :mutant do pattern = ENV.fetch('PATTERN', 'Yaks*') opts = ENV.fetch('MUTANT_OPTS', '').split(' ') requires = %w[-ryaks -ryaks/behaviour/optional_includes] args = %w[-Ilib --use rspec --score 100] + requires + opts + [pattern] result = Mutant::CLI.run(args) raise unless result == Mutant::CLI::EXIT_SUCCESS end end def gem_tasks(gem) Gem::PackageTask.new(Gem::Specification.load("#{gem}.gemspec")) do |task| task.package_dir = '../pkg' end mutant_task(gem) if RUBY_ENGINE == 'ruby' && RUBY_VERSION >= "2.1.0" RSpec::Core::RakeTask.new(:rspec) do |t, _task_args| t.rspec_opts = "-Ispec" t.pattern = "spec" end YARD::Rake::YardocTask.new do |t| t.files = ["lib/**/*.rb" "**/*.md"] t.options = %w[--output-dir ../doc] end end ================================================ FILE: shared/rspec_config.rb ================================================ require 'rspec/its' require 'bogus/rspec' require 'timeout' # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |rspec| # Set the FULLSTACK environment variable to prevent RSpec from # filtering stack traces. This can be useful to debug errors that # happen inside third party libraries rspec.backtrace_exclusion_patterns = [] if ENV['FULLSTACK'] # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching rspec.disable_monkey_patching! # Make sure we stay up to date rspec.raise_errors_for_deprecations! rspec.expect_with :rspec do |expectations| # This option will default to `true` in RSpec 4. It makes the `description` # and `failure_message` of custom matchers include text for helper methods # defined using `chain`, e.g.: # be_bigger_than(2).and_smaller_than(4).description # # => "be bigger than 2 and smaller than 4" # ...rather than: # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end # This is configured for us by including bogus/rspec. We do not include rspec-mocks. # rspec.mock_with :bogus # Mutated code can lead to infinite loops. Consider tests that run # too long as having failed if defined?(Mutant) rspec.around(:each) do |example| Timeout.timeout(1, &example) end end end ================================================ FILE: yaks/.rspec ================================================ -r spec_helper ================================================ FILE: yaks/README.md ================================================ [![Gem Version](https://badge.fury.io/rb/yaks.png)][gem] [![Build Status](https://secure.travis-ci.org/plexus/yaks.png?branch=master)][travis] [![Code Climate](https://codeclimate.com/github/plexus/yaks.png)][codeclimate] [![Gitter](https://badges.gitter.im/Join Chat.svg)][gitter] [gem]: https://rubygems.org/gems/yaks [travis]: https://travis-ci.org/plexus/yaks [codeclimate]: https://codeclimate.com/github/plexus/yaks [gitter]: https://gitter.im/plexus/yaks?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge # Yaks The library that understands hypermedia. **If you use Yaks please help out by filling out the [Yaks Users Survey](https://docs.google.com/forms/d/1sZB03Vf32igmNmJ7RP8mo8H4VZHcVIpSrUSbvx2xD8s/viewform)** Yaks takes your data and transforms it into hypermedia formats such as HAL, JSON-API, or HTML. It allows you to build APIs that are discoverable and browsable. It is built from the ground up around linked resources, a concept central to the architecture of the web. Yaks consists of a resource representation that is independent of any output type. A Yaks mapper transforms an object into a resource, which can then be serialized into whichever output format the client requested. These formats are presently supported: * HAL * JSON API * Collection+JSON * HTML * HALO * Transit ## Table of Contents - [State of Development](#user-content-state-of-development) - [Concepts](#user-content-concepts) - [Mappers](#user-content-mappers) - [Attributes](#user-content-attributes) - [Forms](#user-content-forms) - [Filtering](#user-content-filtering) - [Links](#user-content-links) - [Associations](#user-content-associations) - [Behaviours](#user-content-behaviours) - [Calling Yaks](#user-content-calling-yaks) - [Rack env](#user-content-rack-env) - [Namespace](#user-content-namespace) - [Custom attribute/link/subresource handling](#user-content-custom-attributelinksubresource-handling) - [Resources, Formatters, Serializers](#user-content-resources-formatters-serializers) - [Formats](#user-content-formats) - [HAL](#user-content-hal) - [HTML](#user-content-html) - [JSON-API](#user-content-json-api) - [Collection+JSON](#user-content-collection-json) - [Transit](#user-content-transit) - [Hooks](#user-content-hooks) - [Policy over Configuration](#user-content-policy-over-configuration) - [derive_mapper_from_object](#user-content-derive_mapper_from_object) - [derive_mapper_from_association](#user-content-derive_mapper_from_association) - [derive_rel_from_association](#user-content-derive_rel_from_association) - [Primitivizer](#user-content-primitivizer) - [Integration](#user-content-integration) - [Real World Usage](#user-content-real-world-usage) - [Demo](#user-content-demo) - [Cookbook](#user-content-cookbook) - [Standards Based](#user-content-standards-based) - [How to contribute](#user-content-how-to-contribute) - [License](#user-content-license) ## Packages - [yaks-sinatra](yaks-sinatra/README.md) - [yaks-html](yaks-html/README.md) - [yaks-transit](yaks-transit/README.md) ## State of Development Recent focus has been on stabilizing the core classes, improving format support, and increasing test (mutation) coverage. We are committed to a stable public API and semantic version. On the 0.x line the minor version is bumped when non-backwards compatible changes are introduced. After 1.x regular semver conventions will be used. ## Concepts Yaks is a processing pipeline, you create and configure the pipeline, then feed data through it. ``` ruby yaks = Yaks.new do default_format :hal rel_template 'http://api.example.com/rels/{rel}' format_options(:hal, plural_links: [:copyright]) mapper_namespace ::MyAPI json_serializer do |data| JSON.dump(data) end end yaks.call(product) ``` Yaks performs this serialization in three steps * It *maps* your data to a `Yaks::Resource` * It *formats* the resource to a syntax tree representation * It *serializes* to get the final output For JSON types, the "syntax tree" is just a combination of Ruby primitives, nested arrays and hashes with strings, numbers, booleans, nils. A Resource is an abstraction shared by all output formats. It can contain key-value attributes, RFC5988 style links, and embedded sub-resources. To build an API you create a "mapper" for each type of object you want to represent. Yaks takes care of the rest. For all configuration options see [Yaks::Config::DSL](http://rdoc.info/gems/yaks/frames/Yaks/Config/DSL). See also the [API Docs on rdoc.info](http://rdoc.info/gems/yaks/frames/file/README.md) ## Mappers Say your app has a `Post` object for blog posts. To serve posts over your API, define a `PostMapper` ```ruby class PostMapper < Yaks::Mapper link :self, '/api/posts/{id}' attributes :id, :title has_one :author has_many :comments end ``` Configure a Yaks instance and start serializing! ```ruby yaks = Yaks.new yaks.call(post) ``` or a bit more elaborate ```ruby yaks = Yaks.new do default_format :json_api rel_template 'http://api.example.com/rels/{rel}' format_options(:hal, plural_links: [:copyright]) end yaks.call(post, mapper: ::PostMapper, format: :hal) ``` ### Attributes Use the `attribute` or `attributes` DSL methods to specify which attributes of your model you want to expose, as in the example above. You can override the `load_attribute` method to change how attributes are fetched from the model. For example, if you are representing data that is stored in a Hash, you could do ```ruby class PostHashMapper < Yaks::Mapper attributes :id, :body # @param name [Symbol] def load_attribute(name) object[name] end end ``` The `attribute` method may also take a block that will be called with the context of the mapper instance. The default implementation will use the block if provided, otherwise it will first try to find a matching method for an attribute on the mapper itself, and will then fall back to calling the actual model. So you can add extra 'virtual' attributes like so : ```ruby class CommentMapper < Yaks::Mapper attributes :body, :date attribute :id do "Id-#{object.id}" end def date object.created_at.strftime("at %I:%M%p") end end ``` ### Forms Mapper can contain form defintions, for formats that support them. The form DSL mimics the HTML5 field and attribute names. ```ruby class PostMapper < Yaks::Mapper attributes :id, :body, :date form :add_comment do action '/api/comments' method 'POST' media_type 'application/json' text :body hidden :post_id, value: -> { object.id } end end ``` TODO: add more info on form element types, attributes, conditional rendering of forms, dynamic form sections, ... #### Filtering You can override `#attributes`, or `#associations`. ```ruby class SongMapper < Yaks::Mapper attributes :title, :duration, :lyrics has_one :artist has_one :album def minimal? env['HTTP_PREFER'] =~ /minimal/ end # @return Array def attributes return super.reject {|attr| attr.name.equal? :lyrics } if minimal? super end # @return Array def associations return [] if minimal? super end end ``` ### Links You can specify link templates that will be expanded with model attributes. The link relation name should be a registered [IANA link relation](http://www.iana.org/assignments/link-relations/link-relations.xhtml) or a URL. The template syntax follows [RFC6570 URI templates](http://tools.ietf.org/html/rfc6570). ```ruby class FooMapper < Yaks::Mapper link :self, '/api/foo/{id}' link 'http://api.foo.com/rels/comments', '/api/foo/{id}/comments' end ``` To prevent a link to be expanded, add `expand: false` as an option. Now the actual template will be rendered in the result, so clients can use it to generate links from. To partially expand the template, pass an array with field names to expand. e.g. ```ruby class ProductMapper < Yaks::Mapper link 'http://api.foo.com/rels/line_item', '/api/line_items?product_id={product_id}&quantity={quantity}', expand: [:product_id] end # "_links": { # "http://api.foo.com/rels/line_item": { # "href": "/api/line_items?product_id=273&quantity={quantity}", # "templated": true # } # } ``` You can pass a proc instead of a template, in that case the proc will be resolved in the context of the mapper. What this means is that, if the proc takes no arguments, it will be evaluated with the mapper instance as the value of `self`. If the proc does take an argument, then it will receive the mapper instance, and will be evaluated as a closure, i.e. with access to the scope in which it was defined. ```ruby class FooMapper < Yaks::Mapper link 'http://api.foo.com/rels/go_home', -> { home_url } # by default calls object.home_url def home_url object.setting('home_url') end end ``` To only include links based on certain conditions, add an `:if` option, passing it a block. The block will be resolved in the context of the mapper, as explained before. For example, say you want to notify the consumer of your API that upon confirming an order, the previously held cart is no longer valid, you could use the IANA standard `invalidates` rel to communicate this. ``` ruby class OrderMapper < Yaks::Mapper link :invalidates, '/api/cart', if: ->{ env['api.invalidate_cart'] } end ``` ### Associations Use `has_one` for an association that returns a single object, or `has_many` for embedding a collection. Options * `:mapper` : Use a specific for each instance, will be derived from the class name if omitted (see Policy vs Configuration) * `:collection_mapper` : For mapping the collection as a whole, this defaults to Yaks::CollectionMapper, but you can subclass it for example to add links or attributes on the collection itself * `:rel` : Set the relation (symbol or URI) this association has with the object. Will be derived from the association name and the configured rel_template if ommitted * `:if`: Only render the association if a condition holds * `:link_if`: Conditionally render the association as a link. A `:href` option is required ```ruby class ShowMapper < Yaks::Mapper has_many :events, href: '/show/{id}/events', link_if: ->{ events.count > 50 } end ``` ### Behaviours Yaks provides mixins to change how your mappers work. These need to be required separately, they are not loaded by default. #### OptionalIncludes You may choose to not render associations by default, but to only do so when the client explicitly asks for them. This can be done by including `Yaks::Behaviour::OptionalIncludes`. Which associations to load is specified with the the `include` query parameter. You can use dots to load nested associated. ```ruby require "yaks/behaviour/optional_includes" class PostMapper < Yaks::Mapper include Yaks::Behaviour::OptionalIncludes has_one :author has_many :comments end class AuthorMapper < Yaks::Mapper include Yaks::Behaviour::OptionalIncludes has_one :profile end ``` ``` GET /post/42?include=comments,author.profile ``` Note that this will only work when Yaks has access to the Rack environment. When using an existing integration like `yaks-sinatra` this will be handled for you. To force an association to always be included, override its `if` condition to always return true. ```ruby require "yaks/behaviour/optional_includes" class PostMapper < Yaks::Mapper include Yaks::Behaviour::OptionalIncludes has_one :author has_many :comments, if: ->{ true } end ``` ## Calling Yaks Once you have a Yaks instance, you can call it with `call` (`serialize` also works but might be deprecated in the future.) Pass it the data to be serialized, plus options. * `:env` a Rack environment, see next section * `:format` the format to be used, e.g. `:json_api`. Note that if the Rack env contains an `Accept` header which resolves to a recognized format, then the header takes precedence * `:mapper` the mapper to be used. Will be inferred if omitted * `:item_mapper` When rendering a collection, the mapper to be used for each item in the collection. Will be inferred from the class of the first item in the collection if omitted. ### Rack env When serializing, Yaks lets you pass in an `env` hash, which will be made available to all mappers. ```ruby class FooMapper < Yaks::Mapper attributes :bar def bar if env['something'] #... end end end yaks = Yaks.new yaks.call(foo, env: my_env) ``` The env hash will be available to all mappers, so you can use this to pass around context. In particular context related to the current HTTP request, e.g. the current logged in user, which is why the recommended use is to pass in the Rack environment. If `env` contains a `HTTP_ACCEPT` key (Rack's way of representing the `Accept` header), Yaks will return the format that most closely matches what was requested. ## Namespace Yaks by default will find your mappers for you if they follow the naming convention of appending 'Mapper' to the model class name. This (and all other "conventions") can be easily redefined though, see the policy section. If you have your mappers inside a module, use `mapper_namespace`. ```ruby module API module Mappers class PostMapper < Yaks::Mapper #... end end end yaks = Yaks.new do mapper_namespace API::Mappers end ``` If your namespace contains a `CollectionMapper`, Yaks will use that instead of `Yaks::CollectionMapper`, e.g. ```ruby module API module Mappers class CollectionMapper < Yaks::CollectionMapper link :profile, 'http://api.example.com/profiles/collection' end end end ``` You can also have collection mappers based on the type of members the collection holds, e.g. ```ruby module API module Mappers class LineItemCollectionMapper < Yaks::CollectionMapper link :profile, 'http://api.example.com/profiles/line_items' attributes :total def total collection.inject(0) do |memo, line_item| memo + line_item.price * line_item.quantity end end end end end ``` Yaks will automatically detect and use this collection when serializing an array of `LineItem` objects. See derive_mapper_from_object for details. ## Custom attribute/link/subresource handling When inheriting from `Yaks::Mapper`, you can override `map_attributes`, `map_links` and `map_resources` to skip (or augment) above methods, and instead implement your own custom mechanism. These methods take a `Yaks::Resource` instance, and should return an updated resource. They should not alter the resource instance in-place. For example ```ruby class ErrorMapper < Yaks::Mapper link :profile, '/api/error' def map_attributes(resource) attrs = { http_code: 500, message: object.to_s, type: object.class.name.underscore } case object when AllocationException attrs[:http_code] = 422 when ActiveRecord::RecordNotFound attrs[:http_code] = 404 attrs[:type] = "record_not_found" end resource.update_attributes(attrs) end end ``` ## Resources, Formatters, Serializers Yaks uses an intermediate "Resource" representation to support multiple output formats. A mapper turns a domain model into a `Yaks::Resource`. A formatter (e.g. `Yaks::Format::Hal`) takes the resource and outputs the structure of the target format. Finally a serializer will take this document structure and turn it into a string. For JSON documents the intermediate format consists of Ruby primitives like arrays and hashes. HTML/XML based formats on the other hand return a [Hexp::Node](https://github.com/plexus/hexp). For JSON based format there's an extra step between `format` and `serialize` called `primitivize`, this way Ruby objects which don't have an equivalent in the JSON spec, like `Symbol` or `Date`, can be turned into objects that are representable in JSON. See [Primitiver](#primitivizer). ## Formats Below follows a brief overview of formats that are available in Yaks. The maturity of these formats varies, since we depend on people that use a certain format actively to contribute. Implementing formats is in generally straightforward, and consists mostly of deciding how the attributes, links, forms, of a `Yaks::Resource` should be represented. Depending on the format this might be a subject for debate. We welcome these discussions, and if your opinion differs from what ends up in Yaks, it should be trivial to change these representations for your use case. ### HAL This is the default. In HAL one decides when building an API which links can only be singular (e.g. self), and which are always represented as an array. Yaks defaults to singular as I've found it to be the most common case. If you want specific links to be plural, then configure their rel href as such. ```ruby hal = Yaks.new do format_options :hal, plural_links: ['http://api.example.com/rels/foo'] end ``` CURIEs are not explicitly supported (yet), but it's possible to use them with some manual effort. The line between a singular resource and a collection is fuzzy in HAL. To stick close to the spec you're best to create your own singular types that represent collections, rather than rendering a top level CollectionResource. Yaks also has a derived format called HALO, which is a non-standard extension to HAL which includes form elements. ### HTML The hypermedia format *par excellence*. Yaks can generate a version of your API, including links and forms, that is usable straight from a standard web browser. This allows API interactions to be developed and tested independent from any client application. If you let Yaks handle your content type negotiation (i.e. pass it the rack env, and honour the content type it detects, see [integration](#integration), simply opening a browser and pointing it at your API entry point should do the trick. ### JSON-API ```ruby Yaks.new do default_format :json_api end ``` JSON-API has no concept of outbound links, so these will not be rendered. Instead the key will be inferred from the mapper class name by default. This can be changed per mapper: ```ruby class AnimalMapper < Yaks::Mapper type :pet end ``` Or the policy can be overridden: ```ruby yaks = Yaks.new do derive_type_from_mapper_class do |mapper_class| piglatinize(mapper_class.to_s.sub(/Mapper$/, '')) end end ``` For optional includes, see [`Yaks::Behaviour::OptionalIncludes`](#user-content-behaviours). ### Collection+JSON Collection+JSON has support for write templates. To use them, the `:template` option can be used. It will map the specified form to a CJ template. Please notice that CJ only allows one template per representation. ```ruby Yaks.new do default_format :collection_json collection_json = Yaks.new do format_options :collection_json, template: :my_template_form end end class PostMapper < Yaks::Mapper form :my_template_form do # This will be used for template end form :not_my_template do # This won't be used for template end end ``` Subresources aren't mapped because Collection+JSON doesn't really have that concept. ### Transit There is experimental support for Transit. The transit gem handles serialization internally, so there is no intermediate document. The `format` step already returns the serialized string. ## Hooks It is possible to hook into the Yaks pipeline to perform extra processing steps before, after, or around each step. It also possible to skip a step. ``` ruby yaks = Yaks.new do # Automatically give every resource a self link after :map, :add_self_link do |resource| resource.add_link(Yaks::Resource::Link.new(:self, "/#{resource.type}/#{resource.attributes[:id]}")) end # Skip serialization, so Ruby primitives come back instead of JSON # This was the default before versions < 0.5.0 skip :serialize end ``` ## Policy over Configuration It's an old adage in the Ruby/Rails world to have "Convention over Configuration", mostly to derive values that were not given explicitly. Typically based on things having similar names and a 1-1 derivable relationship. This saves a lot of typing, but for the uninitiated it can also create confusion, the implicitness makes it hard to follow what's going on. What's worse, is that often the Configuration part is skipped entirely, making it very hard to deviate from the Golden Standard. There is another old adage, "Policy vs Mechanism". Implement the mechanisms, but don't dictate the policy. In Yaks whenever missing values need to be inferred, like finding an unspecified mapper for a relation, this is handled by a policy object. The default is `Yaks::DefaultPolicy`, you can go there to find all the rules of inference. Single rules of inference can be redefined directly in the Yaks configuration: ```ruby yaks = Yaks.new do mapper_for Post, SpecialMapper derive_mapper_from_object do |model| # ... end derive_mapper_from_collection do |collection| # ... end derive_mapper_from_item do |model| # ... end derive_type_from_mapper_class do |mapper_class| # ... end derive_mapper_from_association do |association| # ... end derive_rel_from_association do |mapper, association| # ... end end ``` Note that within these blocks, you may call `super()` which would call the default implementation. You can also subclass or create from scratch your own policy class ```ruby class MyPolicy < Yaks::DefaultPolicy #... end yaks = Yaks.new do policy_class MyPolicy end ``` ### derive_mapper_from_object This is called when trying to serialize something and no explicit mapper is given. To recap, it's always possible to be explicit, e.g. ``` yaks.call(widget, mapper: WidgetMapper) yaks.call(array_of_widgets, mapper: MyCollectionMapper, item_mapper: WidgetMapper) ``` If the mapper is left unspecified, Yaks will inspect whatever you pass it. First it will test the given object against the mappings defined using `mapper_for`. If no mapper is found, it will call `derive_mapper_from_item` or `derive_mapper_from_collection` depending on whether the given object is a collection or not. If the object responds to `to_ary` it is considered a collection. ### mapper_for This method allows you to define a one-to-one mapping between a mapping rule and a mapper class. During the lookup, Yaks will check if any mapping rule matches the given object using the `#===` operator. Here are a few examples on how to use it: ```ruby yaks = Yaks.new do mapper_for(:home, HomeMapper) mapper_for(Post, SpecialMapper) mapper_for(->(author) { author.respond_to?(:name) && author.name == 'doh' }, AuthorMapper) end yaks.call(:home) # would map using HomeMapper yaks.call(Post.new) # would map using PostMapper yaks.call(Author.new(name: 'doh')) # would map using AuthorMapper ``` ### derive_mapper_from_collection This method will try various constant lookups based on naming. These all happen in the configured namespace, which defaults to the Ruby top level. If the first object in the collection has a class of `Widget`, and the configured namespace is `API`, then these are tried in turn * `API::WidgetCollectionMapper` * `API::CollectionMapper` * `Yaks::CollectionMapper` Note that Yaks can only find a specific collection mapper for a type if the collection passed to Yaks contains at least one element. If it's important that empty collections are handled by the right mapper (e.g. to set a specific `self` or `profile` link), then you have to be explicit. ### derive_mapper_from_item When using this method, the lookup happens based on the class name, and will traverse up the class hierarchy in the configured namespace if no suitable mapper is found. Take the following code: ```ruby module Stuff class Thing ; end class Widget < Thing ; end end ``` The lookup we'll be done as followed. * If the `namespace` option is set (to `Mappers` for example): * `Mappers::Stuff::WidgetMapper` * `Mappers::Stuff::ThingMapper` * `Mappers::Stuff::ObjectMapper` * `Mappers::Stuff::BasicObjectMapper` * `Mappers::WidgetMapper` * `Mappers::ThingMapper` * If the `namespace` option is not set: * `Stuff::WidgetMapper` * `Stuff::ThingMapper` * `Stuff::ObjectMapper` * `Stuff::BasicObjectMapper` * `WidgetMapper` * `ThingMapper` If none of these are found an error is raised. ### derive_mapper_from_association When no mapper is specified for an association, then this method is called to find the right mapper, based on the association name. In case of `has_many` collections this is the "item mapper", the collection mapper is resolved using `derive_mapper_from_object`. By default the mapper class is derived from the name of the association, e.g. ``` has_many :widgets #=> WidgetMapper has_one :widget #=> WidgetMapper ``` It is always possible to explicitly set a mapper. ``` has_one :widget, mapper: FooMapper has_many :widgets, collection_mapper: MyCollectionMapper, mapper: FooMapper ``` ### derive_rel_from_association Associations have a "rel", an IANA registered identifier or fully qualified URI, that specifies how the object relates to the parent document. When configuring Yaks one can set a `rel_template`, that will be used to generate these rels if not explicitly given. The `rel` placeholder in the template will be substituted with the association name. ``` ruby yaks = Yaks.new do rel_template "http://api.example.com/rel/{rel}" end class MyMapper < Yaks::Mapper # rel: "http://api.example.com/rel/widgets" has_many :widgets # rel: "http://api.example.com/rel/widget" has_one :widget end ``` ## Primitivizer For JSON based formats, the "syntax tree" is merely a structure of Ruby primitives that have a JSON equivalent. If your mappers return non-primitive attribute values, you can define how they should be converted. For example, JSON has no notion of dates. If your mappers return these types as attributes, then Yaks needs to know how to turn these into primitives. To add extra types, use `map_to_primitive` Here's an example with a custom `Currency` class, which can be represented as an integer. ```ruby Yaks.new do map_to_primitive Currency do |currency| currency.to_i end end ``` One notable use case is representing dates and times. The JSON specification does not define any syntax for these, so the only solution is to represent them either as numbers or strings. If you're not sure what to do with these then the ISO8601 standard is a safe bet. It defines a way to represent times and dates as strings, and is also adopted by the W3C in [RFC3339](http://tools.ietf.org/html/rfc3339). An alternative representation that is sometimes used is "unix time", defined as the numbers of seconds passed since 1 January 1970. Here's an example for a Rails app, so including ActiveSupport's `TimeWithZone`. ```ruby Yaks.new do map_to_primitive Date, Time, DateTime, ActiveSupport::TimeWithZone, &:iso8601 end ``` `map_to_primitive` can also be used to transform alternative data structures, like those from [Hamster](https://github.com/hamstergem/hamster), into Ruby arrays and hashes. Use `call()` to recursively turn things into primitives. ```ruby Yaks.new do map_to_primitive Hamster::Vector, Hamster::List do |list| list.map do |item| call(item) end end end ``` Yaks by default "primitivizes" symbols (as strings), and classes that include Enumerable (as arrays). ## Integration It is recommended to let Yaks handle the negotiation of media types, so that consumer can request the format they prefer using an `Accept:` header. To do this requires two steps: first make sure you pass the rack env to Yaks, this way it will detect any `Accept` header and honor it. While this is enough to get the correct serialized output, it will likely be served up with the wrong `Content-Type` header by your web framework. To fix this, ask Yaks first for the "runner" for a given input, then get the media type and serialized resource from the runner. ```ruby # Tell your web framework about the supported formats Yaks::Format.all.each do |format| mime_type format.format_name, format.media_type end # one time Yaks configuration yaks = Yaks.new # on each request runner = yaks.runner(post, env: rack_env) format = runner.format_name output = runner.call ``` ## Real World Usage Yaks is used in production by * [Ticketsolve](http://www.ticketsolve.com/). You can find an example API endpoint [here](http://leicestersquaretheatre.ticketsolve.com/api). * Advertile Mobile for their product AppBounty (internal API) ## Demo You can find an outdated example app at [Yakports](https://github.com/plexus/yakports), or browse the HAL api directly using the [HAL browser](http://yaks-airports.herokuapp.com/browser.html). ## Cookbook See the [cookbook](COOKBOOK.md) for some usage examples taking from a real world app. ## Standards Based Yaks is based on internet standards, including * [RFC4288 Media types](http://tools.ietf.org/html/rfc4288) * [RFC5988 Web Linking](http://tools.ietf.org/html/rfc5988) * [RFC6906 The "profile" link relation](http://tools.ietf.org/search/rfc6906) * [RFC6570 URI Templates](http://tools.ietf.org/html/rfc6570) * [RFC4229 HTTP Header Field Registrations](http://tools.ietf.org/html/rfc4229). ## How to contribute Run the tests, the examples, try it with your own stuff and leave your impressions in the issues. To fix a bug 1. Fork the repo 2. Fix the bug, add tests for it 3. Push it to a named branch 4. Add a PR To add a feature 1. Open an issue as soon as possible to gather feedback 2. Same as above, fork, push to named branch, make a pull-request Yaks uses [Mutation Testing](https://github.com/mbj/mutant). Run `rake mutant` and look for percentage coverage. In general this should only go up. ## License MIT License (Expat License), see [LICENSE](./LICENSE) ![](shaved_yak.gif) ================================================ FILE: yaks/Rakefile ================================================ load '../shared/rake_tasks.rb' gem_tasks(:yaks) task :mutant_chunked do # No subjects: # Yaks, # Yaks::Error, # Yaks::IllegalStateError, # Yaks::UnsupportedOperationError, # Yaks::PrimitivizeError, # Yaks::Undefined, # Yaks::HTML5Forms, # Hangs: # Yaks::Changelog, # 100% verified: # Yaks::Util, # Yaks::Util::Deprecated, # Yaks::FP # Yaks::FP::Callable, # Yaks::DefaultPolicy, # Yaks::Mapper::HasOne, # Yaks::Mapper::HasMany, # Yaks::Mapper::Attribute, # Yaks::Mapper::Config, # Yaks::Mapper::ClassMethods, # Yaks::Mapper::AssociationMapper, # Yaks::Format::CollectionJson, # Yaks::Config, # Yaks::Config::DSL, # Yaks::Attributes::InstanceMethods, # Yaks::Configurable, # Yaks::NullResource, # Yaks::Runner, [ # >> Yaks::Attributes # >> Yaks::Attributes::InstanceMethods # >> Yaks::Builder # >> Yaks::CollectionMapper # >> Yaks::CollectionResource # >> Yaks::Config # >> Yaks::Configurable # >> Yaks::DefaultPolicy # >> Yaks::Error # >> Yaks::FP # >> Yaks::FP::Callable # >> Yaks::Format # >> Yaks::Format::CollectionJson # >> Yaks::Format::HTML # >> Yaks::Format::Hal # >> Yaks::Format::Halo # >> Yaks::Format::JsonAPI # >> Yaks::Format::Reader # >> Yaks::Format::Transit # >> Yaks::Format::Transit::ReadHandler # >> Yaks::Format::Transit::WriteHandler # >> Yaks::HTML5Forms # >> Yaks::IllegalStateError # >> Yaks::Mapper # >> Yaks::Mapper::Association # >> Yaks::Mapper::AssociationMapper # >> Yaks::Mapper::Attribute # >> Yaks::Mapper::Config # >> Yaks::Mapper::Form # >> Yaks::Mapper::Form::Config # >> Yaks::Mapper::Form::Field # >> Yaks::Mapper::Form::Field::Option # >> Yaks::Mapper::Form::Fieldset # >> Yaks::Mapper::HasMany # >> Yaks::Mapper::HasOne # >> Yaks::Mapper::Link # >> Yaks::NullResource # >> Yaks::Pipeline # >> Yaks::Primitivize # >> Yaks::PrimitivizeError # >> Yaks::Reader # >> Yaks::Reader::Hal # Yaks::Resource, # Yaks::Resource::Form, # Yaks::Resource::Form::Field, # Yaks::Resource::Form::Field::Option, # Yaks::Resource::Form::Fieldset, # Yaks::Resource::Link, Yaks::Resource::HasFields, # >> Yaks::Runner # >> Yaks::RuntimeError # >> Yaks::Serializer # >> Yaks::Serializer::JSONReader # >> Yaks::Serializer::JSONWriter # >> Yaks::Undefined # >> Yaks::UnsupportedOperationError # >> Yaks::Util # >> Yaks::Util::Deprecated ].each do |space| puts space ENV['PATTERN'] = "#{space}" Rake::Task["mutant"].execute break end end ================================================ FILE: yaks/ataru_setup.rb ================================================ # "Require your project source code, with the correct path" require "yaks" require "hamster" Post = Struct.new(:id, :title, :author, :comments) Author = Struct.new(:name) module MyAPI Product = Struct.new(:id, :label) class ProductMapper < Yaks::Mapper attributes :id, :label end end class AuthorMapper < Yaks::Mapper end class CommentMapper < Yaks::Mapper end class PostMapper < Yaks::Mapper link :self, '/api/posts/{id}' attributes :id, :title has_one :author has_many :comments end class HomeMapper < Yaks::Mapper; end class SpecialMapper < Yaks::Mapper; end module ActiveSupport class TimeWithZone < Time ; end end class Currency ; end module Setup def setup # Do some nice setup that is run before every snippet # If you'd like to use instance variables define them here, e.g # @important_variable_i_will_use_in_my_code_snippets = true end def teardown # Do some cleanup that is run after every snippet end # If you like local variables you can define methods, e.g # def number_of_wishes # 101 # end def my_env {'something' => true} end alias_method :rack_env, :my_env def post Post.new(7, "Yaks is Al Dente", nil, []) end alias_method :foo, :post def product MyAPI::Product.new(42, "Shiny thing") end # # Tell your web framework about the supported formats # Yaks::Format.all.each do |format| # mime_type format.format_name, format.media_type # end def mime_type(*_args) end end ================================================ FILE: yaks/find_missing_tests.rb ================================================ #!/usr/bin/env ruby require 'mutant' require 'pry' # These are private methods that are tested by other methods in the same class SKIP = %w[ Yaks::CollectionMapper#collection_rel Yaks::CollectionMapper#collection_type Yaks::CollectionMapper#mapper_for_model Yaks::Resource::Form::Field#select_options_for_value Yaks::Mapper::AssociationMapper#add_link Yaks::Mapper::AssociationMapper#add_subresource Yaks::Mapper::Link#resource_link_options ] args = ["-Ilib", "-ryaks", "--use", "rspec", "Yaks*"] env = Mutant::Env::Bootstrap.call(Mutant::CLI.call(args)) integration = env.config.integration integration.setup binding.pry if integration.all_tests.empty? # rubocop:disable Lint/Debugger env.subjects.each do |subject| match_expression = subject.match_expressions.first subject_tests = integration.all_tests.select do |test| match_expression.prefix?(test.expression) end unless subject_tests.any? || SKIP.include?(subject.expression.syntax) puts subject.identification exit if ARGV.include?("-1") end end ================================================ FILE: yaks/lib/yaks/behaviour/optional_includes.rb ================================================ require "rack/utils" module Yaks module Behaviour module OptionalIncludes RACK_KEY = "yaks.optional_includes".freeze def associations super.select do |association| association.if != Undefined || include_association?(association) end end private def include_association?(association) includes = env.fetch(RACK_KEY) do query_string = env.fetch("QUERY_STRING", nil) query = Rack::Utils.parse_query(query_string) env[RACK_KEY] = query.fetch("include", "").split(",").map { |r| r.split(".") } end includes.any? do |relationship| relationship[mapper_stack.size].eql?(association.name.to_s) end end end end end ================================================ FILE: yaks/lib/yaks/breaking_changes.rb ================================================ module Yaks # These are displayed in a post-install message when installing the # gem to aid upgraiding BreakingChanges = { '0.7.6' => %q~ Breaking Changes in Yaks 0.7.6 ============================== Breaking change: using a symbol instead of link template no longer works, use a lambda. link :foo, :bar Becomes link :foo, ->{ bar } Strictly speaking the equivalent version would be `link :foo, ->{ load_attribute(:bar) }`. Depending on if `bar` is implemented on the mapper or is an attribute of the object, this would simplify to `link :foo, ->{ bar }` or `link :foo, ->{ object.bar }` respectively. The `href` attribute of a control has been renamed `action`, in line with the attribute name in HTML. An alias is available but will output a deprecation warning. ~, '0.7.0' => %q~ Breaking Changes in Yaks 0.7.0 ============================== Yaks::Resource#subresources is now an array, not a hash. The rel is stored on the resource itself as Yaks::Resource#rels (an array). This should only be of concern if you implement custom output formats The general signature of all processing steps (mapper, formatter, hooks) has changed to incldue a second parameter, the rack env. If you have custom implementations of any of these, or hooks that are not specified as ruby blocks, you will need to take this into account ~, '0.5.0' => %q~ Breaking Changes in Yaks 0.5.0 ============================== Yaks now serializes its output, you no longer have to convert to JSON yourself. Use `skip :serialize' to get the old behavior, or `json_serializer` to use a different JSON implementation. The single `after' hook has been replaced with a set of `before', `after', `around' and `skip' hooks. If you've created your own subclass of `Yaks::Format' (previously: `Yaks::Serializer'), then you need to update the call to `Format.register'. These are potentially breaking changes. See the CHANGELOG and README for full documentation. ~, '0.4.3' => %q~ Breaking Changes in Yaks 0.4.3 ============================== Yaks::Mapper#filter was removed, if you override this method in your mappers to conditionally filter attributes or associations, you will have to override #attributes or #associations instead. When specifying a rel_template, now a single {rel} placeholder is expected instead of {src} and {dest}. There are other internal changes. See the CHANGELOG and README for full documentation. ~ } BreakingChanges['0.4.4'] = BreakingChanges['0.4.3'] BreakingChanges['0.7.1'] = BreakingChanges['0.7.0'] end ================================================ FILE: yaks/lib/yaks/builder.rb ================================================ module Yaks # State monad-ish thing. # # Generate a DSL syntax for immutable classes. # # @example # # # This code # Form.create(:search) # .method("POST") # .action("/search") # # # Can be written as # Builder.new(Form, [:method, :action]).create(:search) do # method "POST" # action "/search" # end # class Builder include Configurable def initialize(klass, methods = [], &block) @klass = klass @methods = methods def_forward(*methods) if methods.any? instance_eval(&block) if block end def create(*args, &block) build(@klass.create(*args), &block) end def build(init_state, *extra_args, &block) @config = init_state instance_exec(*extra_args, &block) if block @config end def inspect "#" end end end ================================================ FILE: yaks/lib/yaks/changelog.rb ================================================ module Yaks module Changelog module_function def current versions[Yaks::VERSION] end def versions markdown.split(/(?=###\s*[\d\w\.]+\n)/).each_with_object({}) do |section, hsh| version = section.each_line.first[/[\d\w\.]+/] log = section.each_line.drop(1).join.strip hsh[version] = log end end def markdown Pathname(__FILE__).join('../../../../CHANGELOG.md').read end end end ================================================ FILE: yaks/lib/yaks/collection_mapper.rb ================================================ module Yaks class CollectionMapper < Mapper alias_method :collection, :object # @param [Array] collection # @return [Array] def call(collection, _env = nil) @object = collection attrs = { type: collection_type, members: collection().map do |obj| mapper_for_model(obj).new(context).call(obj) end } # For collections from associations the rel will be based on the # association. At the top level there's no association, so we # use a generic rel. This matters especially for HAL, where a # top-level collection is rendered as an object with the # collection as a subresource. attrs[:rels] = [collection_rel] if context[:mapper_stack].empty? map_attributes( map_links( CollectionResource.new(attrs) ) ) end private def collection_rel if collection_type policy.expand_rel(pluralize(collection_type)) else 'collection' end end def collection_type if item_mapper = context[:item_mapper] item_mapper.config.type || policy.derive_type_from_mapper_class(item_mapper) else policy.derive_type_from_collection(collection) end end def mapper_for_model(model) context.fetch(:item_mapper) do policy.derive_mapper_from_object(model) end end end end ================================================ FILE: yaks/lib/yaks/collection_resource.rb ================================================ module Yaks # A collection of Resource objects, it has members, and its own set of link # relations like self and profile describing the collection. # # A collection can be the top-level result of an API call, like all posts to # a blog, or a subresource collection, like the comments on a post result. # class CollectionResource < Resource include attributes.add(members: []) extend Forwardable def_delegators :members, :each, :map, :each_with_object # @return [Boolean] def collection? true end def seq self end end end ================================================ FILE: yaks/lib/yaks/config.rb ================================================ module Yaks class Config extend Yaks::Util::Deprecated include Yaks::FP::Callable, Attribs.new( format_options_hash: Hash.new({}), default_format: :hal, policy_options: {}, policy_class: DefaultPolicy, primitivize: Primitivize.create, serializers: Serializer.all, hooks: [] ) class << self alias_method :create, :new end deprecated_alias :namespace, :mapper_namespace def format_options(format, options) with(format_options_hash: format_options_hash.merge(format => options)) end def serializer(type, &serializer) with(serializers: serializers.merge(type => serializer)) end def json_serializer(&serializer) serializer(:json, &serializer) end %w[before after around skip].map(&:intern).each do |hook_type| define_method hook_type do |step, name = :"#{hook_type}_#{step}", &block| append_to(:hooks, [hook_type, step, name, block]) end end def rel_template(template) with(policy_options: policy_options.merge(rel_template: template)) end def mapper_namespace(namespace) with(policy_options: policy_options.merge(namespace: namespace)) end def mapper_for(rule, mapper_class) policy_options[:mapper_rules] ||= {} mapper_rules = policy_options[:mapper_rules].merge(rule => mapper_class) with(policy_options: policy_options.merge(mapper_rules: mapper_rules)) end def map_to_primitive(*args, &block) with(primitivize: primitivize.dup.tap { |prim| prim.map(*args, &block) }) end DefaultPolicy.public_instance_methods(false).each do |method| define_method method do |&block| with( policy_class: Class.new(policy_class) do define_method method, &block end ) end end # @return [Yaks::DefaultPolicy] def policy @policy ||= @policy_class.new(@policy_options) end def runner(object, options) Runner.new(config: self, object: object, options: options) end # Main entry point into yaks # # @param object [Object] The object to serialize # @param options [Hash] Serialization options # # @option env [Hash] The rack environment # @option format [Symbol] The target format, default :hal # @option mapper [Class] Mapper class to use # @option item_mapper [Class] Mapper class to use for items in a top-level collection # def call(object, options = {}) runner(object, options).call end alias_method :serialize, :call def map(object, options = {}) runner(object, options).map end def format(data, options = {}) runner(data, options).format end def read(data, options = {}) runner(data, options).read end end end ================================================ FILE: yaks/lib/yaks/configurable.rb ================================================ module Yaks # A "Configurable" class is one that keeps a configuration in a # separate immutable object, of type class::Config. say you have # # class MyMapper < Yaks::Mapper # # use yaks configuration DSL in here # end # # The links, associations, etc, that you set up for MyMapper, will # be available in MyMapper.config, which is an instance of # Yaks::Mapper::Config. # # Each configuration step, like `link`, `has_many`, will replace # MyMapper.config with an updated version, discarding the old # config. # # By extending Configurable, a number of "macros" become available # to describe the DSL that subclasses can use. See the docs for # `def_set`. `def_forward`, and `def_add`. module Configurable attr_accessor :config def self.extended(child) child.config = child::Config.new end def inherited(child) child.config = config end # Create a DSL method to set a certain config property. The # generated method will take either a plain value, or a block, # which will be captured and stored instead. def def_set(*method_names) method_names.each do |method_name| define_singleton_method method_name do |arg = Undefined, &block| if arg.equal?(Undefined) unless block raise ArgumentError, "setting #{method_name}: no value and no block given" end self.config = config.with(method_name => block) else if block raise ArgumentError, "ambiguous invocation setting #{method_name}: give either a value or a block, not both." end self.config = config.with(method_name => arg) end end end end # Forward a method to the config object. This assumes the method # will return an updated config instance. # # Either takes a list of methods to forward, or a mapping (hash) # of source to destination method name. def def_forward(mappings, *names) if mappings.instance_of? Hash mappings.each do |method_name, target| define_singleton_method method_name do |*args, &block| self.config = config.public_send(target, *args, &block) end end else def_forward([mappings, *names].map{|name| {name => name}}.inject(:merge)) end end # Generate a DSL method that creates a certain type of domain # object, and adds it to a list on the config. # # def_add :fieldset, create: Fieldset, append_to: :fields # # This will generate a `fieldset` method, which will call # `Fieldset.create`, and append the result to `config.fields` def def_add(name, options) old_verbose, $VERBOSE = $VERBOSE, false # skip method redefinition warning define_singleton_method name do |*args, &block| defaults = options.fetch(:defaults, {}) klass = options.fetch(:create) if args.last.instance_of?(Hash) args[-1] = defaults.merge(args[-1]) else args << defaults end self.config = config.append_to( options.fetch(:append_to), klass.create(*args, &block) ) end ensure $VERBOSE = old_verbose end end end ================================================ FILE: yaks/lib/yaks/default_policy.rb ================================================ module Yaks class DefaultPolicy include Util # Default policy options. DEFAULTS = { rel_template: "rel:{rel}", namespace: Object, mapper_rules: {} } # @!attribute [r] # @return [Hash] attr_reader :options # @param options [Hash] options def initialize(options = {}) @options = DEFAULTS.merge(options) end # Main point of entry for mapper derivation. Calls # derive_mapper_from_collection or derive_mapper_from_item # depending on the model. # # @param model [Object] # @return [Class] A mapper, typically a subclass of Yaks::Mapper # # @raise [RuntimeError] occurs when no mapper is found def derive_mapper_from_object(model) mapper = detect_configured_mapper_for(model) return mapper if mapper return derive_mapper_from_collection(model) if model.respond_to? :to_ary derive_mapper_from_item(model) end # Derives a mapper from the given collection. # # @param collection [Object] # @return [Class] A mapper, typically a subclass of Yaks::Mapper def derive_mapper_from_collection(collection) if m = collection.first name = "#{m.class.name.split('::').last}CollectionMapper" begin return @options[:namespace].const_get(name) rescue NameError # rubocop:disable Lint/HandleExceptions end end begin return @options[:namespace].const_get(:CollectionMapper) rescue NameError # rubocop:disable Lint/HandleExceptions end CollectionMapper end # Derives a mapper from the given item. This item should not # be a collection. # # @param item [Object] # @return [Class] A mapper, typically a subclass of Yaks::Mapper # # @raise [RuntimeError] only occurs when no mapper is found for the given item. def derive_mapper_from_item(item) klass = item.class namespaces = klass.name.split("::")[0...-1] begin return build_mapper_class(namespaces, klass) rescue NameError klass = next_class_for_lookup(item, namespaces, klass) retry if klass end raise_mapper_not_found(item) end # Derive the a mapper type name # # This returns the 'system name' for a mapper, # e.g. ShowEventMapper => show_event. # # @param [Class] mapper_class # # @return [String] def derive_type_from_mapper_class(mapper_class) underscore(mapper_class.name.split('::').last.sub(/Mapper$/, '')) end # Derive the mapper type name from a collection # # This inspects the first element of the collection, so # it requires a collection with truthy elements. Will # return `nil` if the collection has no truthy elements. # # @param [#first] collection # # @return [String|nil] # # @raise [NameError] def derive_type_from_collection(collection) return if collection.none? derive_type_from_mapper_class(derive_mapper_from_object(collection.first)) end def derive_mapper_from_association(association) @options[:namespace].const_get("#{camelize(association.singular_name)}Mapper") end # @param association [Yaks::Mapper::Association] # @return [String] def derive_rel_from_association(association) expand_rel(association.name) end # @param relname [String] # @return [String] def expand_rel(relname) URITemplate.new(@options[:rel_template]).expand(rel: relname) end private def build_mapper_class(namespaces, klass) mapper_class = "#{klass.name.split('::').last}Mapper" [*namespaces, mapper_class].inject(@options[:namespace]) do |namespace, module_or_class| namespace.const_get(module_or_class, false) end end def next_class_for_lookup(item, namespaces, klass) superclass = klass.superclass return superclass if superclass < Object return nil if namespaces.empty? namespaces.clear item.class end def raise_mapper_not_found(item) namespace = "#{@options[:namespace]}::" unless Object.equal?(@options[:namespace]) mapper_class = "#{namespace}#{item.class}Mapper" raise "Failed to find a mapper for #{item.inspect}. Did you mean to implement #{mapper_class}?" end def detect_configured_mapper_for(object) @options[:mapper_rules].each do |rule, mapper_class| return mapper_class if rule === object # rubocop:disable Style/CaseEquality end nil end end end ================================================ FILE: yaks/lib/yaks/errors.rb ================================================ module Yaks Error = Class.new(StandardError) IllegalStateError = Class.new(Error) RuntimeError = Class.new(Error) UnsupportedOperationError = Class.new(Error) PrimitivizeError = Class.new(Error) end ================================================ FILE: yaks/lib/yaks/format/collection_json.rb ================================================ module Yaks class Format class CollectionJson < self register :collection_json, :json, 'application/vnd.collection+json' include FP # @param [Yaks::Resource] resource # @return [Hash] def serialize_resource(resource) result = { version: "1.0", items: serialize_items(resource) } result[:href] = resource.self_link.uri if resource.self_link result[:links] = serialize_links(resource) if links?(resource) result[:queries] = serialize_queries(resource) if queries?(resource) result[:template] = serialize_template(resource) if template?(resource) {collection: result} end # @param [Yaks::Resource] resource # @return [Array] def serialize_items(resource) resource.seq.map do |item| attrs = item.attributes.map do |name, value| { name: name, value: value } end result = {data: attrs} result[:href] = item.self_link.uri if item.self_link item.links.each do |link| next if link.rel.equal? :self result[:links] = [] unless result.key?(:links) result[:links] << {rel: link.rel, href: link.uri} result[:links].last[:name] = link.title if link.title end result end end def serialize_links(resource) resource.links.each_with_object([]) do |link, result| result << {href: link.uri, rel: link.rel} end end def serialize_queries(resource) resource.forms.each_with_object([]) do |form, result| next unless form_is_query? form result << {rel: form.name, href: form.action} result.last[:prompt] = form.title if form.title form.fields_flat.each do |field| result.last[:data] = [] unless result.last.key? :data result.last[:data] << {name: field.name, value: nil.to_s} result.last[:data].last[:prompt] = field.label if field.label end end end def queries?(resource) resource.forms.any? { |f| form_is_query? f } end def links?(resource) resource.collection? && resource.links.any? end def template?(resource) options.key?(:template) && template_form_exists?(resource) end protected def form_is_query?(form) form.method?(:get) && form.has_action? end def template_form_exists?(resource) !resource.find_form(options.fetch(:template)).nil? end def serialize_template(resource) fields = resource.find_form(options.fetch(:template)).fields result = {data: []} fields.each do |field| result[:data] << {name: field.name, value: nil.to_s} result[:data].last[:prompt] = field.label if field.label end result end end end end ================================================ FILE: yaks/lib/yaks/format/hal.rb ================================================ module Yaks class Format # Hypertext Application Language (http://stateless.co/hal_specification.html) # # A lightweight JSON Hypermedia message format. # # Options: +:plural_links+ In HAL, a single rel can correspond to # a single link, or to a list of links. Which rels are singular # and which are plural is application-dependant. Yaks assumes all # links are singular. If your resource might contain multiple # links for the same rel, then configure that rel to be plural. In # that case it will always be rendered as a collection, even when # the resource only contains a single link. # # @example # # yaks = Yaks.new do # format_options :hal, {plural_links: [:related_content]} # end # class Hal < self register :hal, :json, 'application/hal+json' def transitive? true end def inverse Yaks::Reader::Hal.new end protected # @param [Yaks::Resource] resource # @return [Hash] def serialize_resource(resource) # The HAL spec doesn't say explicitly how to deal missing values, # looking at client behavior (Hyperagent) it seems safer to return an empty # resource. # result = resource.attributes if resource.links.any? result = result.merge(_links: serialize_links(resource.links)) end if resource.collection? result = result.merge(_embedded: serialize_embedded([resource])) elsif resource.subresources.any? result = result.merge(_embedded: serialize_embedded(resource.subresources)) end result end # @param [Array] links # @return [Hash] def serialize_links(links) links.reduce({}, &method(:serialize_link)) end # @param [Hash] memo # @param [Yaks::Resource::Link] # @return [Hash] def serialize_link(memo, link) hal_link = {href: link.uri} hal_link.merge!(link.options) memo[link.rel] = if singular?(link.rel) hal_link else (memo[link.rel] || []) + [hal_link] end memo end # @param [String] rel # @return [Boolean] def singular?(rel) !options.fetch(:plural_links) { [] }.include?(rel) end # @param [Array] subresources # @return [Hash] def serialize_embedded(subresources) subresources.each_with_object({}) do |sub, memo| memo[sub.rels.first] = if sub.collection? sub.map(&method(:serialize_resource)) elsif sub.null_resource? nil else serialize_resource(sub) end end end end end end ================================================ FILE: yaks/lib/yaks/format/halo.rb ================================================ module Yaks class Format # Extension of Hal loosely based on the example by Mike Kelly given at # https://gist.github.com/mikekelly/893552 class Halo < Hal register :halo, :json, 'application/halo+json' def serialize_resource(resource) if resource.forms.any? super.merge(_controls: serialize_forms(resource.forms)) else super end end def serialize_forms(forms) forms.each_with_object({}) do |form, result| result[form.name] = serialize_form(form) end end def serialize_form(form) raw = form.to_h_compact raw[:href] = raw.delete(:action) if raw[:action] raw[:fields] = form.fields.map(&method(:serialize_form_field)) raw end def serialize_form_field(field) if field.type == :fieldset { type: :fieldset, fields: field.fields.map(&method(:serialize_form_field)) } else field.to_h_compact.each_with_object({}) do |(attr, value), hsh| if attr == :options #