From daa6596349bfa32f925f9f594e0542e78e655807 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Thu, 28 Jan 2016 22:50:06 -0600 Subject: [PATCH] Proposed JSON API object skeleton --- lib/active_model_serializers.rb | 1 + lib/active_model_serializers/adapter.rb | 6 + .../adapter/json_api.rb | 416 ++++++++++++++++++ 3 files changed, 423 insertions(+) create mode 100644 lib/active_model_serializers/adapter.rb create mode 100644 lib/active_model_serializers/adapter/json_api.rb diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb index d92823b5a..c03201299 100644 --- a/lib/active_model_serializers.rb +++ b/lib/active_model_serializers.rb @@ -4,6 +4,7 @@ module ActiveModelSerializers extend ActiveSupport::Autoload autoload :Model + autoload :Adapter autoload :Callbacks autoload :Deserialization autoload :Logging diff --git a/lib/active_model_serializers/adapter.rb b/lib/active_model_serializers/adapter.rb new file mode 100644 index 000000000..8c10a8d1d --- /dev/null +++ b/lib/active_model_serializers/adapter.rb @@ -0,0 +1,6 @@ +module ActiveModelSerializers + module Adapter + extend ActiveSupport::Autoload + autoload :JsonApi + end +end diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb new file mode 100644 index 000000000..f0e3b6d39 --- /dev/null +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -0,0 +1,416 @@ +# TODO: implement! +# TODO: use uri_template in link generation? +# TODO: validate against a JSON schema document? +module ActiveModelSerializers + module Adapter + class JsonApi + + def initialize(serializable_resource) + @serializable_resource = serializable_resource + end + + def as_json + if serializable_resource.success? + success + else + errors + end + end + + # definition: + # ☐ toplevel_data (required) + # ☐ toplevel_included + # ☑ toplevel_meta + # ☑ toplevel_links + # ☑ toplevel_jsonapi + def success + { + data: toplevel_data, + included: toplevel_included, + meta: toplevel_meta, + links: toplevel_links, + jsonapi: toplevel_jsonapi + }.reject! {|_,v| v.nil? } + end + + # definition: + # ☐ toplevel_errors (required) + # ☑ toplevel_meta + # ☑ toplevel_jsonapi + def failure + { + errors: toplevel_errors, + meta: toplevel_meta, + jsonapi: toplevel_jsonapi + }.reject! {|_,v| v.nil? } + end + + # definition: + # JSON Object + # + # properties: + # version : String + # meta + # + # description: + # An object describing the server's implementation + def toplevel_jsonapi + { + version: ActiveModelSerializers.config.jsonapi_version, + meta: ActiveModelSerializers.config.jsonapi_toplevel_meta + }.reject! { |_, v| v.blank? } + end + alias jsonapi toplevel_jsonapi + + # 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. + def toplevel_data + if serializable_resource.resource? + resource + elsif serializable_resource.collection? + [ + resource, + resource + ] + else + nil + end + end + alias data toplevel_data + + # definition: + # array of unique items of type 'error' + def toplevel_errors + [ + error, + error + ] + end + alias errors toplevel_errors + + # 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". + def toplevel_included + [ + resource, + resource + ] + end + alias included toplevel_included + + # definition: + # meta + def toplevel_meta + { + :'git-ref' => 'abc123' + } + end + + # definition: + # allOf + # ☐ links + # ☐ pagination + # + # description: + # Link members related to the primary data. + def toplevel_links + links.merge!(pagination) + end + + # definition: + # JSON Object + # + # description: + # Non-standard meta-information that can not be represented as an attribute or relationship. + def meta + { + attitude: 'adjustable' + } + end + + # 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 + def resource + { + type: 'admin--some-user', + id: '1336', + attributes: attributes, + relationships: relationships, + links: links, + meta: meta, + }.reject! {|_,v| v.nil? } + end + + # 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. + def links + { + self: 'http://example.com/etc', + related: link + }.reject! {|_,v| v.nil? } + end + + # definition: + # oneOf + # linkString + # linkObject + # + # description: + # A link **MUST** be represented as either: a string containing the link's URL or a link + # object." + def link + if href? + linkString + else + linkObject + end + end + + # definition: + # URI + # + # description: + # A string containing the link's URL. + def linkString + 'http://example.com/link-string' + end + + # definition: + # JSON Object + # + # properties: + # href (required) : URI + # meta + def linkObject + { + href: 'http://example.com/link-object', + meta: meta, + }.reject! {|_,v| v.nil? } + end + + # 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 + def attributes + { + foo: 'bar' + } + end + + # 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." + def relationships + { + links: links, + meta: meta, + data: relationshipsData + }.reject! {|_,v| v.nil? } + end + + # definition: + # oneOf + # relationshipToOne + # relationshipToMany + # + # description: + # Member, whose value represents "resource linkage" + def relationshipsData + if has_one? + relationshipToOne + else + relationshipToMany + end + end + + # definition: + # anyOf + # null + # linkage + # + # 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. + def relationshipToOne + if has_related? + linkage + else + nil + end + end + + # definition: + # array of unique items of type 'linkage' + # + # description: + # An array of objects each containing "type" and "id" members for to-many relationships + def relationshipToMany + [ + linkage, + linkage + ] + end + + # definition: + # type (required) : String + # id (required) : String + # meta + # + # description: + # The "type" and "id" to non-empty members. + def linkage + { + type: 'required-type', + id: 'required-id', + meta: meta + }.reject! {|_,v| v.nil? } + end + + # definition: + # first : pageObject + # last : pageObject + # prev : pageObject + # next : pageObject + def pagination + { + first: pageObject, + last: pageObject, + prev: pageObject, + next: pageObject + } + end + + # definition: + # oneOf + # URI + # null + # + # description: + # The page of data + def pageObject + if has_page? + 'http://example.com/some-page?page[number][x]' + else + nil + end + end + + # definition: + # JSON Object + # + # properties: + # id : String + # status : String + # code : String + # title : String + # detail : String + # links + # meta + # errorSource + # + # description: + # id : A unique identifier for this particular occurrence of the problem. + # status : The HTTP status code applicable to this problem, expressed as a string value + # code : An application-specific error code, expressed as a string value. + # 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. + def error + { + title: 'SystemFailure', + detail: 'something went terribly wrong', + status: '500' + }.merge!(errorSource) + end + + # description: + # oneOf + # pointer : String + # parameter : String + # + # description: + # pointer: A JSON Pointer RFC6901 to the associated entity in the request document e.g. "/data" + # for a primary data object, or "/data/attributes/title" for a specific attribute. + # https://tools.ietf.org/html/rfc6901 + # + # parameter: A string indicating which query parameter caused the error + def errorSource + if is_attribute? + { + pointer: '/data/attributes/red-button' + } + else + { + parameter: 'pres' + } + end + end + end + end +end