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