From ab6af9d073760f7ca2a660c8b5d4b50a99686450 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Thu, 28 Jan 2016 22:50:06 -0600 Subject: [PATCH] Document JSON API implementation defs and progress in class --- CHANGELOG.md | 1 + lib/active_model_serializers/adapter.rb | 11 +- .../adapter/json_api.rb | 308 +++++++++++++++++- .../adapter/json_api/error.rb | 17 + .../adapter/json_api/jsonapi.rb | 18 + .../adapter/json_api/link.rb | 37 +++ .../adapter/json_api/meta.rb | 10 + 7 files changed, 395 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db8eecbc..cb0c66a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Fixes: - [#1488](https://github.com/rails-api/active_model_serializers/pull/1488) Require ActiveSupport's string inflections (@nate00) Misc: +- [#1482](https://github.com/rails-api/active_model_serializers/pull/1482) Document JSON API implementation defs and progress in class. (@bf4) - [#1527](https://github.com/rails-api/active_model_serializers/pull/1527) Refactor fragment cache class. (@groyoh) - [#1560](https://github.com/rails-api/active_model_serializers/pull/1560) Update rubocop and address its warnings. (@bf4 @groyoh) - [#1545](https://github.com/rails-api/active_model_serializers/pull/1545) Document how to pass arbitrary options to the diff --git a/lib/active_model_serializers/adapter.rb b/lib/active_model_serializers/adapter.rb index 35adbccdb..335de9db9 100644 --- a/lib/active_model_serializers/adapter.rb +++ b/lib/active_model_serializers/adapter.rb @@ -84,10 +84,11 @@ def find_by_name(adapter_name) end # Gotta be at the bottom to use the code above it :( - require 'active_model_serializers/adapter/base' - require 'active_model_serializers/adapter/null' - require 'active_model_serializers/adapter/attributes' - require 'active_model_serializers/adapter/json' - require 'active_model_serializers/adapter/json_api' + extend ActiveSupport::Autoload + autoload :Base + autoload :Null + autoload :Attributes + autoload :Json + autoload :JsonApi end end diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb index f06a69e92..e1daae08f 100644 --- a/lib/active_model_serializers/adapter/json_api.rb +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -1,3 +1,20 @@ +# {http://jsonapi.org/format/ JSON API specification} +# rubocop:disable Style/AsciiComments +# TODO: implement! +# ☐ https://github.com/rails-api/active_model_serializers/issues/1235 +# TODO: use uri_template in link generation? +# ☐ https://github.com/rails-api/active_model_serializers/pull/1282#discussion_r42528812 +# TODO: validate against a JSON schema document? +# ☐ https://github.com/rails-api/active_model_serializers/issues/1162 +# ☑ https://github.com/rails-api/active_model_serializers/pull/1270 +# TODO: Routing +# ☐ https://github.com/rails-api/active_model_serializers/pull/1476 +# TODO: Query Params +# ☑ `include` https://github.com/rails-api/active_model_serializers/pull/1131 +# ☑ `fields` https://github.com/rails-api/active_model_serializers/pull/700 +# ☑ `page[number]=3&page[size]=1` https://github.com/rails-api/active_model_serializers/pull/1041 +# ☐ `filter` +# ☐ `sort` module ActiveModelSerializers module Adapter class JsonApi < Base @@ -30,13 +47,69 @@ def serializable_hash(options = nil) end # {http://jsonapi.org/format/#document-top-level Primary data} + # definition: + # ☐ toplevel_data (required) + # ☐ toplevel_included + # ☑ toplevel_meta + # ☑ toplevel_links + # ☑ toplevel_jsonapi + # structure: + # { + # data: toplevel_data, + # included: toplevel_included, + # meta: toplevel_meta, + # links: toplevel_links, + # jsonapi: toplevel_jsonapi + # }.reject! {|_,v| v.nil? } def success_document(options) is_collection = serializer.respond_to?(:each) serializers = is_collection ? serializer : [serializer] primary_data, included = resource_objects_for(serializers) hash = {} + # toplevel_data + # definition: + # oneOf + # resource + # array of unique items of type 'resource' + # null + # + # description: + # The document's "primary data" is a representation of the resource or collection of resources + # targeted by a request. + # + # Singular: the resource object. + # + # Collection: one of an array of resource objects, an array of resource identifier objects, or + # an empty array ([]), for requests that target resource collections. + # + # None: null if the request is one that might correspond to a single resource, but doesn't currently. + # structure: + # if serializable_resource.resource? + # resource + # elsif serializable_resource.collection? + # [ + # resource, + # resource + # ] + # else + # nil + # end hash[:data] = is_collection ? primary_data : primary_data[0] + # toplevel_included + # alias included + # definition: + # array of unique items of type 'resource' + # + # description: + # To reduce the number of HTTP requests, servers **MAY** allow + # responses that include related resources along with the requested primary + # resources. Such responses are called "compound documents". + # structure: + # [ + # resource, + # resource + # ] hash[:included] = included if included.any? Jsonapi.add!(hash) @@ -56,17 +129,31 @@ def success_document(options) # {http://jsonapi.org/format/#errors JSON API Errors} # TODO: look into caching - # rubocop:disable Style/AsciiComments # definition: # ☑ toplevel_errors array (required) # ☐ toplevel_meta # ☐ toplevel_jsonapi - # rubocop:enable Style/AsciiComments + # structure: + # { + # errors: toplevel_errors, + # meta: toplevel_meta, + # jsonapi: toplevel_jsonapi + # }.reject! {|_,v| v.nil? } + # prs: + # https://github.com/rails-api/active_model_serializers/pull/1004 def failure_document hash = {} # PR Please :) # Jsonapi.add!(hash) + # toplevel_errors + # definition: + # array of unique items of type 'error' + # structure: + # [ + # error, + # error + # ] if serializer.respond_to?(:each) hash[:errors] = serializer.flat_map do |error_serializer| Error.resource_errors(error_serializer) @@ -89,6 +176,40 @@ def fragment_cache(cached_hash, non_cached_hash) private # {http://jsonapi.org/format/#document-resource-objects Primary data} + # resource + # definition: + # JSON Object + # + # properties: + # type (required) : String + # id (required) : String + # attributes + # relationships + # links + # meta + # + # description: + # "Resource objects" appear in a JSON API document to represent resources + # structure: + # { + # type: 'admin--some-user', + # id: '1336', + # attributes: attributes, + # relationships: relationships, + # links: links, + # meta: meta, + # }.reject! {|_,v| v.nil? } + # prs: + # type + # https://github.com/rails-api/active_model_serializers/pull/1122 + # [x] https://github.com/rails-api/active_model_serializers/pull/1213 + # https://github.com/rails-api/active_model_serializers/pull/1216 + # https://github.com/rails-api/active_model_serializers/pull/1029 + # links + # [x] https://github.com/rails-api/active_model_serializers/pull/1246 + # [x] url helpers https://github.com/rails-api/active_model_serializers/issues/1269 + # meta + # [x] https://github.com/rails-api/active_model_serializers/pull/1340 def resource_objects_for(serializers) @primary = [] @included = [] @@ -131,6 +252,21 @@ def process_relationship(serializer, include_tree) end # {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes} + # attributes + # definition: + # JSON Object + # + # patternProperties: + # ^(?!relationships$|links$)\\w[-\\w_]*$ + # + # description: + # Members of the attributes object ("attributes") represent information about the resource + # object in which it's defined. + # Attributes may contain any valid JSON value + # structure: + # { + # foo: 'bar' + # } def attributes_for(serializer, fields) serializer.attributes(fields).except(:id) end @@ -151,8 +287,29 @@ def resource_object_for(serializer) resource_object[:relationships] = relationships if relationships.any? links = links_for(serializer) + # toplevel_links + # definition: + # allOf + # ☐ links + # ☐ pagination + # + # description: + # Link members related to the primary data. + # structure: + # links.merge!(pagination) + # prs: + # https://github.com/rails-api/active_model_serializers/pull/1247 + # https://github.com/rails-api/active_model_serializers/pull/1018 resource_object[:links] = links if links.any? + # toplevel_meta + # alias meta + # definition: + # meta + # structure + # { + # :'git-ref' => 'abc123' + # } meta = meta_for(serializer) resource_object[:meta] = meta unless meta.nil? @@ -160,6 +317,100 @@ def resource_object_for(serializer) end # {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship} + # relationships + # definition: + # JSON Object + # + # patternProperties: + # ^\\w[-\\w_]*$" + # + # properties: + # data : relationshipsData + # links + # meta + # + # description: + # + # Members of the relationships object ("relationships") represent references from the + # resource object in which it's defined to other resource objects." + # structure: + # { + # links: links, + # meta: meta, + # data: relationshipsData + # }.reject! {|_,v| v.nil? } + # + # prs: + # links + # [x] https://github.com/rails-api/active_model_serializers/pull/1454 + # meta + # [x] https://github.com/rails-api/active_model_serializers/pull/1454 + # polymorphic + # [ ] https://github.com/rails-api/active_model_serializers/pull/1420 + # + # relationshipsData + # definition: + # oneOf + # relationshipToOne + # relationshipToMany + # + # description: + # Member, whose value represents "resource linkage" + # structure: + # if has_one? + # relationshipToOne + # else + # relationshipToMany + # end + # + # definition: + # anyOf + # null + # linkage + # + # relationshipToOne + # description: + # + # References to other resource objects in a to-one ("relationship"). Relationships can be + # specified by including a member in a resource's links object. + # + # None: Describes an empty to-one relationship. + # structure: + # if has_related? + # linkage + # else + # nil + # end + # + # relationshipToMany + # definition: + # array of unique items of type 'linkage' + # + # description: + # An array of objects each containing "type" and "id" members for to-many relationships + # structure: + # [ + # linkage, + # linkage + # ] + # prs: + # polymorphic + # [ ] https://github.com/rails-api/active_model_serializers/pull/1282 + # + # linkage + # definition: + # type (required) : String + # id (required) : String + # meta + # + # description: + # The "type" and "id" to non-empty members. + # structure: + # { + # type: 'required-type', + # id: 'required-id', + # meta: meta + # }.reject! {|_,v| v.nil? } def relationships_for(serializer, requested_associations) include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(requested_associations) serializer.associations(include_tree).each_with_object({}) do |association, hash| @@ -174,6 +425,28 @@ def relationships_for(serializer, requested_associations) end # {http://jsonapi.org/format/#document-links Document Links} + # links + # definition: + # JSON Object + # + # properties: + # self : URI + # related : link + # + # description: + # A resource object **MAY** contain references to other resource objects ("relationships"). + # Relationships may be to-one or to-many. Relationships can be specified by including a member + # in a resource's links object. + # + # A `self` member’s value is a URL for the relationship itself (a "relationship URL"). This + # URL allows the client to directly manipulate the relationship. For example, it would allow + # a client to remove an `author` from an `article` without deleting the people resource + # itself. + # structure: + # { + # self: 'http://example.com/etc', + # related: link + # }.reject! {|_,v| v.nil? } def links_for(serializer) serializer._links.each_with_object({}) do |(name, value), hash| hash[name] = Link.new(serializer, value).as_json @@ -181,6 +454,36 @@ def links_for(serializer) end # {http://jsonapi.org/format/#fetching-pagination Pagination Links} + # pagination + # definition: + # first : pageObject + # last : pageObject + # prev : pageObject + # next : pageObject + # structure: + # { + # first: pageObject, + # last: pageObject, + # prev: pageObject, + # next: pageObject + # } + # + # pageObject + # definition: + # oneOf + # URI + # null + # + # description: + # The page of data + # structure: + # if has_page? + # 'http://example.com/some-page?page[number][x]' + # else + # nil + # end + # prs: + # https://github.com/rails-api/active_model_serializers/pull/1041 def pagination_links_for(serializer, options) PaginationLinks.new(serializer.object, options[:serialization_context]).serializable_hash(options) end @@ -192,3 +495,4 @@ def meta_for(serializer) end end end +# rubocop:enable Style/AsciiComments diff --git a/lib/active_model_serializers/adapter/json_api/error.rb b/lib/active_model_serializers/adapter/json_api/error.rb index c8383d216..67166d41e 100644 --- a/lib/active_model_serializers/adapter/json_api/error.rb +++ b/lib/active_model_serializers/adapter/json_api/error.rb @@ -36,6 +36,12 @@ def self.resource_errors(error_serializer) # title : A short, human-readable summary of the problem. It **SHOULD NOT** change from # occurrence to occurrence of the problem, except for purposes of localization. # detail : A human-readable explanation specific to this occurrence of the problem. + # structure: + # { + # title: 'SystemFailure', + # detail: 'something went terribly wrong', + # status: '500' + # }.merge!(errorSource) def self.attribute_error_objects(attribute_name, attribute_errors) attribute_errors.map do |attribute_error| { @@ -45,6 +51,7 @@ def self.attribute_error_objects(attribute_name, attribute_errors) end end + # errorSource # description: # oneOf # ☑ pointer : String @@ -56,6 +63,16 @@ def self.attribute_error_objects(attribute_name, attribute_errors) # https://tools.ietf.org/html/rfc6901 # # parameter: A string indicating which query parameter caused the error + # structure: + # if is_attribute? + # { + # pointer: '/data/attributes/red-button' + # } + # else + # { + # parameter: 'pres' + # } + # end def self.error_source(source_type, attribute_name) case source_type when :pointer diff --git a/lib/active_model_serializers/adapter/json_api/jsonapi.rb b/lib/active_model_serializers/adapter/json_api/jsonapi.rb index 8df1ae966..e94578af6 100644 --- a/lib/active_model_serializers/adapter/json_api/jsonapi.rb +++ b/lib/active_model_serializers/adapter/json_api/jsonapi.rb @@ -2,6 +2,24 @@ module ActiveModelSerializers module Adapter class JsonApi < Base # {http://jsonapi.org/format/#document-jsonapi-object Jsonapi Object} + + # toplevel_jsonapi + # definition: + # JSON Object + # + # properties: + # version : String + # meta + # + # description: + # An object describing the server's implementation + # structure: + # { + # version: ActiveModelSerializers.config.jsonapi_version, + # meta: ActiveModelSerializers.config.jsonapi_toplevel_meta + # }.reject! { |_, v| v.blank? } + # prs: + # https://github.com/rails-api/active_model_serializers/pull/1050 module Jsonapi module_function diff --git a/lib/active_model_serializers/adapter/json_api/link.rb b/lib/active_model_serializers/adapter/json_api/link.rb index 255f875a5..e6f5c76b0 100644 --- a/lib/active_model_serializers/adapter/json_api/link.rb +++ b/lib/active_model_serializers/adapter/json_api/link.rb @@ -3,6 +3,43 @@ module ActiveModelSerializers module Adapter class JsonApi + # link + # definition: + # oneOf + # linkString + # linkObject + # + # description: + # A link **MUST** be represented as either: a string containing the link's URL or a link + # object." + # structure: + # if href? + # linkString + # else + # linkObject + # end + # + # linkString + # definition: + # URI + # + # description: + # A string containing the link's URL. + # structure: + # 'http://example.com/link-string' + # + # linkObject + # definition: + # JSON Object + # + # properties: + # href (required) : URI + # meta + # structure: + # { + # href: 'http://example.com/link-object', + # meta: meta, + # }.reject! {|_,v| v.nil? } class Link include SerializationContext.url_helpers delegate :default_url_options, to: SerializationContext diff --git a/lib/active_model_serializers/adapter/json_api/meta.rb b/lib/active_model_serializers/adapter/json_api/meta.rb index 4d818e72a..d889b3eb8 100644 --- a/lib/active_model_serializers/adapter/json_api/meta.rb +++ b/lib/active_model_serializers/adapter/json_api/meta.rb @@ -1,6 +1,16 @@ module ActiveModelSerializers module Adapter class JsonApi + # meta + # definition: + # JSON Object + # + # description: + # Non-standard meta-information that can not be represented as an attribute or relationship. + # structure: + # { + # attitude: 'adjustable' + # } class Meta def initialize(serializer) @object = serializer.object