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,60 @@
1
+ # Represents a single relationship on a resource
2
+ #
3
+ # @!attribute [r] name
4
+ # @return [Symbol]
5
+ # @!attribute [r] cardinality
6
+ # @return [:singular,:collection]
7
+ # @!attribute [r] serializer_inferrer
8
+ # @return [SerializerInferrer]
9
+ # @!attribute [r] description
10
+ # @return [String]
11
+ # @!attribute [r] data_definition
12
+ # @return [Proc]
13
+ class SimpleJsonapi::Definition::Relationship < SimpleJsonapi::Definition::Base
14
+ include SimpleJsonapi::Definition::Concerns::HasLinksObject
15
+ include SimpleJsonapi::Definition::Concerns::HasMetaObject
16
+
17
+ attr_reader :name, :cardinality, :serializer_inferrer, :description, :data_definition
18
+
19
+ # @param name [Symbol]
20
+ # @param cardinality [Symbol] +:singular+, +:collection+
21
+ # @param description [String]
22
+ # @yieldparam resource [Object]
23
+ # @yieldreturn [Object,Array<Object>] The related resource or resources
24
+ # @option (see Definition::Base#initialize)
25
+ def initialize(name, cardinality:, description: nil, **options, &block)
26
+ super
27
+
28
+ unless %i[singular collection].include?(cardinality)
29
+ raise ArgumentError, "Cardinality must be :singular or :collection"
30
+ end
31
+
32
+ @name = name.to_sym
33
+ @cardinality = cardinality.to_sym
34
+ @description = description.to_s.presence
35
+ @serializer_inferrer = SimpleJsonapi::SerializerInferrer.wrap(options[:serializer])
36
+
37
+ instance_eval(&block) if block_given?
38
+
39
+ data { |resource| resource.public_send(name) } unless data_definition
40
+ end
41
+
42
+ private def initialize_dup(new_def)
43
+ super
44
+ # name and cardinality are symbols, can't be duped
45
+ # serializer_inferrer doesn't need to be duped?
46
+ end
47
+
48
+ # @return [void]
49
+ def data(&block)
50
+ @data_definition = block
51
+ end
52
+
53
+ def singular?
54
+ cardinality == :singular
55
+ end
56
+
57
+ def collection?
58
+ cardinality == :collection
59
+ end
60
+ end
@@ -0,0 +1,104 @@
1
+ # Represents a single resource object.
2
+ #
3
+ # @!attribute [r] id_definition
4
+ # @return [Proc]
5
+ # @!attribute [r] type_definition
6
+ # @return [Proc]
7
+ # @!attribute [r] attribute_definitions
8
+ # @return [Hash{Symbol => Attribute}]
9
+ # @!attribute [r] relationship_definitions
10
+ # @return [Hash{Symbol => Relationship}]
11
+ class SimpleJsonapi::Definition::Resource < SimpleJsonapi::Definition::Base
12
+ include SimpleJsonapi::Definition::Concerns::HasLinksObject
13
+ include SimpleJsonapi::Definition::Concerns::HasMetaObject
14
+
15
+ attr_reader :id_definition, :type_definition, :attribute_definitions, :relationship_definitions
16
+
17
+ def initialize
18
+ super
19
+ @id_definition = wrap_in_proc(&:id)
20
+ @type_definition = wrap_in_proc do |resource|
21
+ resource.class.name.demodulize.underscore.pluralize
22
+ end
23
+
24
+ @attribute_definitions = {}
25
+ @relationship_definitions = {}
26
+ end
27
+
28
+ private def initialize_dup(new_def)
29
+ super
30
+ new_def.instance_variable_set(:@id_definition, @id_definition.dup) unless @id_definition.nil?
31
+ new_def.instance_variable_set(:@type_definition, @type_definition.dup) unless @type_definition.nil?
32
+
33
+ unless @attribute_definitions.nil?
34
+ new_def.instance_variable_set(:@attribute_definitions, @attribute_definitions.dup)
35
+ end
36
+
37
+ unless @relationship_definitions.nil?
38
+ new_def.instance_variable_set(:@relationship_definitions, @relationship_definitions.dup)
39
+ end
40
+ end
41
+
42
+ # @overload id(&block)
43
+ # @overload id(value)
44
+ # @yieldparam resource [Object]
45
+ # @yieldreturn [String]
46
+ # @return [void]
47
+ def id(*args, &block)
48
+ @id_definition = wrap_in_proc(*args, &block)
49
+ end
50
+
51
+ # @overload type(&block)
52
+ # @overload type(value)
53
+ # @yieldparam resource [Object]
54
+ # @yieldreturn [String]
55
+ # @return [void]
56
+ def type(*args, &block)
57
+ @type_definition = wrap_in_proc(*args, &block)
58
+ end
59
+
60
+ # @overload attribute(name, type: nil, description: nil, **options)
61
+ # @overload attribute(name, type: nil, description: nil, **options, &block)
62
+ # @option (see Definition::Base#initialize)
63
+ # @option options [Symbol] type data type
64
+ # @option options [String] description
65
+ # @yieldparam resource [Object]
66
+ # @yieldreturn [#to_json] the value
67
+ # @return [void]
68
+ def attribute(name, **options, &block)
69
+ # Allow type attribute to be defined before throwing error to support non-compliant data_comleteness/v1
70
+ attribute_definitions[name.to_sym] = SimpleJsonapi::Definition::Attribute.new(name, **options, &block)
71
+
72
+ if %w[id type].include?(name.to_s)
73
+ raise ArgumentError, "`#{name}` is not allowed as an attribute name"
74
+ end
75
+ end
76
+
77
+ # @overload has_one(name, description: nil, **options, &block)
78
+ # @param name [Symbol]
79
+ # @option (see Definition::Relationship#initialize)
80
+ # @option options [String] description
81
+ # @yieldparam (see Definition::Relationship#initialize)
82
+ # @yieldreturn (see Definition::Relationship#initialize)
83
+ # @return [void]
84
+ def has_one(name, **options, &block)
85
+ relationship(name, cardinality: :singular, **options, &block)
86
+ end
87
+
88
+ # @overload has_many(name, description: nil, **options, &block)
89
+ # @param name [Symbol]
90
+ # @option (see Definition::Relationship#initialize)
91
+ # @option options [String] description
92
+ # @yieldparam (see Definition::Relationship#initialize)
93
+ # @yieldreturn (see Definition::Relationship#initialize)
94
+ # @return [void]
95
+ def has_many(name, **options, &block)
96
+ relationship(name, cardinality: :collection, **options, &block)
97
+ end
98
+
99
+ private
100
+
101
+ def relationship(name, **options, &block)
102
+ relationship_definitions[name.to_sym] = SimpleJsonapi::Definition::Relationship.new(name, options, &block)
103
+ end
104
+ end
@@ -0,0 +1,76 @@
1
+ # Subclass {ErrorSerializer} to create serializers for specific types of errors.
2
+ class SimpleJsonapi::ErrorSerializer
3
+ include SimpleJsonapi::SerializerMethods
4
+
5
+ class << self
6
+ # @overload (see Definition::Error#id)
7
+ # @return (see Definition::Error#id)
8
+ def id(*args, **options, &block)
9
+ definition.id(*args, **options, &block)
10
+ end
11
+
12
+ # @overload (see Definition::Error#status)
13
+ # @return (see Definition::Error#status)
14
+ def status(*args, **options, &block)
15
+ definition.status(*args, **options, &block)
16
+ end
17
+
18
+ # @overload (see Definition::Error#code)
19
+ # @return (see Definition::Error#code)
20
+ def code(*args, **options, &block)
21
+ definition.code(*args, **options, &block)
22
+ end
23
+
24
+ # @overload (see Definition::Error#title)
25
+ # @return (see Definition::Error#title)
26
+ def title(*args, **options, &block)
27
+ definition.title(*args, **options, &block)
28
+ end
29
+
30
+ # @overload (see Definition::Error#detail)
31
+ # @return (see Definition::Error#detail)
32
+ def detail(*args, **options, &block)
33
+ definition.detail(*args, **options, &block)
34
+ end
35
+
36
+ # @overload (see Definition::Error#source)
37
+ # @return (see Definition::Error#source)
38
+ def source(*args, &block)
39
+ definition.source(*args, &block)
40
+ end
41
+
42
+ # @overload (see Definition::Error#about_link)
43
+ # @return (see Definition::Error#about_link)
44
+ def about_link(*args, **options, &block)
45
+ definition.about_link(*args, **options, &block)
46
+ end
47
+
48
+ # @overload (see Definition::Concerns::HasMetaObject#meta)
49
+ # @return (see Definition::Concerns::HasMetaObject#meta)
50
+ def meta(name, *args, **options, &block)
51
+ definition.meta(name, *args, **options, &block)
52
+ end
53
+ end
54
+
55
+ self.definition = SimpleJsonapi::Definition::Error.new
56
+
57
+ # @return (see Definition::Error#member_definitions)
58
+ def member_definitions
59
+ definition.member_definitions
60
+ end
61
+
62
+ # @return (see Definition::Error#source_definitions)
63
+ def source_definition
64
+ definition.source_definition
65
+ end
66
+
67
+ # @return (see Definition::Concerns::HasLinksObject#link_definitions)
68
+ def link_definitions
69
+ definition.link_definitions
70
+ end
71
+
72
+ # @return (see Definition::Concerns::HasMetaObject#meta_definitions)
73
+ def meta_definitions
74
+ definition.meta_definitions
75
+ end
76
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'wrapped_error'
2
+
3
+ class SimpleJsonapi::Errors::BadRequest < SimpleJsonapi::Errors::WrappedError
4
+ def initialize(_cause = nil, **attributes)
5
+ super attributes.merge(
6
+ status: "400",
7
+ code: "bad_request",
8
+ title: "Bad request",
9
+ )
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ # A generic serializer for Ruby +Exception+ objects.
2
+ class SimpleJsonapi::Errors::ExceptionSerializer < SimpleJsonapi::ErrorSerializer
3
+ code { |err| err.class.name.underscore.tr('/', '_') }
4
+ title { |err| err.class.name }
5
+ detail(&:message)
6
+ end
@@ -0,0 +1,35 @@
1
+ module SimpleJsonapi::Errors
2
+ # A generic serializable error class.
3
+ class WrappedError
4
+ # @!attribute [rw] cause
5
+ # The original error.
6
+ # @return [Object]
7
+ # @!attribute [rw] id
8
+ # @return [String]
9
+ # @!attribute [rw] status
10
+ # @return [String]
11
+ # @!attribute [rw] code
12
+ # @return [String]
13
+ # @!attribute [rw] title
14
+ # @return [String]
15
+ # @!attribute [rw] detail
16
+ # @return [String]
17
+ # @!attribute [rw] source_pointer
18
+ # @return [String]
19
+ # @!attribute [rw] source_parameter
20
+ # @return [String]
21
+ # @!attribute [rw] about_link
22
+ # @return [String]
23
+ attr_accessor :cause, :id, :status, :code, :title, :detail, :source_pointer, :source_parameter, :about_link
24
+
25
+ # @param cause [Object] The underlying error
26
+ # @param attributes [Hash{Symbol => String}]
27
+ def initialize(cause = nil, **attributes)
28
+ self.cause = cause
29
+
30
+ attributes.each do |name, value|
31
+ send("#{name}=", value) if respond_to?("#{name}=")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # A serializer for {WrappedError} objects.
2
+ class SimpleJsonapi::Errors::WrappedErrorSerializer < SimpleJsonapi::ErrorSerializer
3
+ id(if: ->(err) { err.id.to_s.present? }) do |err|
4
+ err.id.to_s.presence
5
+ end
6
+
7
+ status(if: ->(err) { err.status.to_s.present? }) do |err|
8
+ err.status.to_s.presence
9
+ end
10
+
11
+ code(if: ->(err) { err.code.to_s.present? }) do |err|
12
+ err.code.to_s.presence
13
+ end
14
+
15
+ title(if: ->(err) { err.title.to_s.present? }) do |err|
16
+ err.title.to_s.presence
17
+ end
18
+
19
+ detail(if: ->(err) { err.detail.to_s.present? }) do |err|
20
+ err.detail.to_s.presence
21
+ end
22
+
23
+ source do
24
+ pointer(if: ->(err) { err.source_pointer.to_s.present? }) do |err|
25
+ err.source_pointer.to_s.presence
26
+ end
27
+ parameter(if: ->(err) { err.source_parameter.to_s.present? }) do |err|
28
+ err.source_parameter.to_s.presence
29
+ end
30
+ end
31
+
32
+ about_link(if: ->(err) { err.about_link.to_s.present? }) do |err|
33
+ err.about_link.to_s.presence
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ module SimpleJsonapi
2
+ # The error raised when a document does not have a valid JSONAPI structure.
3
+ class InvalidJsonStructureError < StandardError
4
+ end
5
+
6
+ # The error raised when the same resource is added to the +included+ member twice.
7
+ class DuplicateResourceError < StandardError
8
+ attr_reader :type, :id
9
+
10
+ # @param type [String]
11
+ # @param id [String]
12
+ # @param msg [String]
13
+ def initialize(type, id, msg = nil)
14
+ @type = type
15
+ @id = id
16
+ @msg = msg
17
+ end
18
+
19
+ def to_s
20
+ @msg.present? ? @msg : "Resource with type #{type} and id #{id} is already included"
21
+ end
22
+ end
23
+
24
+ # The error raised when a {SerializerInferrer} cannot find the serializer for a resource or error.
25
+ class SerializerInferenceError < StandardError
26
+ attr_reader :object
27
+
28
+ # @param object [Object] the resource or error
29
+ # @param msg [String]
30
+ def initialize(object, msg = nil)
31
+ @object = object
32
+ @msg = msg
33
+ end
34
+
35
+ def to_s
36
+ @msg.present? ? @msg : "Unable to infer serializer for #{object.class}"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,136 @@
1
+ module SimpleJsonapi
2
+ # Identifies the serializer class that should be used for a resource or error object.
3
+ class SerializerInferrer
4
+ # @!attribute [r] namespace
5
+ # @return [String]
6
+ attr_reader :namespace
7
+
8
+ # # A {SerializerInferrer} that always returns the serializer class provided in the constructor.
9
+ # class Constant
10
+ # # @param serializer_class [Serializer,ErrorSerializer]
11
+ # def initialize(serializer_class)
12
+ # @serializer_class = case serializer_class
13
+ # when Class then serializer_class
14
+ # else serializer_class.to_s.constantize
15
+ # end
16
+
17
+ # super { |resource| @serializer_class }
18
+ # end
19
+ # end
20
+
21
+ # @param serializer [SerializerInferrer,Serializer,ErrorSerializer,nil]
22
+ # @return [SerializerInferrer]
23
+ def self.wrap(serializer)
24
+ if serializer.is_a?(SerializerInferrer)
25
+ serializer
26
+ elsif serializer.present?
27
+ klass = serializer_class(serializer)
28
+ SerializerInferrer.new { |_resource| klass }
29
+ else
30
+ SimpleJsonapi.serializer_inferrer
31
+ end
32
+ end
33
+
34
+ # @param explicit_mappings [Hash{Class => Class}] A mapping of resource classes to serializer classes
35
+ # @param namespace [String] A namespace in which to search for serializer classes
36
+ # @yieldparam object [Object] The resource or error
37
+ # @yieldreturn [Class] A serializer class
38
+ def initialize(explicit_mappings: nil, namespace: nil, &block)
39
+ @explicit_mappings = {}
40
+ @explicit_mappings.merge!(explicit_mappings) if explicit_mappings
41
+
42
+ @namespace = namespace
43
+ @infer_proc = block
44
+ end
45
+
46
+ delegate :each, to: :@explicit_mappings
47
+
48
+ # @param explicit_mappings [Hash{Class => Class}]
49
+ # @return [self]
50
+ def merge(explicit_mappings)
51
+ explicit_mappings.each do |resource_class, serializer_class|
52
+ add(resource_class, serializer_class)
53
+ end
54
+ self
55
+ end
56
+
57
+ # @param resource_class [Class]
58
+ # @param serializer_class [Class]
59
+ # @return [void]
60
+ def add(resource_class, serializer_class)
61
+ @explicit_mappings[resource_class.name] = serializer_class
62
+ end
63
+
64
+ # @param resource [Object]
65
+ # @return [Class] A serializer class
66
+ # @raise [SerializerInferenceError] if a serializer isn't found
67
+ def infer(resource)
68
+ serializer = (@infer_proc || default_infer_proc).call(resource)
69
+ raise SerializerInferenceError.new(resource) unless serializer
70
+ serializer
71
+ end
72
+
73
+ # @return [Class,nil]
74
+ def default_serializer
75
+ unless defined?(@default_serializer)
76
+ begin
77
+ @default_serializer = infer(nil)
78
+ rescue SerializerInferenceError
79
+ @default_serializer = nil
80
+ end
81
+ end
82
+ @default_serializer
83
+ end
84
+
85
+ def default_serializer?
86
+ default_serializer != nil
87
+ end
88
+
89
+ private
90
+
91
+ def default_infer_proc
92
+ @default_infer_proc ||= proc do |resource|
93
+ serializer = nil
94
+
95
+ resource.class.ancestors.find do |ancestor|
96
+ serializer = find_serializer_by_name(ancestor.name)
97
+ end
98
+
99
+ serializer
100
+ end
101
+ end
102
+
103
+ def find_serializer_by_name(name)
104
+ if @explicit_mappings.key?(name)
105
+ @explicit_mappings[name]
106
+ else
107
+ serializer_name_for_class_name(name).safe_constantize
108
+ end
109
+ end
110
+
111
+ def serializer_name_for_class_name(resource_class_name)
112
+ "#{prefix(resource_class_name)}#{resource_class_name}Serializer"
113
+ end
114
+
115
+ def prefix(resource_class_name)
116
+ if namespace.blank?
117
+ ""
118
+ elsif resource_class_name.starts_with?("#{namespace}::")
119
+ ""
120
+ else
121
+ "#{namespace}::"
122
+ end
123
+ end
124
+
125
+ def self.serializer_class(serializer)
126
+ case serializer
127
+ when Class
128
+ serializer
129
+ else
130
+ serializer.to_s.constantize
131
+ end
132
+ end
133
+
134
+ private_class_method :serializer_class
135
+ end
136
+ end