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