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.
@@ -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