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.
@@ -0,0 +1,173 @@
1
+ module JSONAPI
2
+ module Serializable
3
+ class Resource
4
+ module DSL
5
+ def self.extended(klass)
6
+ class << klass
7
+ attr_accessor :id_block, :type_val, :type_block, :attribute_blocks,
8
+ :relationship_blocks, :relationship_options,
9
+ :link_blocks, :meta_val, :meta_block
10
+ end
11
+
12
+ klass.attribute_blocks = {}
13
+ klass.relationship_blocks = {}
14
+ klass.relationship_options = {}
15
+ klass.link_blocks = {}
16
+ end
17
+
18
+ # rubocop:disable Metrics/AbcSize
19
+ def inherited(klass)
20
+ klass.id_block = id_block
21
+ klass.type_val = type_val
22
+ klass.type_block = type_block
23
+ klass.attribute_blocks = attribute_blocks.dup
24
+ klass.relationship_blocks = relationship_blocks.dup
25
+ klass.relationship_options = relationship_options.dup
26
+ klass.link_blocks = link_blocks.dup
27
+ klass.meta_val = meta_val
28
+ klass.meta_block = meta_block
29
+ end
30
+ # rubocop:enable Metrics/AbcSize
31
+
32
+ # Declare the JSON API id of this resource.
33
+ #
34
+ # @yieldreturn [String] The id of the resource.
35
+ #
36
+ # @example
37
+ # id { @user.id.to_s }
38
+ def id(&block)
39
+ self.id_block = block
40
+ end
41
+
42
+ # @overload type(value)
43
+ # Declare the JSON API type of this resource.
44
+ # @param [String] value The value of the type.
45
+ #
46
+ # @example
47
+ # type 'users'
48
+ #
49
+ # @overload type(&block)
50
+ # Declare the JSON API type of this resource.
51
+ # @yieldreturn [String] The value of the type.
52
+ #
53
+ # @example
54
+ # type { @object.type }
55
+ def type(value = nil, &block)
56
+ self.type_val = value.to_sym if value
57
+ self.type_block = block
58
+ end
59
+
60
+ # Declare an attribute for this resource.
61
+ #
62
+ # @param [Symbol] name The key of the attribute.
63
+ # @yieldreturn [Hash, String, nil] The block to compute the value.
64
+ #
65
+ # @example
66
+ # attribute(:name) { @object.name }
67
+ def attribute(name, _options = {}, &block)
68
+ block ||= proc { @object.public_send(name) }
69
+ attribute_blocks[name.to_sym] = block
70
+ end
71
+
72
+ # Declare a list of attributes for this resource.
73
+ #
74
+ # @param [Array] *args The attributes keys.
75
+ #
76
+ # @example
77
+ # attributes :title, :body, :date
78
+ def attributes(*args)
79
+ args.each do |attr|
80
+ attribute(attr)
81
+ end
82
+ end
83
+
84
+ # Declare a link for this resource. The properties of the link are set
85
+ # by providing a block in which the DSL methods of
86
+ # +JSONAPI::Serializable::Link+ are called, or the value of the link
87
+ # is returned directly.
88
+ # @see JSONAPI::Serialiable::Link
89
+ #
90
+ # @param [Symbol] name The key of the link.
91
+ # @yieldreturn [Hash, String, nil] The block to compute the value, if
92
+ # any.
93
+ #
94
+ # @example
95
+ # link(:self) do
96
+ # "http://api.example.com/users/#{@user.id}"
97
+ # end
98
+ #
99
+ # @example
100
+ # link(:self) do
101
+ # href "http://api.example.com/users/#{@user.id}"
102
+ # meta is_self: true
103
+ # end
104
+ def link(name, &block)
105
+ link_blocks[name] = block
106
+ end
107
+
108
+ # @overload meta(value)
109
+ # Declare the meta information for this resource.
110
+ # @param [Hash] value The meta information hash.
111
+ #
112
+ # @example
113
+ # meta key: value
114
+ #
115
+ # @overload meta(&block)
116
+ # Declare the meta information for this resource.
117
+ # @yieldreturn [String] The meta information hash.
118
+ # @example
119
+ # meta do
120
+ # { key: value }
121
+ # end
122
+ def meta(value = nil, &block)
123
+ self.meta_val = value
124
+ self.meta_block = block
125
+ end
126
+
127
+ # Declare a relationship for this resource. The properties of the
128
+ # relationship are set by providing a block in which the DSL methods
129
+ # of +JSONAPI::Serializable::Relationship+ are called.
130
+ # @see JSONAPI::Serializable::Relationship
131
+ #
132
+ # @param [Symbol] name The key of the relationship.
133
+ # @param [Hash] options The options for the relationship.
134
+ #
135
+ # @example
136
+ # relationship :posts do
137
+ # resources { @user.posts.map { |p| PostResource.new(post: p) } }
138
+ # end
139
+ #
140
+ # @example
141
+ # relationship :author do
142
+ # resources do
143
+ # @post.author && UserResource.new(user: @post.author)
144
+ # end
145
+ # data do
146
+ # { type: 'users', id: @post.author_id }
147
+ # end
148
+ # link(:self) do
149
+ # "http://api.example.com/posts/#{@post.id}/relationships/author"
150
+ # end
151
+ # link(:related) do
152
+ # "http://api.example.com/posts/#{@post.id}/author"
153
+ # end
154
+ # meta do
155
+ # { author_online: @post.author.online? }
156
+ # end
157
+ # end
158
+ def relationship(name, options = {}, &block)
159
+ rel_block = proc do
160
+ data { @object.public_send(name) }
161
+ instance_eval(&block) unless block.nil?
162
+ end
163
+ relationship_blocks[name.to_sym] = rel_block
164
+ relationship_options[name.to_sym] = options
165
+ end
166
+
167
+ alias has_many relationship
168
+ alias has_one relationship
169
+ alias belongs_to relationship
170
+ end
171
+ end
172
+ end
173
+ end
@@ -4,10 +4,10 @@ module JSONAPI
4
4
  # Extension for handling automatic key formatting of
5
5
  # attributes/relationships.
6
6
  #
7
- # @usage
7
+ # @example
8
8
  # class SerializableUser < JSONAPI::Serializable::Resource
9
- # prepend JSONAPI::Serializable::Resource::KeyFormat
10
- # self.key_format = proc { |key| key.camelize }
9
+ # extend JSONAPI::Serializable::Resource::KeyFormat
10
+ # key_format -> (key) { key.camelize }
11
11
  #
12
12
  # attribute :user_name
13
13
  # has_many :close_friends
@@ -15,42 +15,60 @@ module JSONAPI
15
15
  # # => will modify the serialized keys to `UserName` and `CloseFriends`.
16
16
  module KeyFormat
17
17
  def self.prepended(klass)
18
+ warn <<-EOT
19
+ DERPRECATION WARNING (called from #{caller_locations(1...2).first}):
20
+ Prepending `#{name}' is deprecated and will be removed in future releases. Use `Object#extend' instead.
21
+ EOT
22
+
23
+ klass.extend self
24
+ end
25
+
26
+ def self.extended(klass)
18
27
  klass.class_eval do
19
- extend DSL
20
28
  class << self
21
- attr_accessor :key_format
29
+ attr_accessor :_key_formatter
22
30
  end
23
31
  end
24
32
  end
25
33
 
26
- # DSL extensions for automatic key formatting.
27
- module DSL
28
- def inherited(klass)
29
- super
30
- klass.key_format = key_format
31
- end
34
+ def inherited(klass)
35
+ super
36
+ klass._key_formatter = _key_formatter
37
+ end
32
38
 
33
- # Handles automatic key formatting for attributes.
34
- def attribute(name, options = {}, &block)
35
- block ||= proc { @object.public_send(name) }
36
- super(key_format.call(name), options, &block)
37
- end
39
+ # Set the callable responsible for formatting keys, either directly, or
40
+ # via a block.
41
+ #
42
+ # @example
43
+ # key_format -> (key) { key.capitalize }
44
+ #
45
+ # @example
46
+ # key_format { |key| key.capitalize }
47
+ #
48
+ def key_format(callable = nil, &block)
49
+ self._key_formatter = callable || block
50
+ end
38
51
 
39
- # Handles automatic key formatting for relationships.
40
- def relationship(name, options = {}, &block)
41
- rel_block = proc do
42
- data(options[:class]) { @object.public_send(name) }
43
- instance_eval(&block) unless block.nil?
44
- end
45
- super(key_format.call(name), options, &rel_block)
46
- end
52
+ # Handles automatic key formatting for attributes.
53
+ def attribute(name, options = {}, &block)
54
+ block ||= proc { @object.public_send(name) }
55
+ super(_key_formatter.call(name), options, &block)
56
+ end
47
57
 
48
- # NOTE(beauby): Re-aliasing those is necessary for the
49
- # overridden `#relationship` method to be called.
50
- alias has_many relationship
51
- alias has_one relationship
52
- alias belongs_to relationship
58
+ # Handles automatic key formatting for relationships.
59
+ def relationship(name, options = {}, &block)
60
+ rel_block = proc do
61
+ data { @object.public_send(name) }
62
+ instance_eval(&block) unless block.nil?
63
+ end
64
+ super(_key_formatter.call(name), options, &rel_block)
53
65
  end
66
+
67
+ # NOTE(beauby): Re-aliasing those is necessary for the
68
+ # overridden `#relationship` method to be called.
69
+ alias has_many relationship
70
+ alias has_one relationship
71
+ alias belongs_to relationship
54
72
  end
55
73
  end
56
74
  end
@@ -0,0 +1,62 @@
1
+ require 'jsonapi/serializable/link'
2
+ require 'jsonapi/serializable/resource/relationship/dsl'
3
+
4
+ module JSONAPI
5
+ module Serializable
6
+ class Resource
7
+ class Relationship
8
+ include DSL
9
+
10
+ def initialize(exposures = {}, options = {}, &block)
11
+ exposures.each { |k, v| instance_variable_set("@#{k}", v) }
12
+ @_exposures = exposures
13
+ @_options = options
14
+ @_links = {}
15
+ instance_eval(&block)
16
+ end
17
+
18
+ def as_jsonapi(included)
19
+ {}.tap do |hash|
20
+ hash[:links] = @_links if @_links.any?
21
+ hash[:data] = linkage_data if included || @_include_linkage
22
+ hash[:meta] = @_meta unless @_meta.nil?
23
+ hash[:meta] = { included: false } if hash.empty?
24
+ end
25
+ end
26
+
27
+ # @api private
28
+ def related_resources
29
+ @_related_resources ||= Array(resources)
30
+ end
31
+
32
+ # @api private
33
+ def links
34
+ @_links
35
+ end
36
+
37
+ # @api private
38
+ def meta
39
+ @_meta
40
+ end
41
+
42
+ # @api private
43
+ def resources
44
+ @_resources ||= @_resources_block.call
45
+ end
46
+
47
+ private
48
+
49
+ # @api private
50
+ def linkage_data
51
+ return @_linkage_block.call if @_linkage_block
52
+
53
+ linkage_data = related_resources.map do |res|
54
+ { type: res.jsonapi_type, id: res.jsonapi_id }
55
+ end
56
+
57
+ resources.respond_to?(:each) ? linkage_data : linkage_data.first
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,103 @@
1
+ require 'jsonapi/serializable/resource_builder'
2
+
3
+ module JSONAPI
4
+ module Serializable
5
+ class Resource
6
+ class Relationship
7
+ module DSL
8
+ # Declare the related resources for this relationship.
9
+ # @yieldreturn The related resources for this relationship.
10
+ # If it is nil, an object implementing the Serializable::Resource
11
+ # interface, an empty array, or an array of objects implementing the
12
+ # Serializable::Resource interface, then it is used as is.
13
+ # Otherwise an appropriate Serializable::Resource subclass is
14
+ # inferred from the object(s)' namespace/class, the `class`
15
+ # relationship option, and the @_resource_builder.
16
+ #
17
+ # @example
18
+ # data do
19
+ # @user.posts.map { |p| PostResource.new(post: p) }
20
+ # end
21
+ #
22
+ # @example
23
+ # data do
24
+ # @post.author && UserResource.new(user: @user.author)
25
+ # end
26
+ #
27
+ # @example
28
+ # data do
29
+ # @user.posts
30
+ # end
31
+ # end
32
+ def data
33
+ # NOTE(beauby): Lazify computation since it is only needed when
34
+ # the corresponding relationship is included.
35
+ @_resources_block = proc do
36
+ @_resource_builder.build(yield, @_exposures, @_options[:class])
37
+ end
38
+ end
39
+
40
+ # @overload linkage(options = {}, &block)
41
+ # Explicitly declare linkage data.
42
+ # @yieldreturn The resource linkage.
43
+ #
44
+ # @example
45
+ # linkage do
46
+ # @object.posts.map { |p| { id: p.id.to_s, type: 'posts' } }
47
+ # end
48
+ #
49
+ # @overload linkage(options = {})
50
+ # Forces standard linkage even if relationship not included.
51
+ #
52
+ # @example
53
+ # linkage always: true
54
+ def linkage(always: false, &block)
55
+ @_include_linkage = always
56
+ @_linkage_block = block
57
+ end
58
+
59
+ # @overload meta(value)
60
+ # Declare the meta information for this relationship.
61
+ # @param [Hash] value The meta information hash.
62
+ #
63
+ # @example
64
+ # meta paginated: true
65
+ #
66
+ # @overload meta(&block)
67
+ # Declare the meta information for this relationship.
68
+ # @yieldreturn [Hash] The meta information hash.
69
+ #
70
+ # @example
71
+ # meta do
72
+ # { paginated: true }
73
+ # end
74
+ def meta(value = nil)
75
+ @_meta = value || yield
76
+ end
77
+
78
+ # Declare a link for this relationship. The properties of the link are set
79
+ # by providing a block in which the DSL methods of
80
+ # +JSONAPI::Serializable::Link+ are called.
81
+ # @see JSONAPI::Serialiable::Link
82
+ #
83
+ # @param [Symbol] name The key of the link.
84
+ # @yieldreturn [Hash, String, nil] The block to compute the value, if any.
85
+ #
86
+ # @example
87
+ # link(:self) do
88
+ # "http://api.example.com/users/#{@user.id}/relationships/posts"
89
+ # end
90
+ #
91
+ # @example
92
+ # link(:related) do
93
+ # href "http://api.example.com/users/#{@user.id}/posts"
94
+ # meta authorization_needed: true
95
+ # end
96
+ def link(name, &block)
97
+ @_links[name] = Link.as_jsonapi(@_exposures, &block)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,74 +1,54 @@
1
1
  module JSONAPI
2
2
  module Serializable
3
3
  class ResourceBuilder
4
- DEFAULT_RESOURCE_INFERER = lambda do |object_klass_name|
5
- names = object_klass_name.split('::'.freeze)
6
- klass_name = names.pop
7
- namespace = names.join('::'.freeze)
8
-
9
- klass_name = [namespace, "Serializable#{klass_name}"]
10
- .reject(&:nil?)
11
- .reject(&:empty?)
12
- .join('::'.freeze)
4
+ # @api private
5
+ def initialize(inferrer = nil)
6
+ @inferrer = inferrer
7
+ @lookup_cache = {}
13
8
 
14
- Object.const_get(klass_name)
9
+ freeze
15
10
  end
16
11
 
17
- def self.build(objects, expose, klass)
12
+ # @api private
13
+ def build(objects, expose, klass = nil)
18
14
  return objects if objects.nil? ||
19
15
  Array(objects).first.respond_to?(:as_jsonapi)
20
16
 
21
17
  if objects.respond_to?(:to_ary)
22
- Array(objects).map { |obj| new(obj, expose, klass).resource }
18
+ Array(objects).map { |obj| _build(obj, expose, klass) }
23
19
  else
24
- new(objects, expose, klass).resource
20
+ _build(objects, expose, klass)
25
21
  end
26
22
  end
27
23
 
28
- attr_reader :resource
29
-
30
- def initialize(object, expose, klass)
31
- @object = object
32
- @expose = expose || {}
33
- @klass = klass
34
- @resource = serializable_class.new(serializable_params)
35
- freeze
36
- end
37
-
38
24
  private
39
25
 
40
26
  # @api private
41
- def serializable_params
42
- @expose.merge(object: @object)
27
+ def _build(object, expose, klass)
28
+ serializable_class(object.class.name, klass)
29
+ .new(expose.merge(object: object))
43
30
  end
44
31
 
45
32
  # @api private
46
- # rubocop:disable Metrics/MethodLength
47
- def serializable_class
48
- klass =
49
- if @klass.respond_to?(:call)
50
- @klass.call(@object.class.name)
51
- elsif @klass.is_a?(Hash)
52
- @klass[@object.class.name.to_sym]
53
- elsif @klass.nil?
54
- DEFAULT_RESOURCE_INFERER.call(@object.class.name)
55
- else
56
- @klass
57
- end
33
+ def serializable_class(object_class_name, klass)
34
+ klass = klass[object_class_name.to_sym] if klass.is_a?(Hash)
58
35
 
59
- reify_class(klass)
36
+ @lookup_cache[[object_class_name, klass.to_s]] ||=
37
+ reify_class(klass || @inferrer.call(object_class_name))
60
38
  end
61
- # rubocop:enable Metrics/MethodLength
62
39
 
63
40
  # @api private
64
41
  def reify_class(klass)
65
42
  if klass.is_a?(Class)
66
43
  klass
67
- elsif klass.is_a?(String)
68
- Object.const_get(klass)
44
+ elsif klass.is_a?(String) || klass.is_a?(Symbol)
45
+ begin
46
+ Object.const_get(klass)
47
+ rescue NameError
48
+ raise NameError, "Undefined serializable class #{klass}"
49
+ end
69
50
  else
70
- # TODO(beauby): Raise meaningful exception.
71
- raise
51
+ raise ArgumentError, "Invalid serializable class #{klass}"
72
52
  end
73
53
  end
74
54
  end