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,49 @@
1
+ module SimpleJsonapi::Node::Resource
2
+ # Represents a single resource object.
3
+ class Full < Base
4
+ # @param options see {Node::Resource::Base#initialize} for additional parameters
5
+ def initialize(**options)
6
+ super(options)
7
+
8
+ @attributes_node = build_child_node(
9
+ SimpleJsonapi::Node::Attributes,
10
+ resource: resource,
11
+ resource_type: resource_type,
12
+ attribute_definitions: serializer.attribute_definitions,
13
+ )
14
+
15
+ @relationships_node = build_child_node(
16
+ SimpleJsonapi::Node::Relationships,
17
+ resource: resource,
18
+ resource_type: resource_type,
19
+ relationship_definitions: serializer.relationship_definitions,
20
+ )
21
+
22
+ @links_node = build_child_node(
23
+ SimpleJsonapi::Node::ObjectLinks,
24
+ object: resource,
25
+ link_definitions: serializer.link_definitions,
26
+ )
27
+
28
+ @meta_node = build_child_node(
29
+ SimpleJsonapi::Node::ObjectMeta,
30
+ object: resource,
31
+ meta_definitions: serializer.meta_definitions,
32
+ )
33
+ end
34
+
35
+ # @return [Hash{Symbol => Hash}]
36
+ def as_jsonapi
37
+ json = {}
38
+ json[:id] = resource_id
39
+ json[:type] = resource_type
40
+
41
+ json.merge!(@attributes_node.as_jsonapi)
42
+ json.merge!(@relationships_node.as_jsonapi)
43
+ json.merge!(@links_node.as_jsonapi)
44
+ json.merge!(@meta_node.as_jsonapi)
45
+
46
+ json
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ module SimpleJsonapi::Node::Resource
2
+ # Represents a single resource linkage object.
3
+ #
4
+ # @!attribute [r] meta
5
+ # @return [Hash{Symbol => Object}]
6
+ class Linkage < Base
7
+ attr_reader :meta
8
+
9
+ # @param meta [Hash{Symbol => Object}]
10
+ # @param options see {Node::Resource::Base#initialize} for additional parameters
11
+ def initialize(meta: nil, **options)
12
+ super(options)
13
+ @meta = meta
14
+ end
15
+
16
+ # @return [Hash{Symbol => Hash}]
17
+ def as_jsonapi
18
+ json = {}
19
+ json[:id] = resource_id
20
+ json[:type] = resource_type
21
+ json[:meta] = meta if meta.present?
22
+ json
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ module SimpleJsonapi::Parameters
2
+ # Represents the +fields+ parameter as defined by the JSONAPI spec.
3
+ class FieldsSpec
4
+ # Wraps a +fields+ parameter in a {FieldsSpec} instance.
5
+ # @param fields [Hash{Symbol => String},FieldsSpec]
6
+ def self.wrap(fields)
7
+ if fields.is_a?(FieldsSpec)
8
+ fields
9
+ else
10
+ FieldsSpec.new(fields)
11
+ end
12
+ end
13
+
14
+ # @param specs [Hash{Symbol => String,Array<String>,Array<Symbol>}]
15
+ # The hash keys are resource types, and the values are lists of field names to render in the output.
16
+ def initialize(specs = {})
17
+ @data = {}
18
+ merge(specs) if specs.present?
19
+ end
20
+
21
+ # @param specs [Hash{Symbol => String,Array<String>,Array<Symbol>}]
22
+ # The hash keys are resource types, and the values are lists of field names to render in the output.
23
+ # @return [self]
24
+ def merge(specs = {})
25
+ specs.each do |type, fields|
26
+ @data[type.to_sym] = Array
27
+ .wrap(fields)
28
+ .flat_map { |s| s.to_s.split(",") }
29
+ .map { |s| s.strip.to_sym }
30
+ end
31
+ self
32
+ end
33
+
34
+ # @param type [String,Symbol]
35
+ # @return [Array<Symbol>]
36
+ def [](type)
37
+ @data[type.to_sym]
38
+ end
39
+
40
+ # @param type [String,Symbol]
41
+ def all_fields?(type)
42
+ @data[type.to_sym].nil?
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,57 @@
1
+ module SimpleJsonapi::Parameters
2
+ # Represents the +include+ parameter as defined by the JSONAPI spec.
3
+ class IncludeSpec
4
+ # Wraps an +include+ parameter in an {IncludeSpec} instance.
5
+ # @param specs [IncludeSpec,String,Array<String>,Array<Symbol>]
6
+ def self.wrap(specs)
7
+ if specs.is_a?(IncludeSpec)
8
+ specs
9
+ else
10
+ IncludeSpec.new(specs)
11
+ end
12
+ end
13
+
14
+ # @param specs [String,Array<String>,Array<Symbol>]
15
+ # e.g. <code>"author,comments,comments.author"</code> or <code>["author", "comments", "comments.author"]</code>
16
+ def initialize(*specs)
17
+ @data = {}
18
+ merge(*specs) if specs.any?
19
+ end
20
+
21
+ # @param specs [String,Array<String>,Array<Symbol>]
22
+ # e.g. <code>"author,comments,comments.author"</code> or <code>["author", "comments", "comments.author"]</code>
23
+ # @return [self]
24
+ def merge(*specs)
25
+ paths = specs.flatten.flat_map { |s| s.to_s.split(",") }
26
+
27
+ paths.each do |path|
28
+ terms = path.split(".")
29
+
30
+ nested_spec = @data[terms.first.to_sym] ||= IncludeSpec.new
31
+ if terms.size > 1
32
+ nested_spec.merge(terms.drop(1).join("."))
33
+ end
34
+ end
35
+
36
+ self
37
+ end
38
+
39
+ # @param relationship_name [String,Symbol]
40
+ # @return [IncludeSpec]
41
+ def [](relationship_name)
42
+ @data[relationship_name.to_sym]
43
+ end
44
+
45
+ # @param relationship_name [String,Symbol]
46
+ def include?(relationship_name)
47
+ @data.key?(relationship_name.to_sym)
48
+ end
49
+
50
+ # @return [Hash]
51
+ def to_h
52
+ @data.each_with_object({}) do |(name, spec), hash|
53
+ hash[name] = spec.to_h
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,107 @@
1
+ module SimpleJsonapi::Parameters
2
+ # Represents the +sort+ parameter as defined by the JSONAPI spec.
3
+ class SortSpec
4
+ # Wraps a +sort+ parameter in a {SortSpec} instance.
5
+ # @param sorts [SortSpec,Hash{Symbol => String},Hash{Symbol => Array<String>}]
6
+ def self.wrap(sorts)
7
+ if sorts.is_a?(SortSpec)
8
+ sorts
9
+ else
10
+ SortSpec.new(sorts)
11
+ end
12
+ end
13
+
14
+ # Creates a {SortSpec} that raises an error when it's called.
15
+ # @return [SortSpec]
16
+ def self.not_supported
17
+ @not_supported ||= new.tap do |spec|
18
+ spec.instance_variable_set(:@not_supported, true)
19
+ end
20
+ end
21
+
22
+ delegate :inspect, :to_s, to: :@data
23
+
24
+ # @param specs [Hash{Symbol => String},Hash{Symbol => Array<String>}]
25
+ # e.g., { comments: "-date,author" }
26
+ def initialize(specs = {})
27
+ @not_supported = nil
28
+ @data = Hash.new { |_h, _k| [] }
29
+ merge(specs) if specs.present?
30
+ end
31
+
32
+ # @param specs [Hash{Symbol => String},Hash{Symbol => Array<String>}]
33
+ # e.g., { comments: "-date,author" } or { comments: ["-date", "author"] }
34
+ # @return [self]
35
+ def merge(specs = {})
36
+ specs.each do |relationship_name, field_specs|
37
+ @data[relationship_name.to_sym] = Array
38
+ .wrap(field_specs)
39
+ .flat_map { |fs| fs.to_s.split(",") }
40
+ .map { |fs| SortFieldSpec.new(fs) }
41
+ .presence
42
+ end
43
+ self
44
+ end
45
+
46
+ # @param relationship_name [String,Symbol]
47
+ # @return [SortFieldSpec]
48
+ def [](relationship_name)
49
+ if not_supported?
50
+ raise NotImplementedError, "Sorting nested relationships is not implemented."
51
+ else
52
+ @data[relationship_name.to_sym]
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ def not_supported?
59
+ !!@not_supported
60
+ end
61
+ end
62
+
63
+ # Represents a single field (and direction) in a {SortSpec}.
64
+ # @!attribute [rw] field
65
+ # @return Symbol
66
+ # @!attribute [rw] dir
67
+ # @return [:asc,:desc]
68
+ class SortFieldSpec
69
+ attr_accessor :field, :dir
70
+
71
+ # @param spec [String]
72
+ def initialize(spec)
73
+ if spec =~ /\A(-?)(\w+)\Z/
74
+ self.field = $2.to_sym
75
+ self.dir = ($1 == '-' ? :desc : :asc)
76
+ else
77
+ raise ArgumentError, "field spec must match 'field' or '-field'"
78
+ end
79
+ end
80
+
81
+ def asc?
82
+ dir == :asc
83
+ end
84
+
85
+ def desc?
86
+ dir == :desc
87
+ end
88
+
89
+ def dup
90
+ SortFieldSpec.new(to_s)
91
+ end
92
+
93
+ def ==(other)
94
+ other.respond_to?(:field) && other.respond_to?(:dir) && [field, dir] == [other.field, other.dir]
95
+ end
96
+ alias_method :eql?, :==
97
+
98
+ def hash
99
+ [field, dir].hash
100
+ end
101
+
102
+ def to_s
103
+ "#{'-' if desc?}#{field}"
104
+ end
105
+ alias_method :inspect, :to_s
106
+ end
107
+ end
@@ -0,0 +1,89 @@
1
+ # Subclass {Serializer} to create serializers for specific types of resources.
2
+ class SimpleJsonapi::Serializer
3
+ include SimpleJsonapi::SerializerMethods
4
+
5
+ class << self
6
+ # @overload (see Definition::Resource#id)
7
+ # @return (see Definition::Resource#id)
8
+ def id(*args, &block)
9
+ definition.id(*args, &block)
10
+ end
11
+
12
+ # @overload (see Definition::Resource#type)
13
+ # @return (see Definition::Resource#type)
14
+ def type(*args, &block)
15
+ definition.type(*args, &block)
16
+ end
17
+
18
+ # @overload (see Definition::Resource#id)
19
+ # @return (see Definition::Resource#id)
20
+ # @overload attribute(name, options = {})
21
+ # @overload attribute(name, options = {}, &block)
22
+ # @return (see Definition::Resource#attribute)
23
+ def attribute(name, **options, &block)
24
+ definition.attribute(name, **options, &block)
25
+ end
26
+
27
+ # @overload (see Definition::Resource#has_one)
28
+ # @param (see Definition::Resource#has_one)
29
+ # @yieldparam (see Definition::Resource#has_one)
30
+ # @yieldreturn (see Definition::Resource#has_one)
31
+ # @return (see Definition::Resource#has_one)
32
+ def has_one(name, **options, &block)
33
+ definition.has_one(name, **options, &block)
34
+ end
35
+
36
+ # @overload (see Definition::Resource#has_many)
37
+ # @param (see Definition::Resource#has_many)
38
+ # @yieldparam (see Definition::Resource#has_many)
39
+ # @yieldreturn (see Definition::Resource#has_many)
40
+ # @return (see Definition::Resource#has_many)
41
+ def has_many(name, **options, &block)
42
+ definition.has_many(name, **options, &block)
43
+ end
44
+
45
+ # @overload (see Definition::Concerns::HasLinksObject#link)
46
+ # @return (see Definition::Concerns::HasLinksObject#link)
47
+ def link(name, *args, **options, &block)
48
+ definition.link(name, *args, **options, &block)
49
+ end
50
+
51
+ # @overload (see Definition::Concerns::HasMetaObject#meta)
52
+ # @return (see Definition::Concerns::HasMetaObject#meta)
53
+ def meta(name, *args, **options, &block)
54
+ definition.meta(name, *args, **options, &block)
55
+ end
56
+ end
57
+
58
+ self.definition = SimpleJsonapi::Definition::Resource.new
59
+
60
+ # @return (see Definition::Resource#id_definition)
61
+ def id_definition
62
+ definition.id_definition
63
+ end
64
+
65
+ # @return (see Definition::Resource#type_definition)
66
+ def type_definition
67
+ definition.type_definition
68
+ end
69
+
70
+ # @return (see Definition::Resource#attribute_definitions)
71
+ def attribute_definitions
72
+ definition.attribute_definitions
73
+ end
74
+
75
+ # @return (see Definition::Resource#relationship_definitions)
76
+ def relationship_definitions
77
+ definition.relationship_definitions
78
+ end
79
+
80
+ # @return (see Definition::Concerns::HasLinksObject#link_definitions)
81
+ def link_definitions
82
+ definition.link_definitions
83
+ end
84
+
85
+ # @return (see Definition::Concerns::HasMetaObject#meta_definitions)
86
+ def meta_definitions
87
+ definition.meta_definitions
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleJsonapi
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'simple_jsonapi/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'simple_jsonapi'
8
+ spec.version = SimpleJsonapi::VERSION
9
+ spec.license = "MIT"
10
+ spec.authors = ['PatientsLikeMe']
11
+ spec.email = ['engineers@patientslikeme.com']
12
+ spec.homepage = 'https://www.patientslikeme.com'
13
+
14
+ spec.summary = 'A library for building JSONAPI documents in Ruby.'
15
+ spec.description = 'A library for building JSONAPI documents in Ruby.'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.test_files = spec.files.grep(%r{^test/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency 'activesupport', '~> 5.1'
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.10'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'minitest'
26
+ spec.add_development_dependency 'minitest-reporters'
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'mocha'
29
+ end
@@ -0,0 +1,34 @@
1
+ require 'test_helper'
2
+
3
+ class BadRequestTest < Minitest::Spec
4
+ let(:error) do
5
+ SimpleJsonapi::Errors::BadRequest.new(
6
+ id: "the id",
7
+ status: "the status",
8
+ code: "the code",
9
+ title: "the title",
10
+ detail: "the detail",
11
+ source_pointer: "the source pointer",
12
+ source_parameter: "the source parameter",
13
+ about_link: "the about link",
14
+ )
15
+ end
16
+
17
+ describe SimpleJsonapi::Errors::BadRequest do
18
+ describe "properties" do
19
+ it "are assigned specific values" do
20
+ assert_equal "400", error.status
21
+ assert_equal "bad_request", error.code
22
+ assert_equal "Bad request", error.title
23
+ end
24
+
25
+ it "are stored" do
26
+ assert_equal "the id", error.id
27
+ assert_equal "the detail", error.detail
28
+ assert_equal "the source pointer", error.source_pointer
29
+ assert_equal "the source parameter", error.source_parameter
30
+ assert_equal "the about link", error.about_link
31
+ end
32
+ end
33
+ end
34
+ end