jsonapi-serializable 0.1.3 → 0.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f59f8e3e6a7affd39f462919816e0931c277699e
4
- data.tar.gz: 11647e5104f611adafd09c25e3c81d1b3c26d2f4
3
+ metadata.gz: a2bfb6228e6b03c8bde3724af76fdbcedc097e0e
4
+ data.tar.gz: 28e75ac8293502f8e2a617c79fff3f803ca172d7
5
5
  SHA512:
6
- metadata.gz: 104a61271b884fd79e98308850ee6943c14a195171cccfb00f77a4cd3faeeea38eae0665bb11403bec1674ac5c3b9ee8315552d3cbc611c7df5dbfe949ed7dbe
7
- data.tar.gz: 9e840331b935f596a62f1b15fca6e66e74810a0db04d0cde0796bf0ef0cdb80ebba5643995de26bb44f68e58e5bf34d627c486583393b5e7dbdd61fd19704815
6
+ metadata.gz: 909fa65c618b4507da112491cc7b84646c07146dd027c5ba52386509ed9ea090916b413e78940f3fd4dad8cc2c518d56bd727953ad871ef4b404205ba5c9f39d
7
+ data.tar.gz: 8d924e246526821a8ab66d571054cdbe50b235c3d909e5c4c3ca935d6f57fac9b6221e3dcde1ce81eeebfcd32e99bf40bbc87e980a1113c0811bf7c76a32133a
@@ -35,6 +35,12 @@ module JSONAPI
35
35
  :link_blocks
36
36
  end
37
37
 
38
+ def self.create(hash)
39
+ hash.each_with_object(new) do |(k, v), e|
40
+ e.instance_variable_set("@_#{k}", v)
41
+ end
42
+ end
43
+
38
44
  self.link_blocks = {}
39
45
 
40
46
  def self.inherited(klass)
@@ -6,9 +6,10 @@ module JSONAPI
6
6
  class Relationship
7
7
  include DSL
8
8
 
9
- def initialize(exposures = {}, &block)
9
+ def initialize(exposures = {}, options = {}, &block)
10
10
  exposures.each { |k, v| instance_variable_set("@#{k}", v) }
11
11
  @_exposures = exposures
12
+ @_options = options
12
13
  @_links = {}
13
14
  instance_eval(&block)
14
15
  end
@@ -5,14 +5,13 @@ module JSONAPI
5
5
  class Relationship
6
6
  module DSL
7
7
  # Declare the related resources for this relationship.
8
- # @param [String,Constant,Hash{Symbol=>String,Constant}] resource_class
9
8
  # @yieldreturn The related resources for this relationship.
10
9
  # If it is nil, an object implementing the Serializable::Resource
11
10
  # interface, an empty array, or an array of objects implementing the
12
11
  # Serializable::Resource interface, then it is used as is.
13
12
  # Otherwise an appropriate Serializable::Resource subclass is inferred
14
- # from the object(s)' namespace/class, the resource_class parameter if
15
- # provided, and the @_resource_inferrer.
13
+ # from the object(s)' namespace/class, the `class` relationship
14
+ # option, and the @_resource_builder.
16
15
  #
17
16
  # @example
18
17
  # data do
@@ -29,21 +28,11 @@ module JSONAPI
29
28
  # @user.posts
30
29
  # end
31
30
  # end
32
- #
33
- # @example
34
- # data SerializablePost do
35
- # @user.posts
36
- # end
37
- #
38
- # @example
39
- # data "SerializableUser" do
40
- # @post.author
41
- # end
42
- def data(resource_class = nil)
31
+ def data
43
32
  # NOTE(beauby): Lazify computation since it is only needed when
44
33
  # the corresponding relationship is included.
45
34
  @_resources_block = proc do
46
- _resources_for(yield, resource_class)
35
+ @_resource_builder.build(yield, @_exposures, @_options[:class])
47
36
  end
48
37
  end
49
38
 
@@ -106,15 +95,6 @@ module JSONAPI
106
95
  def link(name, &block)
107
96
  @_links[name] = Link.as_jsonapi(@_exposures, &block)
108
97
  end
109
-
110
- private
111
-
112
- # @api private
113
- def _resources_for(objects, resource_class)
114
- resource_class ||= @_resource_inferrer
115
-
116
- ResourceBuilder.build(objects, @_exposures, resource_class)
117
- end
118
98
  end
119
99
  end
120
100
  end
@@ -3,77 +3,86 @@ require 'jsonapi/serializable/resource_builder'
3
3
 
4
4
  module JSONAPI
5
5
  module Serializable
6
- class Renderer
7
- def self.render(objects, options = {})
8
- new(objects, options).render
6
+ class SuccessRenderer
7
+ def initialize(renderer = JSONAPI::Renderer.new)
8
+ @renderer = renderer
9
9
  end
10
10
 
11
- def initialize(objects, options)
12
- @objects = objects
13
- @options = options.dup
14
- @klass = @options.delete(:class)
15
- @namespace = @options.delete(:namespace)
16
- @inferrer = @options.delete(:inferrer) || namespace_inferrer
17
- @exposures = @options.delete(:expose) || {}
18
- @exposures[:_resource_inferrer] = @inferrer
11
+ # Serialize resources into a JSON API document.
12
+ #
13
+ # @param resources [nil,Object,Array]
14
+ # @param options [Hash]@see JSONAPI.render
15
+ # @option class [Class,Symbol,String,Hash{Symbol,String=>Class,Symbol,String}]
16
+ # The serializable resource class(es) to use for the primary resources.
17
+ # @option namespace [String] The namespace in which to look for
18
+ # serializable resource classes.
19
+ # @option inferrer [#call] The callable used for inferring a serializable
20
+ # resource class name from a resource class name.
21
+ # @option expose [Hash] The exposures made available in serializable
22
+ # resource class instances as instance variables.
23
+ # @return [Hash]
24
+ #
25
+ # @example
26
+ # renderer.render(nil)
27
+ # # => { data: nil }
28
+ #
29
+ # @example
30
+ # renderer.render(user)
31
+ # # => {
32
+ # data: {
33
+ # type: 'users',
34
+ # id: 'foo',
35
+ # attributes: { ... },
36
+ # relationships: { ... }
37
+ # }
38
+ # }
39
+ #
40
+ # @example
41
+ # renderer.render([user1, user2])
42
+ # # => { data: [{ type: 'users', id: 'foo', ... }, ...] }
43
+ def render(resources, options = {})
44
+ options = options.dup
45
+ klass = options.delete(:class)
46
+ namespace = options.delete(:namespace)
47
+ inferrer = options.delete(:inferrer) || namespace_inferrer(namespace)
48
+ expose = options.delete(:expose) || {}
49
+ resource_builder = JSONAPI::Serializable::ResourceBuilder.new(inferrer)
50
+ exposures = expose.merge(_resource_builder: resource_builder)
19
51
 
20
- freeze
21
- end
52
+ resources = resource_builder.build(resources, exposures, klass)
22
53
 
23
- def render
24
- JSONAPI.render(jsonapi_params.merge(data: jsonapi_resources)).to_json
54
+ @renderer.render(options.merge(data: resources))
25
55
  end
26
56
 
27
57
  private
28
58
 
29
59
  # @api private
30
- def jsonapi_params
31
- @options
32
- end
33
-
34
- # @api private
35
- def jsonapi_resources
36
- toplevel_inferrer = @klass || @inferrer
37
- JSONAPI::Serializable::ResourceBuilder.build(@objects,
38
- @exposures,
39
- toplevel_inferrer)
40
- end
41
-
42
- # @api private
43
- def namespace_inferrer
44
- return nil unless @namespace
60
+ def namespace_inferrer(namespace)
45
61
  proc do |class_name|
46
62
  names = class_name.split('::')
47
63
  klass = names.pop
48
- [@namespace, *names, "Serializable#{klass}"].reject(&:nil?).join('::')
64
+ [namespace, *names, "Serializable#{klass}"].reject(&:nil?).join('::')
49
65
  end
50
66
  end
51
67
  end
52
68
 
53
- class ErrorRenderer
54
- def self.render(errors, options)
55
- new(errors, options).render
56
- end
57
-
58
- def initialize(errors, options)
59
- @errors = errors
60
- @options = options.dup
69
+ class ErrorsRenderer
70
+ def initialize(renderer = JSONAPI::Renderer.new)
71
+ @renderer = renderer
61
72
  end
62
73
 
63
- def render
64
- JSONAPI.render(jsonapi_params.merge(errors: jsonapi_errors)).to_json
65
- end
66
-
67
- private
68
-
69
- # @api private
70
- def jsonapi_params
71
- @options
72
- end
73
-
74
- # @api private
75
- def jsonapi_errors
76
- @errors
74
+ # Serialize errors into a JSON API document.
75
+ #
76
+ # @param errors [Array]
77
+ # @param options [Hash] @see JSONAPI.render
78
+ # @return [Hash]
79
+ #
80
+ # @example
81
+ # error = JSONAPI::Serializable::Error.create(id: 'foo', title: 'bar')
82
+ # renderer.render([error])
83
+ # # => { errors: [{ id: 'foo', title: 'bar' }] }
84
+ def render(errors, options = {})
85
+ @renderer.render(options.merge(errors: errors))
77
86
  end
78
87
  end
79
88
  end
@@ -1,9 +1,9 @@
1
- require 'jsonapi/serializable/resource/id'
2
- require 'jsonapi/serializable/resource/type'
3
- require 'jsonapi/serializable/resource/meta'
4
- require 'jsonapi/serializable/resource/links'
5
- require 'jsonapi/serializable/resource/attributes'
6
- require 'jsonapi/serializable/resource/relationships'
1
+ require 'jsonapi/serializable/resource_builder'
2
+
3
+ require 'jsonapi/serializable/resource/dsl'
4
+
5
+ require 'jsonapi/serializable/link'
6
+ require 'jsonapi/serializable/relationship'
7
7
 
8
8
  require 'jsonapi/serializable/resource/conditional_fields'
9
9
  require 'jsonapi/serializable/resource/key_format'
@@ -11,24 +11,61 @@ require 'jsonapi/serializable/resource/key_format'
11
11
  module JSONAPI
12
12
  module Serializable
13
13
  class Resource
14
- prepend Id
15
- prepend Type
16
- prepend Meta
17
- prepend Links
18
- prepend Attributes
19
- prepend Relationships
14
+ extend DSL
20
15
 
21
16
  # Default the value of id.
22
17
  id { @object.public_send(:id).to_s }
23
18
 
19
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
24
20
  def initialize(exposures = {})
25
- exposures.each { |k, v| instance_variable_set("@#{k}", v) }
26
- @_exposures = exposures
21
+ @_exposures = {
22
+ _resource_builder: JSONAPI::Serializable::ResourceBuilder.new
23
+ }.merge(exposures)
24
+ @_exposures.each { |k, v| instance_variable_set("@#{k}", v) }
25
+
26
+ @_id = instance_eval(&self.class.id_block).to_s
27
+ @_type = if (b = self.class.type_block)
28
+ instance_eval(&b).to_sym
29
+ else
30
+ self.class.type_val || :unknown
31
+ end
32
+ @_relationships = self.class.relationship_blocks
33
+ .each_with_object({}) do |(k, v), h|
34
+ opts = self.class.relationship_options[k] || {}
35
+ h[k] = Relationship.new(@_exposures, opts, &v)
36
+ end
37
+ @_meta = if (b = self.class.meta_block)
38
+ instance_eval(&b)
39
+ else
40
+ self.class.meta_val
41
+ end
42
+
43
+ freeze
27
44
  end
45
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
28
46
 
29
- def as_jsonapi(*)
30
- {}
47
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
48
+ def as_jsonapi(fields: nil, include: [])
49
+ attrs = requested_attributes(fields).each_with_object({}) do |(k, v), h|
50
+ h[k] = instance_eval(&v)
51
+ end
52
+ rels = requested_relationships(fields)
53
+ .each_with_object({}) do |(k, v), h|
54
+ h[k] = v.as_jsonapi(include.include?(k))
55
+ end
56
+ links = link_blocks.each_with_object({}) do |(k, v), h|
57
+ h[k] = Link.as_jsonapi(@_exposures, &v)
58
+ end
59
+ {}.tap do |hash|
60
+ hash[:id] = @_id
61
+ hash[:type] = @_type
62
+ hash[:attributes] = attrs if attrs.any?
63
+ hash[:relationships] = rels if rels.any?
64
+ hash[:links] = links if links.any?
65
+ hash[:meta] = @_meta unless @_meta.nil?
66
+ end
31
67
  end
68
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
32
69
 
33
70
  def jsonapi_type
34
71
  @_type
@@ -43,6 +80,24 @@ module JSONAPI
43
80
  .select { |k, _| include.include?(k) }
44
81
  .each_with_object({}) { |(k, v), h| h[k] = v.related_resources }
45
82
  end
83
+
84
+ private
85
+
86
+ # @api private
87
+ def requested_attributes(fields)
88
+ self.class.attribute_blocks
89
+ .select { |k, _| fields.nil? || fields.include?(k) }
90
+ end
91
+
92
+ # @api private
93
+ def requested_relationships(fields)
94
+ @_relationships.select { |k, _| fields.nil? || fields.include?(k) }
95
+ end
96
+
97
+ # @api private
98
+ def link_blocks
99
+ self.class.link_blocks
100
+ end
46
101
  end
47
102
  end
48
103
  end
@@ -5,7 +5,7 @@ module JSONAPI
5
5
  #
6
6
  # @usage
7
7
  # class SerializableUser < JSONAPI::Serializable::Resource
8
- # prepend JSONAPI::Serializable::Resource::ConditionalFields
8
+ # extend JSONAPI::Serializable::Resource::ConditionalFields
9
9
  #
10
10
  # attribute :email, if: -> { @current_user.admin? }
11
11
  # has_many :friends, unless: -> { @object.private_profile? }
@@ -13,67 +13,107 @@ module JSONAPI
13
13
  #
14
14
  module ConditionalFields
15
15
  def self.prepended(klass)
16
+ warn <<-EOT
17
+ DERPRECATION WARNING (called from #{caller_locations(1...2).first}):
18
+ Prepending `#{name}' is deprecated and will be removed in future releases. Use `Object#extend' instead.
19
+ EOT
20
+
21
+ klass.extend self
22
+ end
23
+
24
+ def self.extended(klass)
16
25
  klass.class_eval do
17
- extend DSL
26
+ include InstanceMethods
18
27
  class << self
19
- attr_accessor :condition_blocks
28
+ attr_accessor :field_condition_blocks
29
+ attr_accessor :link_condition_blocks
20
30
  end
21
- self.condition_blocks ||= {}
31
+ self.field_condition_blocks ||= {}
32
+ self.link_condition_blocks ||= {}
22
33
  end
23
34
  end
24
35
 
25
- # DSL extensions for conditional fields.
26
- module DSL
27
- def inherited(klass)
28
- super
29
- klass.condition_blocks = condition_blocks.dup
30
- end
36
+ def inherited(klass)
37
+ super
38
+ klass.field_condition_blocks = field_condition_blocks.dup
39
+ klass.link_condition_blocks = link_condition_blocks.dup
40
+ end
31
41
 
32
- # Handle the `if` and `unless` options for attributes.
33
- #
34
- # @example
35
- # attribute :email, if: -> { @current_user.admin? }
36
- #
37
- def attribute(name, options = {}, &block)
38
- super
39
- _register_condition(name, options)
40
- end
42
+ # Handle the `if` and `unless` options for attributes.
43
+ #
44
+ # @example
45
+ # attribute :email, if: -> { @current_user.admin? }
46
+ #
47
+ def attribute(name, options = {}, &block)
48
+ super
49
+ _register_condition(field_condition_blocks, name, options)
50
+ end
41
51
 
42
- # Handle the `if` and `unless` options for relationships (has_one,
43
- # belongs_to, has_many).
44
- #
45
- # @example
46
- # has_many :friends, unless: -> { @object.private_profile? }
47
- #
48
- def relationship(name, options = {}, &block)
49
- super
50
- _register_condition(name, options)
51
- end
52
+ # Handle the `if` and `unless` options for relationships (has_one,
53
+ # belongs_to, has_many).
54
+ #
55
+ # @example
56
+ # has_many :friends, unless: -> { @object.private_profile? }
57
+ #
58
+ def relationship(name, options = {}, &block)
59
+ super
60
+ _register_condition(field_condition_blocks, name, options)
61
+ end
52
62
 
53
- # @api private
54
- def _register_condition(name, options)
55
- condition_blocks[name.to_sym] =
56
- if options.key?(:if)
57
- options[:if]
58
- elsif options.key?(:unless)
59
- proc { !instance_exec(&options[:unless]) }
60
- end
61
- end
63
+ # Handle the `if` and `unless` options for links.
64
+ #
65
+ # @example
66
+ #
67
+ # link :self, if: -> { @object.render_self_link? } do
68
+ # "..."
69
+ # end
70
+ def link(name, options = {}, &block)
71
+ super(name, &block)
72
+ _register_condition(link_condition_blocks, name, options)
73
+ end
74
+
75
+ # NOTE(beauby): Re-aliasing those is necessary for the
76
+ # overridden `#relationship` method to be called.
77
+ alias has_many relationship
78
+ alias has_one relationship
79
+ alias belongs_to relationship
80
+
81
+ # @api private
82
+ def _register_condition(condition_blocks, name, options)
83
+ condition_blocks[name.to_sym] =
84
+ if options.key?(:if)
85
+ options[:if]
86
+ elsif options.key?(:unless)
87
+ proc { !instance_exec(&options[:unless]) }
88
+ end
62
89
  end
90
+ end
63
91
 
92
+ module InstanceMethods
64
93
  # @api private
65
94
  def requested_attributes(fields)
66
- super.select { |k, _| _conditionally_included?(k) }
95
+ super.select do |k, _|
96
+ _conditionally_included?(self.class.field_condition_blocks, k)
97
+ end
67
98
  end
68
99
 
69
100
  # @api private
70
101
  def requested_relationships(fields)
71
- super.select { |k, _| _conditionally_included?(k) }
102
+ super.select do |k, _|
103
+ _conditionally_included?(self.class.field_condition_blocks, k)
104
+ end
105
+ end
106
+
107
+ # @api private
108
+ def link_blocks
109
+ super.select do |k, _|
110
+ _conditionally_included?(self.class.link_condition_blocks, k)
111
+ end
72
112
  end
73
113
 
74
114
  # @api private
75
- def _conditionally_included?(field)
76
- condition = self.class.condition_blocks[field]
115
+ def _conditionally_included?(condition_blocks, field)
116
+ condition = condition_blocks[field]
77
117
  condition.nil? || instance_exec(&condition)
78
118
  end
79
119
  end