simple_jsonapi 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +131 -0
  4. data/CHANGELOG.md +2 -0
  5. data/Gemfile +5 -0
  6. data/Jenkinsfile +92 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +532 -0
  9. data/Rakefile +10 -0
  10. data/lib/simple_jsonapi.rb +112 -0
  11. data/lib/simple_jsonapi/definition/attribute.rb +45 -0
  12. data/lib/simple_jsonapi/definition/base.rb +50 -0
  13. data/lib/simple_jsonapi/definition/concerns/has_links_object.rb +36 -0
  14. data/lib/simple_jsonapi/definition/concerns/has_meta_object.rb +36 -0
  15. data/lib/simple_jsonapi/definition/error.rb +70 -0
  16. data/lib/simple_jsonapi/definition/error_source.rb +29 -0
  17. data/lib/simple_jsonapi/definition/link.rb +27 -0
  18. data/lib/simple_jsonapi/definition/meta.rb +27 -0
  19. data/lib/simple_jsonapi/definition/relationship.rb +60 -0
  20. data/lib/simple_jsonapi/definition/resource.rb +104 -0
  21. data/lib/simple_jsonapi/error_serializer.rb +76 -0
  22. data/lib/simple_jsonapi/errors/bad_request.rb +11 -0
  23. data/lib/simple_jsonapi/errors/exception_serializer.rb +6 -0
  24. data/lib/simple_jsonapi/errors/wrapped_error.rb +35 -0
  25. data/lib/simple_jsonapi/errors/wrapped_error_serializer.rb +35 -0
  26. data/lib/simple_jsonapi/helpers/exceptions.rb +39 -0
  27. data/lib/simple_jsonapi/helpers/serializer_inferrer.rb +136 -0
  28. data/lib/simple_jsonapi/helpers/serializer_methods.rb +36 -0
  29. data/lib/simple_jsonapi/node/attributes.rb +51 -0
  30. data/lib/simple_jsonapi/node/base.rb +91 -0
  31. data/lib/simple_jsonapi/node/data/collection.rb +25 -0
  32. data/lib/simple_jsonapi/node/data/singular.rb +26 -0
  33. data/lib/simple_jsonapi/node/document/base.rb +62 -0
  34. data/lib/simple_jsonapi/node/document/collection.rb +17 -0
  35. data/lib/simple_jsonapi/node/document/errors.rb +17 -0
  36. data/lib/simple_jsonapi/node/document/singular.rb +17 -0
  37. data/lib/simple_jsonapi/node/error.rb +55 -0
  38. data/lib/simple_jsonapi/node/error_source.rb +40 -0
  39. data/lib/simple_jsonapi/node/errors.rb +28 -0
  40. data/lib/simple_jsonapi/node/included.rb +45 -0
  41. data/lib/simple_jsonapi/node/object_links.rb +40 -0
  42. data/lib/simple_jsonapi/node/object_meta.rb +40 -0
  43. data/lib/simple_jsonapi/node/relationship.rb +79 -0
  44. data/lib/simple_jsonapi/node/relationship_data/base.rb +53 -0
  45. data/lib/simple_jsonapi/node/relationship_data/collection.rb +32 -0
  46. data/lib/simple_jsonapi/node/relationship_data/singular.rb +33 -0
  47. data/lib/simple_jsonapi/node/relationships.rb +60 -0
  48. data/lib/simple_jsonapi/node/resource/base.rb +21 -0
  49. data/lib/simple_jsonapi/node/resource/full.rb +49 -0
  50. data/lib/simple_jsonapi/node/resource/linkage.rb +25 -0
  51. data/lib/simple_jsonapi/parameters/fields_spec.rb +45 -0
  52. data/lib/simple_jsonapi/parameters/include_spec.rb +57 -0
  53. data/lib/simple_jsonapi/parameters/sort_spec.rb +107 -0
  54. data/lib/simple_jsonapi/serializer.rb +89 -0
  55. data/lib/simple_jsonapi/version.rb +3 -0
  56. data/simple_jsonapi.gemspec +29 -0
  57. data/test/errors/bad_request_test.rb +34 -0
  58. data/test/errors/error_serializer_test.rb +229 -0
  59. data/test/errors/exception_serializer_test.rb +25 -0
  60. data/test/errors/wrapped_error_serializer_test.rb +91 -0
  61. data/test/errors/wrapped_error_test.rb +44 -0
  62. data/test/parameters/fields_spec_test.rb +56 -0
  63. data/test/parameters/include_spec_test.rb +58 -0
  64. data/test/parameters/sort_spec_test.rb +65 -0
  65. data/test/resources/attributes_test.rb +109 -0
  66. data/test/resources/extras_test.rb +70 -0
  67. data/test/resources/id_and_type_test.rb +76 -0
  68. data/test/resources/inclusion_test.rb +134 -0
  69. data/test/resources/links_test.rb +63 -0
  70. data/test/resources/meta_test.rb +49 -0
  71. data/test/resources/relationships_test.rb +262 -0
  72. data/test/resources/sorting_test.rb +79 -0
  73. data/test/resources/sparse_fieldset_test.rb +160 -0
  74. data/test/root_objects_test.rb +165 -0
  75. data/test/test_helper.rb +31 -0
  76. metadata +235 -0
@@ -0,0 +1,28 @@
1
+ module SimpleJsonapi::Node
2
+ # @!attribute [r] errors
3
+ # @return [Array<Object>]
4
+ class Errors < Base
5
+ attr_reader :errors
6
+
7
+ # @param errors [Array<Object>]
8
+ # @param options see {Node::Base#initialize} for additional parameters
9
+ def initialize(errors:, **options)
10
+ super(options)
11
+
12
+ @errors = Array.wrap(errors)
13
+
14
+ @error_nodes = @errors.map do |error|
15
+ build_child_node(SimpleJsonapi::Node::Error, error: error)
16
+ end
17
+ end
18
+
19
+ # @return [Hash{Symbol => Hash}]
20
+ def as_jsonapi
21
+ if @error_nodes.any?
22
+ { errors: @error_nodes.map(&:as_jsonapi) }
23
+ else
24
+ {}
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,45 @@
1
+ module SimpleJsonapi::Node
2
+ class Included < Base
3
+ # @param options see {Node::Base#initialize} for additional parameters
4
+ def initialize(**options)
5
+ super
6
+
7
+ @resource_nodes = {}
8
+ end
9
+
10
+ # @param resource_node [Node::Resource::Base]
11
+ def included_resource?(resource_node)
12
+ @resource_nodes.key?(hash_key(resource_node))
13
+ end
14
+
15
+ # @param resource_node [Node::Resource::Full]
16
+ # @return [Node::Resource::Full]
17
+ def append_included_resource(resource_node)
18
+ if included_resource?(resource_node)
19
+ raise DuplicateResourceError.new(resource_node.resource_type, resource_node.resource_id)
20
+ end
21
+
22
+ @resource_nodes[hash_key(resource_node)] = resource_node
23
+ end
24
+
25
+ # @return [Hash{Symbol => Hash}]
26
+ def as_jsonapi
27
+ if @resource_nodes.any?
28
+ resources_json = @resource_nodes
29
+ .each_value
30
+ .sort_by { |node| [node.resource_type, node.resource_id] }
31
+ .map(&:as_jsonapi)
32
+
33
+ { included: resources_json }
34
+ else
35
+ {}
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def hash_key(resource_node)
42
+ [resource_node.resource_type, resource_node.resource_id]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,40 @@
1
+ module SimpleJsonapi::Node
2
+ # Represents a +links+ object (a collection of links).
3
+ #
4
+ # @!attribute [r] object
5
+ # @return [Object]
6
+ # @!attribute [r] link_definitions
7
+ # @return [Hash{Symbol => Definition::Link}]
8
+ class ObjectLinks < Base
9
+ attr_reader :object, :link_definitions
10
+
11
+ # @param object [Object]
12
+ # @param link_definitions [Hash{Symbol => Definition::Link}]
13
+ # @param options see {Node::Base#initialize} for additional parameters
14
+ def initialize(object:, link_definitions:, **options)
15
+ super(options)
16
+
17
+ @object = object
18
+ @link_definitions = link_definitions
19
+ end
20
+
21
+ # @return [Hash{Symbol => Hash}]
22
+ def as_jsonapi
23
+ if link_definitions_to_render.any?
24
+ json = {}
25
+ link_definitions_to_render.each do |name, defn|
26
+ json[name] = evaluate(defn.value_proc, object)
27
+ end
28
+ { links: json }
29
+ else
30
+ {}
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def link_definitions_to_render
37
+ @link_definitions_to_render ||= link_definitions.select { |_, defn| render?(defn, object) }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ module SimpleJsonapi::Node
2
+ # Represents a +meta+ object (a collection of meta members).
3
+ #
4
+ # @!attribute [r] object
5
+ # @return [Object]
6
+ # @!attribute [r] meta_definitions
7
+ # @return [Hash{Symbol => Definition::Meta}]
8
+ class ObjectMeta < Base
9
+ attr_reader :object, :meta_definitions
10
+
11
+ # @param object [Object]
12
+ # @param meta_definitions [Hash{Symbol => Definition::Meta}]
13
+ # @param options see {Node::Base#initialize} for additional parameters
14
+ def initialize(object:, meta_definitions:, **options)
15
+ super(options)
16
+
17
+ @object = object
18
+ @meta_definitions = meta_definitions || {}
19
+ end
20
+
21
+ # @return [Hash{Symbol => Hash}]
22
+ def as_jsonapi
23
+ if meta_definitions_to_render.any?
24
+ json = {}
25
+ meta_definitions_to_render.each do |name, defn|
26
+ json[name] = evaluate(defn.value_proc, object)
27
+ end
28
+ { meta: json }
29
+ else
30
+ {}
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def meta_definitions_to_render
37
+ @meta_definitions_to_render ||= @meta_definitions.select { |_, defn| render?(defn, object) }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,79 @@
1
+ module SimpleJsonapi::Node
2
+ # Represents a single +relationship+ object.
3
+ #
4
+ # @!attribute [r] resource
5
+ # @return [Object]
6
+ # @!attribute [r] relationship_definition
7
+ # @return [Definition::Relationship]
8
+ class Relationship < Base
9
+ attr_reader :resource, :relationship_definition
10
+
11
+ # @param resource [Object]
12
+ # @param relationship_definition [Definition::Relationship]
13
+ # @param options see {Node::Base#initialize} for additional parameters
14
+ def initialize(resource:, relationship_definition:, **options)
15
+ super(options)
16
+
17
+ @resource = resource
18
+ @relationship_definition = relationship_definition
19
+
20
+ if relationship_definition.singular?
21
+ @data_node = build_child_node(
22
+ SimpleJsonapi::Node::RelationshipData::Singular,
23
+ relationship_definition: relationship_definition,
24
+ resource: related_data,
25
+ add_to_included: add_to_included?,
26
+ )
27
+ elsif relationship_definition.collection?
28
+ @data_node = build_child_node(
29
+ SimpleJsonapi::Node::RelationshipData::Collection,
30
+ relationship_definition: relationship_definition,
31
+ resources: related_data,
32
+ add_to_included: add_to_included?,
33
+ )
34
+ end
35
+
36
+ @links_node = build_child_node(
37
+ SimpleJsonapi::Node::ObjectLinks,
38
+ object: resource,
39
+ link_definitions: relationship_definition.link_definitions,
40
+ )
41
+
42
+ @meta_node = build_child_node(
43
+ SimpleJsonapi::Node::ObjectMeta,
44
+ object: resource,
45
+ meta_definitions: relationship_definition.meta_definitions,
46
+ )
47
+ end
48
+
49
+ # @return [String]
50
+ def relationship_name
51
+ relationship_definition.name
52
+ end
53
+
54
+ # @return [Hash{Symbol => Hash}]
55
+ def as_jsonapi
56
+ rel_json = {}
57
+
58
+ rel_json.merge!(@data_node.as_jsonapi) if @data_node
59
+ rel_json.merge!(@links_node.as_jsonapi) if @links_node
60
+ rel_json.merge!(@meta_node.as_jsonapi) if @meta_node
61
+
62
+ rel_json
63
+ end
64
+
65
+ private
66
+
67
+ def related_data
68
+ @related_data ||= evaluate(
69
+ relationship_definition.data_definition,
70
+ resource,
71
+ sort: sort_spec[relationship_name.to_sym],
72
+ )
73
+ end
74
+
75
+ def add_to_included?
76
+ include_spec.include?(relationship_name)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,53 @@
1
+ module SimpleJsonapi::Node::RelationshipData
2
+ # Represents a relationship's +data+ object.
3
+ #
4
+ # @abstract
5
+ # @!attribute [r] relationship_definition
6
+ # @return [Definition::Relationship]
7
+ # @!attribute [r] add_to_included
8
+ # @return [Boolean]
9
+ class Base < SimpleJsonapi::Node::Base
10
+ attr_reader :relationship_definition, :add_to_included
11
+
12
+ # @param relationship_definition [Definition::Relationship]
13
+ # @param add_to_included [Boolean]
14
+ # @param options see {Node::Base#initialize} for additional parameters
15
+ def initialize(relationship_definition:, add_to_included:, **options)
16
+ super(options)
17
+
18
+ @relationship_definition = relationship_definition
19
+ @add_to_included = add_to_included
20
+
21
+ @serializer_inferrer = relationship_definition.serializer_inferrer
22
+ @include_spec = include_spec[relationship_name]
23
+ end
24
+
25
+ private
26
+
27
+ def build_linkage_node(related_resource)
28
+ build_child_node(
29
+ SimpleJsonapi::Node::Resource::Linkage,
30
+ resource: related_resource,
31
+ meta: { included: add_to_included },
32
+ )
33
+ end
34
+
35
+ def add_resource_to_included(related_resource, linkage_node)
36
+ if add_to_included && !root_node.included_resource?(linkage_node)
37
+ full_node = build_child_node(
38
+ SimpleJsonapi::Node::Resource::Full,
39
+ # Only support sorting on first-level relationships because
40
+ # 1. supporting more levels requires more tracking of how the document is being built
41
+ # 2. letting the API client sort a deeper relationship seems unnecessary
42
+ sort_related: nil,
43
+ resource: related_resource,
44
+ )
45
+ root_node.append_included_resource(full_node)
46
+ end
47
+ end
48
+
49
+ def relationship_name
50
+ relationship_definition.name
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ module SimpleJsonapi::Node::RelationshipData
2
+ # Represents a relationship's +data+ object containing a collection of
3
+ # resource linkage objects.
4
+ #
5
+ # @!attribute [r] resources
6
+ # @return [Array<Object>]
7
+ class Collection < Base
8
+ attr_reader :resources
9
+
10
+ # @param resources [Array<Object>]
11
+ # @param options see {Node::RelationshipData::Base#initialize} for additional parameters
12
+ def initialize(resources:, **options)
13
+ super
14
+
15
+ @resources = Array.wrap(resources)
16
+ @linkage_nodes = []
17
+
18
+ @resources.each do |resource|
19
+ linkage_node = build_linkage_node(resource)
20
+
21
+ @linkage_nodes << linkage_node
22
+
23
+ add_resource_to_included(resource, linkage_node)
24
+ end
25
+ end
26
+
27
+ # @return [Hash{Symbol => Hash}]
28
+ def as_jsonapi
29
+ { data: @linkage_nodes.map(&:as_jsonapi) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ module SimpleJsonapi::Node::RelationshipData
2
+ # Represents a relationship's +data+ object containing a single resource
3
+ # linkage object.
4
+ #
5
+ # @!attribute [r] resource
6
+ # @return [Object]
7
+ class Singular < Base
8
+ attr_reader :resource
9
+
10
+ # @param resource [Object]
11
+ # @param options see {Node::RelationshipData::Base#initialize} for additional parameters
12
+ def initialize(resource:, **options)
13
+ super
14
+
15
+ @resource = resource
16
+
17
+ unless @resource.nil?
18
+ @linkage_node = build_linkage_node(@resource)
19
+
20
+ add_resource_to_included(@resource, @linkage_node)
21
+ end
22
+ end
23
+
24
+ # @return [Hash{Symbol => Hash}]
25
+ def as_jsonapi
26
+ if resource.nil?
27
+ { data: nil }
28
+ else
29
+ { data: @linkage_node.as_jsonapi }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ module SimpleJsonapi::Node
2
+ # Represents a resource's +relationships+ object, which contains the
3
+ # individual +relationship+ object.
4
+ #
5
+ # @!attribute [r] resource
6
+ # @return [Object]
7
+ # @!attribute [r] resource_type
8
+ # @return [String]
9
+ # @!attribute [r] relationship_definitions
10
+ # @return [Hash{Symbol => Definition::Relationship}]
11
+ class Relationships < Base
12
+ attr_reader :resource, :resource_type, :relationship_definitions
13
+
14
+ # @param resource [Object]
15
+ # @param resource_type [String]
16
+ # @param relationship_definitions [Hash{Symbol => Definition::Relationship}]
17
+ # @param options see {Node::Base#initialize} for additional parameters
18
+ def initialize(resource:, resource_type:, relationship_definitions:, **options)
19
+ super(options)
20
+
21
+ @resource = resource
22
+ @resource_type = resource_type
23
+ @relationship_definitions = relationship_definitions
24
+
25
+ @relationship_nodes = relationship_definitions_to_render.map do |_name, defn|
26
+ build_child_node(
27
+ SimpleJsonapi::Node::Relationship,
28
+ resource: resource,
29
+ relationship_definition: defn,
30
+ )
31
+ end
32
+ end
33
+
34
+ # @return [Hash{Symbol => Hash}]
35
+ def as_jsonapi
36
+ if @relationship_nodes.any?
37
+ json = {}
38
+ @relationship_nodes.each do |rel_node|
39
+ json[rel_node.relationship_name] = rel_node.as_jsonapi
40
+ end
41
+ { relationships: json }
42
+ else
43
+ {}
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def relationship_definitions_to_render
50
+ @relationship_definitions_to_render ||= begin
51
+ include_all_fields = fields_spec.all_fields?(resource_type)
52
+ explicit_fields = fields_spec[resource_type]
53
+
54
+ relationship_definitions
55
+ .select { |name, _| include_all_fields || explicit_fields.include?(name) }
56
+ .select { |_, defn| render?(defn, resource) }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,21 @@
1
+ module SimpleJsonapi::Node::Resource
2
+ # Represents a single resource object or resource linkage object.
3
+ class Base < SimpleJsonapi::Node::Base
4
+ attr_reader :resource
5
+
6
+ def initialize(resource:, **options)
7
+ super(options)
8
+
9
+ @resource = resource
10
+ @serializer = serializer_inferrer.infer(resource).new
11
+ end
12
+
13
+ def resource_id
14
+ @resource_id ||= evaluate(serializer.id_definition, resource).to_s
15
+ end
16
+
17
+ def resource_type
18
+ @resource_type ||= evaluate(serializer.type_definition, resource).to_s
19
+ end
20
+ end
21
+ end