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,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