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,36 @@
1
+ module SimpleJsonapi::SerializerMethods
2
+ # @!visibility private
3
+ def self.included(base)
4
+ class << base
5
+ # @return [Definition::Resource,Definition::Error]
6
+ attr_accessor :definition
7
+
8
+ def inherited(subclass)
9
+ subclass.definition = definition&.dup
10
+ end
11
+ end
12
+ end
13
+
14
+ # @return [Definition::Resource,Definition::Error]
15
+ def definition
16
+ self.class.definition
17
+ end
18
+
19
+ # Adds the provided data values to the serializer as instance variables for the duration of the block.
20
+ # @param data [Hash{Symbol => Object}]
21
+ def with(**data)
22
+ ivar_data = data.transform_keys { |key| :"@#{key}" }
23
+
24
+ existing_keys = ivar_data.each_key.select { |key| instance_variable_defined?(key) }
25
+ if existing_keys.any?
26
+ raise ArgumentError, "Cannot override existing instance variables #{existing_keys.to_sentence}."
27
+ end
28
+
29
+ begin
30
+ ivar_data.each { |k, v| instance_variable_set(k, v) }
31
+ yield
32
+ ensure
33
+ ivar_data.each_key { |k| remove_instance_variable(k) }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ module SimpleJsonapi::Node
2
+ # Represents a resource's +attributes+ object.
3
+ #
4
+ # @!attribute [r] resource
5
+ # @return [Object]
6
+ # @!attribute [r] resource_type
7
+ # @return [String]
8
+ # @!attribute [r] attribute_definitions
9
+ # @return [Hash{Symbol => Definition::Attribute}]
10
+ class Attributes < Base
11
+ attr_reader :resource, :resource_type, :attribute_definitions
12
+
13
+ # @param resource [Object]
14
+ # @param resource_type [String]
15
+ # @param attribute_definitions [Hash{Symbol => Definition::Attribute}]
16
+ # @param options see {Node::Base#initialize} for additional parameters
17
+ def initialize(resource:, resource_type:, attribute_definitions:, **options)
18
+ super(options)
19
+
20
+ @resource = resource
21
+ @resource_type = resource_type
22
+ @attribute_definitions = attribute_definitions
23
+ end
24
+
25
+ # @return [Hash{Symbol => Hash}]
26
+ def as_jsonapi
27
+ if attribute_definitions_to_render.any?
28
+ json = {}
29
+ attribute_definitions_to_render.each do |name, defn|
30
+ json[name] = evaluate(defn.value_proc, resource)
31
+ end
32
+ { attributes: json }
33
+ else
34
+ {}
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def attribute_definitions_to_render
41
+ @attribute_definitions_to_render ||= begin
42
+ include_all_fields = fields_spec.all_fields?(resource_type)
43
+ explicit_fields = fields_spec[resource_type]
44
+
45
+ attribute_definitions
46
+ .select { |name, _| include_all_fields || explicit_fields.include?(name) }
47
+ .select { |_, defn| render?(defn, resource) }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,91 @@
1
+ module SimpleJsonapi::Node
2
+ # Represents a node in the JSONAPI document. See {file:README.md} for more
3
+ # details.
4
+ #
5
+ # {include:file:doc/node_hierarchy.md}
6
+ # @abstract
7
+ # @!attribute [r] root_node
8
+ # @return [Node::Base]
9
+ # @!attribute [r] serializer_inferrer
10
+ # @return [SerializerInferrer]
11
+ # @!attribute [r] serializer
12
+ # @return [Serializer,ErrorSerializer]
13
+ # @!attribute [r] fields_spec
14
+ # @return [Parameters::FieldsSpec]
15
+ # @!attribute [r] include_spec
16
+ # @return [Parameters::IncludeSpec]
17
+ # @!attribute [r] sort_spec
18
+ # @return [Parameters::SortSpec]
19
+ # @!attribute [r] extras
20
+ # @return [Hash{Symbol => Object}]
21
+ class Base
22
+ attr_reader :root_node, :serializer_inferrer, :serializer, :fields_spec, :include_spec, :sort_spec, :extras
23
+
24
+ # @param root_node [Node::Base]
25
+ # @param serializer_inferrer [SerializerInferrer]
26
+ # @param serializer [Serializer,ErrorSerializer]
27
+ # @param fields [Parameters::FieldsSpec]
28
+ # @param include [Parameters::IncludeSpec]
29
+ # @param sort_related [Parameters::SortSpec]
30
+ # @param extras [Hash{Symbol => Object}]
31
+ def initialize(
32
+ root_node:,
33
+ serializer_inferrer: nil,
34
+ serializer: nil,
35
+ fields: nil,
36
+ include: nil,
37
+ sort_related: nil,
38
+ extras: {},
39
+ **_options
40
+ )
41
+ @root_node = root_node
42
+ @serializer_inferrer = SimpleJsonapi::SerializerInferrer.wrap(serializer_inferrer)
43
+ @serializer = serializer
44
+ @fields_spec = SimpleJsonapi::Parameters::FieldsSpec.wrap(fields)
45
+ @include_spec = SimpleJsonapi::Parameters::IncludeSpec.wrap(include)
46
+ @sort_spec = SimpleJsonapi::Parameters::SortSpec.wrap(sort_related)
47
+ @extras = extras
48
+ end
49
+
50
+ # @abstract
51
+ # @return [Hash{Symbol => Hash}]
52
+ def as_jsonapi
53
+ raise NotImplementedError, "Implement #{__method__} in each subclass."
54
+ end
55
+
56
+ private
57
+
58
+ def build_child_node(node_class, **options)
59
+ defaults = {
60
+ root_node: root_node,
61
+ serializer_inferrer: serializer_inferrer,
62
+ serializer: serializer,
63
+ fields: fields_spec,
64
+ include: include_spec,
65
+ sort_related: sort_spec,
66
+ extras: extras,
67
+ }
68
+
69
+ node_class.new(defaults.merge(options))
70
+ end
71
+
72
+ def evaluate(callable, object, **data)
73
+ serializer.with(data.merge(extras)) do
74
+ serializer.instance_exec(object, &callable)
75
+ end
76
+ end
77
+
78
+ def render?(definition, object)
79
+ if_proc = definition.if_predicate
80
+ unless_proc = definition.unless_predicate
81
+
82
+ if if_proc && !evaluate(if_proc, object)
83
+ false
84
+ elsif unless_proc && evaluate(unless_proc, object)
85
+ false
86
+ else
87
+ true
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,25 @@
1
+ # Represents a root +data+ node that contains a collection of resources.
2
+ module SimpleJsonapi::Node::Data
3
+ # @!attribute [r] resources
4
+ # @return [Array<Object>]
5
+ class Collection < SimpleJsonapi::Node::Base
6
+ attr_reader :resources
7
+
8
+ # @param resources [Array<Object>]
9
+ # @param options see {Node::Base#initialize} for additional parameters
10
+ def initialize(resources:, **options)
11
+ super(options)
12
+
13
+ @resources = Array.wrap(resources)
14
+
15
+ @resource_nodes = @resources.map do |resource|
16
+ build_child_node(SimpleJsonapi::Node::Resource::Full, resource: resource)
17
+ end
18
+ end
19
+
20
+ # @return [Hash{Symbol => Hash}]
21
+ def as_jsonapi
22
+ { data: @resource_nodes.map(&:as_jsonapi) }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # Represents a root +data+ node that contains a single resource.
2
+ module SimpleJsonapi::Node::Data
3
+ # @!attribute [r] resource
4
+ # @return [Object]
5
+ class Singular < SimpleJsonapi::Node::Base
6
+ attr_reader :resource
7
+
8
+ # @param resource [Object]
9
+ # @param options see {Node::Base#initialize} for additional parameters
10
+ def initialize(resource:, **options)
11
+ super(options)
12
+
13
+ @resource = resource
14
+ @resource_node = build_child_node(SimpleJsonapi::Node::Resource::Full, resource: @resource) unless @resource.nil?
15
+ end
16
+
17
+ # @return [Hash{Symbol => Hash}]
18
+ def as_jsonapi
19
+ if resource.nil?
20
+ { data: nil }
21
+ else
22
+ { data: @resource_node.as_jsonapi }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,62 @@
1
+ module SimpleJsonapi::Node::Document
2
+ # Represents a JSONAPI document.
3
+ # @abstract
4
+ class Base < SimpleJsonapi::Node::Base
5
+ # @param links [Hash{Symbol => String,Hash}]
6
+ # @param meta [Hash{Symbol => Object}]
7
+ # @param options see {Node::Base#initialize} for additional parameters
8
+ def initialize(links: nil, meta: nil, **options)
9
+ @data_node = @links_json = @meta_json = @errors_node = nil
10
+
11
+ super(options.merge(root_node: self))
12
+ validate_options!(options.merge(links: links, meta: meta))
13
+
14
+ @links_json = links if links&.any?
15
+ @meta_json = meta if meta&.any?
16
+ end
17
+
18
+ # @return [Hash{Symbol => Hash}]
19
+ def as_jsonapi
20
+ doc = {}
21
+
22
+ doc.merge!(@data_node.as_jsonapi) if @data_node
23
+ doc.merge!(@errors_node.as_jsonapi) if @errors_node
24
+ doc.merge!(included_node.as_jsonapi)
25
+ doc[:links] = @links_json if @links_json
26
+ doc[:meta] = @meta_json if @meta_json
27
+ # jsonapi
28
+
29
+ doc
30
+ end
31
+
32
+ # @param resource_node [Node::Resource::Base]
33
+ # @return [Boolean]
34
+ def included_resource?(resource_node)
35
+ included_node.included_resource?(resource_node)
36
+ end
37
+
38
+ # @param resource_node [Node::Resource::Full]
39
+ # @return [Node::Resource::Full]
40
+ def append_included_resource(resource_node)
41
+ included_node.append_included_resource(resource_node)
42
+ end
43
+
44
+ private
45
+
46
+ def validate_options!(options)
47
+ links, meta = options.values_at(:links, :meta)
48
+
49
+ unless links.nil? || links.is_a?(Hash)
50
+ raise ArgumentError, "Expected links to be NilClass or Hash but got #{links.class.name}"
51
+ end
52
+
53
+ unless meta.nil? || meta.is_a?(Hash)
54
+ raise ArgumentError, "Expected meta to be NilClass or Hash but got #{meta.class.name}"
55
+ end
56
+ end
57
+
58
+ def included_node
59
+ @included_node ||= build_child_node(SimpleJsonapi::Node::Included)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,17 @@
1
+ module SimpleJsonapi::Node::Document
2
+ # Represents a JSONAPI document whose primary data is a collection of resources.
3
+ # @!attribute [r] resources
4
+ # @return [Array<Object>]
5
+ class Collection < Base
6
+ attr_reader :resources
7
+
8
+ # @param resources [Array<Object>]
9
+ # @param options see {Node::Document::Base#initialize} for additional parameters
10
+ def initialize(resources:, **options)
11
+ super
12
+
13
+ @resources = Array.wrap(resources)
14
+ @data_node = build_child_node(SimpleJsonapi::Node::Data::Collection, resources: @resources)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module SimpleJsonapi::Node::Document
2
+ # Represents a JSONAPI document containing a collection of errors.
3
+ # @!attribute [r] errors
4
+ # @return [Array<Object>]
5
+ class Errors < Base
6
+ attr_reader :errors
7
+
8
+ # @param errors [Array<Object>]
9
+ # @param options see {Node::Document::Base#initialize} for additional parameters
10
+ def initialize(errors:, **options)
11
+ super
12
+
13
+ @errors = Array.wrap(errors)
14
+ @errors_node = build_child_node(SimpleJsonapi::Node::Errors, errors: @errors)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module SimpleJsonapi::Node::Document
2
+ # Represents a JSONAPI document whose primary data is a single resource.
3
+ # @!attribute [r] resource
4
+ # @return [Object]
5
+ class Singular < Base
6
+ attr_reader :resource
7
+
8
+ # @param resource [Object]
9
+ # @param options see {Node::Document::Base#initialize} for additional parameters
10
+ def initialize(resource:, **options)
11
+ super
12
+
13
+ @resource = resource
14
+ @data_node = build_child_node(SimpleJsonapi::Node::Data::Singular, resource: @resource)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ module SimpleJsonapi::Node
2
+ # @!attribute [r] error
3
+ # @return [Object]
4
+ class Error < Base
5
+ attr_reader :error
6
+
7
+ # @param error [Object]
8
+ # @param options see {Node::Base#initialize} for additional parameters
9
+ def initialize(error:, **options)
10
+ super(options)
11
+
12
+ @error = error
13
+ @serializer = serializer_inferrer.infer(error).new
14
+
15
+ @source_node = build_child_node(
16
+ SimpleJsonapi::Node::ErrorSource,
17
+ error: error,
18
+ source_definition: serializer.source_definition,
19
+ )
20
+
21
+ @links_node = build_child_node(
22
+ SimpleJsonapi::Node::ObjectLinks,
23
+ object: error,
24
+ link_definitions: serializer.link_definitions,
25
+ )
26
+
27
+ @meta_node = build_child_node(
28
+ SimpleJsonapi::Node::ObjectMeta,
29
+ object: error,
30
+ meta_definitions: serializer.meta_definitions,
31
+ )
32
+ end
33
+
34
+ # @return [Hash{Symbol => Hash}]
35
+ def as_jsonapi
36
+ json = {}
37
+
38
+ member_definitions_to_render.each do |name, defn|
39
+ json[name] = evaluate(defn.value_proc, error).to_s
40
+ end
41
+
42
+ json.merge!(@source_node.as_jsonapi)
43
+ json.merge!(@links_node.as_jsonapi)
44
+ json.merge!(@meta_node.as_jsonapi)
45
+
46
+ json
47
+ end
48
+
49
+ private
50
+
51
+ def member_definitions_to_render
52
+ @member_definitions_to_render ||= serializer.member_definitions.select { |_, defn| render?(defn, error) }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,40 @@
1
+ module SimpleJsonapi::Node
2
+ # @!attribute [r] error
3
+ # @return [Object]
4
+ # @!attribute [r] source_definition
5
+ # @return [Definition::ErrorSource]
6
+ class ErrorSource < Base
7
+ attr_reader :error, :source_definition
8
+
9
+ # @param error [Object]
10
+ # @param source_definition [Definition::ErrorSource]
11
+ # @param options see {Node::Base#initialize} for additional parameters
12
+ def initialize(error:, source_definition:, **options)
13
+ super(options)
14
+
15
+ @error = error
16
+ @source_definition = source_definition
17
+ end
18
+
19
+ # @return [Hash{Symbol => Hash}]
20
+ def as_jsonapi
21
+ source_json = {}
22
+
23
+ member_definitions_to_render.each do |name, defn|
24
+ source_json[name] = evaluate(defn.value_proc, error).to_s
25
+ end
26
+
27
+ if source_json.any?
28
+ { source: source_json }
29
+ else
30
+ {}
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def member_definitions_to_render
37
+ @member_definitions_to_render ||= source_definition.member_definitions.select { |_, defn| render?(defn, error) }
38
+ end
39
+ end
40
+ end