jsonapi-serializable 0.1.3 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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