simple_jsonapi 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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