halitosis 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +33 -19
- data/lib/halitosis/{properties → attributes}/field.rb +1 -1
- data/lib/halitosis/attributes.rb +52 -0
- data/lib/halitosis/base.rb +26 -30
- data/lib/halitosis/collection/field.rb +11 -0
- data/lib/halitosis/collection.rb +25 -27
- data/lib/halitosis/context.rb +54 -0
- data/lib/halitosis/errors.rb +13 -0
- data/lib/halitosis/field.rb +8 -23
- data/lib/halitosis/fields.rb +6 -0
- data/lib/halitosis/hash_util.rb +31 -12
- data/lib/halitosis/links/field.rb +1 -1
- data/lib/halitosis/links.rb +6 -6
- data/lib/halitosis/meta.rb +7 -7
- data/lib/halitosis/permissions.rb +7 -7
- data/lib/halitosis/railtie.rb +4 -0
- data/lib/halitosis/relationships/field.rb +2 -2
- data/lib/halitosis/relationships.rb +26 -16
- data/lib/halitosis/resource.rb +25 -11
- data/lib/halitosis/version.rb +1 -1
- data/lib/halitosis.rb +9 -3
- metadata +5 -4
- data/lib/halitosis/properties.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6dc2b6951bb551687107ecbf28dd84ef5d616b86a01f405bfd0578592e2ae572
|
4
|
+
data.tar.gz: a98c1b99cc9973958db53bd3ea5d6acd11de9b6f79fb9a5c9fcf07f97514de01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dfed5ec625a1764322fa5ddc3df08b3fe0ee2dca6bcbe21da0c92daa00a168518f2f31e69ac0c7834ef4dded8ba3c62e93ba9349f9e68d63e7403ce98131d96b
|
7
|
+
data.tar.gz: bb36dc686ccecdefc51500b9cf5d8142a72d4e460ad562ea020de9b93b57f00f9a2ff4ddac12351afe3060c2494408dbefa3735002672bb80e39d97bb88e2f34
|
data/README.md
CHANGED
@@ -21,7 +21,7 @@ Need something more standardized ([JSON:API](https://jsonapi.org/), or [HAL](htt
|
|
21
21
|
Add this line to your application's Gemfile:
|
22
22
|
|
23
23
|
```ruby
|
24
|
-
gem "
|
24
|
+
gem "halitosis"
|
25
25
|
```
|
26
26
|
|
27
27
|
And then execute:
|
@@ -33,7 +33,7 @@ $ bundle install
|
|
33
33
|
Or install it yourself as:
|
34
34
|
|
35
35
|
```bash
|
36
|
-
$ gem install
|
36
|
+
$ gem install halitosis
|
37
37
|
```
|
38
38
|
|
39
39
|
### Basic usage
|
@@ -51,7 +51,7 @@ class DuckSerializer
|
|
51
51
|
|
52
52
|
resource :duck
|
53
53
|
|
54
|
-
|
54
|
+
attribute :name
|
55
55
|
|
56
56
|
link :self do
|
57
57
|
"/ducks/#{duck.code}"
|
@@ -68,11 +68,13 @@ serializer = DuckSerializer.new(duck)
|
|
68
68
|
|
69
69
|
Then call `serializer.render`:
|
70
70
|
|
71
|
-
```
|
71
|
+
```ruby
|
72
72
|
{
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
duck: {
|
74
|
+
name: 'Ferdi',
|
75
|
+
_links: {
|
76
|
+
self: { href: '/ducks/ferdi' }
|
77
|
+
}
|
76
78
|
}
|
77
79
|
}
|
78
80
|
```
|
@@ -80,7 +82,7 @@ Then call `serializer.render`:
|
|
80
82
|
Or `serializer.to_json`:
|
81
83
|
|
82
84
|
```ruby
|
83
|
-
'{"name": "Ferdi", "_links": {"self": {"href": "/ducks/ferdi"}}}'
|
85
|
+
'{"duck": {"name": "Ferdi", "_links": {"self": {"href": "/ducks/ferdi"}}}}'
|
84
86
|
```
|
85
87
|
|
86
88
|
|
@@ -117,10 +119,10 @@ When a resource is declared, `#initialize` expects the resource as the first arg
|
|
117
119
|
serializer = DuckSerializer.new(Duck.new, ...)
|
118
120
|
```
|
119
121
|
|
120
|
-
This makes
|
122
|
+
This makes attribute definitions cleaner:
|
121
123
|
|
122
124
|
```ruby
|
123
|
-
|
125
|
+
attribute :name # now calls Duck#name by default
|
124
126
|
```
|
125
127
|
|
126
128
|
#### 3. Collection
|
@@ -139,39 +141,51 @@ end
|
|
139
141
|
|
140
142
|
The block should return an array of Halitosis instances in order to be rendered.
|
141
143
|
|
142
|
-
### Defining
|
144
|
+
### Defining attributes, links, relationships, meta, and permissions
|
143
145
|
|
144
|
-
|
146
|
+
Attributes can be defined in several ways:
|
145
147
|
|
146
148
|
```ruby
|
147
|
-
|
149
|
+
attribute(:quacks) { "#{duck.quacks} per minute" }
|
150
|
+
```
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
attribute :quacks # => Duck#quacks, if resource is declared
|
148
154
|
```
|
149
155
|
|
150
156
|
```ruby
|
151
|
-
|
157
|
+
attribute :quacks, value: "many"
|
152
158
|
```
|
153
159
|
|
154
160
|
```ruby
|
155
|
-
|
161
|
+
attribute :quacks do
|
156
162
|
duck.quacks.round
|
157
163
|
end
|
158
164
|
```
|
159
165
|
|
160
166
|
```ruby
|
161
|
-
|
167
|
+
attribute(:quacks) { calculate_quacks }
|
162
168
|
|
163
169
|
def calculate_quacks
|
164
170
|
...
|
165
171
|
end
|
166
172
|
```
|
167
173
|
|
174
|
+
Attributes can also be implemented using the legacy `property` alias:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
property(:quacks) { "#{duck.quacks} per minute" }
|
178
|
+
property :quacks # Duck#quacks
|
179
|
+
property :quacks, value: "many"
|
180
|
+
```
|
181
|
+
|
168
182
|
#### Conditionals
|
169
183
|
|
170
|
-
The inclusion of
|
184
|
+
The inclusion of attributes can be determined by conditionals using `if` and
|
171
185
|
`unless` options. For example, with a method name:
|
172
186
|
|
173
187
|
```ruby
|
174
|
-
|
188
|
+
attribute :quacks, if: :include_quacks?
|
175
189
|
|
176
190
|
def include_quacks?
|
177
191
|
duck.quacks < 10
|
@@ -180,7 +194,7 @@ end
|
|
180
194
|
|
181
195
|
With a proc:
|
182
196
|
```ruby
|
183
|
-
|
197
|
+
attribute :quacks, unless: proc { duck.quacks.nil? }, value: ...
|
184
198
|
```
|
185
199
|
|
186
200
|
For links and relationships:
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Halitosis
|
4
|
+
module Attributes
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
# Legacy alias for attribute
|
13
|
+
#
|
14
|
+
# @param name [Symbol, String]
|
15
|
+
# @param options [nil, Hash]
|
16
|
+
#
|
17
|
+
# @return [Halitosis::Attributes::Field]
|
18
|
+
def property(...)
|
19
|
+
attribute(...)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Rails-style attribute definition
|
23
|
+
#
|
24
|
+
# @param name [Symbol, String]
|
25
|
+
# @param options [nil, Hash]
|
26
|
+
#
|
27
|
+
# @return [Halitosis::Attributes::Field]
|
28
|
+
#
|
29
|
+
def attribute(name, options = {}, &procedure)
|
30
|
+
fields.add(Field.new(name, options, procedure))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module InstanceMethods
|
35
|
+
# @return [Hash] the rendered hash with attributes, if any
|
36
|
+
#
|
37
|
+
def render_with_context(context)
|
38
|
+
super.merge(attributes(context))
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Hash] attributes from fields
|
42
|
+
#
|
43
|
+
def attributes(context = build_context)
|
44
|
+
render_fields(Field, context) do |field, result|
|
45
|
+
result[field.name] = field.value(context)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
require "halitosis/attributes/field"
|
data/lib/halitosis/base.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Halitosis
|
4
|
+
# Base module for all serializer classes.
|
5
|
+
#
|
6
|
+
# Include this module in your serializer class, and include any additional field-type modules
|
4
7
|
module Base
|
5
8
|
def self.included(base)
|
6
9
|
base.extend ClassMethods
|
7
10
|
|
8
11
|
base.send :include, InstanceMethods
|
9
|
-
base.send :include, Links
|
10
|
-
base.send :include, Meta
|
11
|
-
base.send :include, Permissions
|
12
|
-
base.send :include, Properties
|
13
|
-
base.send :include, Relationships
|
14
12
|
|
15
13
|
base.send :attr_reader, :options
|
14
|
+
|
15
|
+
base.class.send :attr_accessor, :resource_type
|
16
16
|
end
|
17
17
|
|
18
18
|
module ClassMethods
|
@@ -32,7 +32,7 @@ module Halitosis
|
|
32
32
|
# @return [Object] the serializer instance
|
33
33
|
#
|
34
34
|
def initialize(**options)
|
35
|
-
@options = Halitosis::HashUtil.
|
35
|
+
@options = Halitosis::HashUtil.symbolize_hash(options).freeze
|
36
36
|
end
|
37
37
|
|
38
38
|
# @return [Hash, Array] rendered JSON
|
@@ -49,20 +49,14 @@ module Halitosis
|
|
49
49
|
# @return [Hash] rendered representation
|
50
50
|
#
|
51
51
|
def render
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
# @return [nil, Object] the parent serializer, if this instance is an
|
56
|
-
# embedded child
|
57
|
-
#
|
58
|
-
def parent
|
59
|
-
@parent ||= options.fetch(:parent, nil)
|
52
|
+
render_with_context(build_context)
|
60
53
|
end
|
61
54
|
|
62
|
-
# @
|
55
|
+
# @param context [Halitosis::Context] the context instance
|
56
|
+
# @return [Hash] the rendered hash
|
63
57
|
#
|
64
|
-
def
|
65
|
-
|
58
|
+
def render_with_context(_context)
|
59
|
+
{}
|
66
60
|
end
|
67
61
|
|
68
62
|
def collection?
|
@@ -71,6 +65,13 @@ module Halitosis
|
|
71
65
|
|
72
66
|
protected
|
73
67
|
|
68
|
+
# Build a new context instance using this serializer instance
|
69
|
+
#
|
70
|
+
# @return [Halitosis::Context] the context instance
|
71
|
+
def build_context(options = {})
|
72
|
+
Context.new(self, HashUtil.deep_merge(@options, options))
|
73
|
+
end
|
74
|
+
|
74
75
|
# Allow included modules to decorate rendered hash
|
75
76
|
#
|
76
77
|
# @param key [Symbol] the key (e.g. `embedded`, `links`)
|
@@ -78,9 +79,9 @@ module Halitosis
|
|
78
79
|
#
|
79
80
|
# @return [Hash] the decorated hash
|
80
81
|
#
|
81
|
-
def decorate_render(key, result)
|
82
|
+
def decorate_render(key, context, result)
|
82
83
|
result.tap do
|
83
|
-
value = send(key)
|
84
|
+
value = send(key, context)
|
84
85
|
|
85
86
|
result[:"_#{key}"] = value if value.any?
|
86
87
|
end
|
@@ -93,11 +94,11 @@ module Halitosis
|
|
93
94
|
#
|
94
95
|
# @return [Hash] the result
|
95
96
|
#
|
96
|
-
def render_fields(type)
|
97
|
-
fields = self.class.fields.
|
97
|
+
def render_fields(type, context)
|
98
|
+
fields = self.class.fields.for_type(type)
|
98
99
|
|
99
100
|
fields.each_with_object({}) do |field, result|
|
100
|
-
next unless field.enabled?(
|
101
|
+
next unless field.enabled?(context)
|
101
102
|
|
102
103
|
yield field, result
|
103
104
|
end
|
@@ -108,15 +109,10 @@ module Halitosis
|
|
108
109
|
#
|
109
110
|
# @return [nil, Hash] the rendered child
|
110
111
|
#
|
111
|
-
def render_child(child, opts)
|
112
|
-
return unless child.class.included_modules.include?(Halitosis)
|
113
|
-
|
114
|
-
child.options[:include] ||= {}
|
115
|
-
child.options[:include] = child.options[:include].merge(opts)
|
116
|
-
|
117
|
-
child.options[:parent] = self
|
112
|
+
def render_child(child, context, opts)
|
113
|
+
return unless child.class.included_modules.include?(Halitosis::Base)
|
118
114
|
|
119
|
-
child.
|
115
|
+
child.render_with_context child.build_context(parent: context, include: opts)
|
120
116
|
end
|
121
117
|
end
|
122
118
|
end
|
@@ -3,6 +3,17 @@
|
|
3
3
|
module Halitosis
|
4
4
|
module Collection
|
5
5
|
class Field < Halitosis::Field
|
6
|
+
# @return [true] if nothing is raised
|
7
|
+
#
|
8
|
+
# @raise [Halitosis::InvalidField] if the definition is invalid
|
9
|
+
#
|
10
|
+
def validate
|
11
|
+
super
|
12
|
+
|
13
|
+
return true if procedure
|
14
|
+
|
15
|
+
raise InvalidField, "Collection #{name} must be defined with a proc"
|
16
|
+
end
|
6
17
|
end
|
7
18
|
end
|
8
19
|
end
|
data/lib/halitosis/collection.rb
CHANGED
@@ -15,8 +15,6 @@ module Halitosis
|
|
15
15
|
base.send :include, InstanceMethods
|
16
16
|
|
17
17
|
base.send :attr_reader, :collection
|
18
|
-
|
19
|
-
base.class.send :attr_accessor, :collection_name
|
20
18
|
end
|
21
19
|
|
22
20
|
module ClassMethods
|
@@ -25,9 +23,9 @@ module Halitosis
|
|
25
23
|
# @return [Module] self
|
26
24
|
#
|
27
25
|
def define_collection(name, options = {}, &procedure)
|
28
|
-
raise InvalidCollection, "#{self.name} collection is already defined" if fields.
|
26
|
+
raise InvalidCollection, "#{self.name || Collection.name} collection is already defined" if fields.for_type(Field).any?
|
29
27
|
|
30
|
-
self.
|
28
|
+
self.resource_type = name.to_s
|
31
29
|
|
32
30
|
alias_method name, :collection
|
33
31
|
|
@@ -39,7 +37,7 @@ module Halitosis
|
|
39
37
|
end
|
40
38
|
|
41
39
|
def collection_field
|
42
|
-
fields
|
40
|
+
fields.for_type(Field).last || raise(InvalidCollection, "#{name || Collection.name} collection is not defined")
|
43
41
|
end
|
44
42
|
end
|
45
43
|
|
@@ -50,47 +48,47 @@ module Halitosis
|
|
50
48
|
#
|
51
49
|
def initialize(collection, **)
|
52
50
|
@collection = collection
|
51
|
+
@collection_field = self.class.collection_field
|
53
52
|
|
54
53
|
super(**)
|
55
54
|
end
|
56
55
|
|
57
|
-
# @return [Hash] the rendered hash with collection,
|
56
|
+
# @return [Hash, Array] the rendered hash with collection, as an array or a hash under a key
|
58
57
|
#
|
59
|
-
def
|
60
|
-
|
61
|
-
|
62
|
-
|
58
|
+
def render_with_context(context)
|
59
|
+
if (include_root = context.fetch(:include_root) { context.depth.zero? })
|
60
|
+
{
|
61
|
+
root_name(include_root) => render_collection_field(context)
|
62
|
+
}.merge(super)
|
63
63
|
else
|
64
|
-
render_collection_field(
|
64
|
+
render_collection_field(context)
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
# @return [Hash] collection from fields
|
69
|
-
#
|
70
|
-
def render_collection_field(field)
|
71
|
-
value = instance_eval(&field.procedure)
|
72
|
-
value.map { |child| render_child(child, collection_opts) }
|
73
|
-
end
|
74
|
-
|
75
68
|
def collection?
|
76
69
|
true
|
77
70
|
end
|
78
71
|
|
79
72
|
private
|
80
73
|
|
81
|
-
|
82
|
-
|
83
|
-
# @return [Hash]
|
74
|
+
attr_reader :collection_field
|
75
|
+
|
76
|
+
# @return [Hash] collection from fields
|
84
77
|
#
|
85
|
-
def
|
86
|
-
|
78
|
+
def render_collection_field(context)
|
79
|
+
value = collection_field.value(context)
|
87
80
|
|
88
|
-
|
81
|
+
return render_child(value, context, context.include_options) if value.is_a?(Halitosis::Collection)
|
82
|
+
|
83
|
+
value.reject { |child| child.is_a?(Halitosis::Collection) } # Skip nested collections in array
|
84
|
+
.map { |child| render_child(child, context, context.include_options) }
|
85
|
+
.compact
|
86
|
+
end
|
89
87
|
|
90
|
-
|
91
|
-
|
88
|
+
def root_name(include_root)
|
89
|
+
return include_root.to_sym if include_root.is_a?(String) || include_root.is_a?(Symbol)
|
92
90
|
|
93
|
-
|
91
|
+
self.class.resource_type.to_sym
|
94
92
|
end
|
95
93
|
end
|
96
94
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Halitosis
|
2
|
+
class Context
|
3
|
+
# @param instance [Halitosis::Base] the serializer instance
|
4
|
+
# @param options [Hash] hash of options
|
5
|
+
def initialize(instance, options = {})
|
6
|
+
@instance = instance
|
7
|
+
@options = HashUtil.symbolize_hash(options).freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
### Instance ###
|
11
|
+
|
12
|
+
# Evaluate guard procedure or method on the serializer instance
|
13
|
+
#
|
14
|
+
def call_instance(guard)
|
15
|
+
case guard
|
16
|
+
when Proc
|
17
|
+
instance.instance_exec(self, &guard)
|
18
|
+
when Symbol, String
|
19
|
+
instance.send(guard)
|
20
|
+
else
|
21
|
+
guard
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
### Options ###
|
26
|
+
|
27
|
+
def fetch(...)
|
28
|
+
options.fetch(...)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Hash] hash of options with top level string keys
|
32
|
+
#
|
33
|
+
def include_options
|
34
|
+
@include_options ||= HashUtil.hasherize_include_option(options[:include] || {})
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [nil, Halitosis::Context] the parent context, if this instance is an
|
38
|
+
# embedded child
|
39
|
+
#
|
40
|
+
def parent
|
41
|
+
options.fetch(:parent, nil)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Integer] the depth at which this serializer is embedded
|
45
|
+
#
|
46
|
+
def depth
|
47
|
+
@depth ||= parent ? parent.depth + 1 : 0
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :instance, :options
|
53
|
+
end
|
54
|
+
end
|
data/lib/halitosis/errors.rb
CHANGED
@@ -3,9 +3,22 @@
|
|
3
3
|
module Halitosis
|
4
4
|
class Error < StandardError; end
|
5
5
|
|
6
|
+
### Configuration Errors ###
|
7
|
+
|
6
8
|
class InvalidCollection < StandardError; end
|
7
9
|
|
8
10
|
class InvalidField < StandardError; end
|
9
11
|
|
10
12
|
class InvalidResource < StandardError; end
|
13
|
+
|
14
|
+
### Rendering Errors ###
|
15
|
+
|
16
|
+
class InvalidQueryParameter < Error
|
17
|
+
def initialize(message, parameter)
|
18
|
+
@parameter = parameter
|
19
|
+
super(message)
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :parameter
|
23
|
+
end
|
11
24
|
end
|
data/lib/halitosis/field.rb
CHANGED
@@ -7,8 +7,6 @@ module Halitosis
|
|
7
7
|
class Field
|
8
8
|
attr_reader :name, :options
|
9
9
|
|
10
|
-
attr_accessor :procedure
|
11
|
-
|
12
10
|
# Construct a new Field instance
|
13
11
|
#
|
14
12
|
# @param name [Symbol, String] Field name
|
@@ -18,27 +16,25 @@ module Halitosis
|
|
18
16
|
#
|
19
17
|
def initialize(name, options, procedure)
|
20
18
|
@name = name.to_sym
|
21
|
-
@options = Halitosis::HashUtil.
|
19
|
+
@options = Halitosis::HashUtil.symbolize_hash(options)
|
22
20
|
@procedure = procedure
|
23
21
|
end
|
24
22
|
|
25
|
-
# @param
|
23
|
+
# @param context [Halitosis::Context] the serializer instance with which to evaluate
|
26
24
|
# the stored procedure
|
27
25
|
#
|
28
|
-
def value(
|
29
|
-
options.fetch(:value)
|
30
|
-
procedure ? instance.instance_eval(&procedure) : instance.send(name)
|
31
|
-
end
|
26
|
+
def value(context)
|
27
|
+
options.fetch(:value) { context.call_instance(procedure || name) }
|
32
28
|
end
|
33
29
|
|
34
30
|
# @return [true, false] whether this Field should be included based on
|
35
31
|
# its conditional guard, if any
|
36
32
|
#
|
37
|
-
def enabled?(
|
33
|
+
def enabled?(context)
|
38
34
|
if options.key?(:if)
|
39
|
-
!!
|
35
|
+
!!context.call_instance(options.fetch(:if))
|
40
36
|
elsif options.key?(:unless)
|
41
|
-
!
|
37
|
+
!context.call_instance(options.fetch(:unless))
|
42
38
|
else
|
43
39
|
true
|
44
40
|
end
|
@@ -57,17 +53,6 @@ module Halitosis
|
|
57
53
|
|
58
54
|
private
|
59
55
|
|
60
|
-
|
61
|
-
#
|
62
|
-
def eval_guard(instance, guard)
|
63
|
-
case guard
|
64
|
-
when Proc
|
65
|
-
instance.instance_eval(&guard)
|
66
|
-
when Symbol, String
|
67
|
-
instance.send(guard)
|
68
|
-
else
|
69
|
-
guard
|
70
|
-
end
|
71
|
-
end
|
56
|
+
attr_reader :procedure
|
72
57
|
end
|
73
58
|
end
|
data/lib/halitosis/fields.rb
CHANGED
data/lib/halitosis/hash_util.rb
CHANGED
@@ -4,23 +4,42 @@ module Halitosis
|
|
4
4
|
module HashUtil
|
5
5
|
module_function
|
6
6
|
|
7
|
-
# Transform
|
7
|
+
# Transform include params into a hash
|
8
8
|
#
|
9
|
-
# @param
|
9
|
+
# @param object [Hash, Array, String]
|
10
10
|
#
|
11
11
|
# @return [Hash]
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
def hasherize_include_option(object)
|
13
|
+
case object
|
14
|
+
when Hash
|
15
|
+
object.transform_keys(&:to_s)
|
16
|
+
when String, Symbol
|
17
|
+
object.to_s.split(",").inject({}) do |output, key|
|
17
18
|
f, value = key.split(".", 2)
|
18
|
-
output
|
19
|
+
deep_merge(output, f => value ? hasherize_include_option(value) : {})
|
19
20
|
end
|
20
21
|
when Array
|
21
|
-
|
22
|
+
object.inject({}) do |output, value|
|
23
|
+
deep_merge(output, hasherize_include_option(value))
|
24
|
+
end
|
22
25
|
else
|
23
|
-
|
26
|
+
object
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Deep merge two hashes
|
31
|
+
#
|
32
|
+
# @param hash [Hash]
|
33
|
+
# @param other_hash [Hash]
|
34
|
+
#
|
35
|
+
# @return [Hash]
|
36
|
+
def deep_merge(hash, other_hash)
|
37
|
+
hash.merge(other_hash) do |key, this_val, other_val|
|
38
|
+
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
|
39
|
+
deep_merge(this_val, other_val)
|
40
|
+
else
|
41
|
+
other_val
|
42
|
+
end
|
24
43
|
end
|
25
44
|
end
|
26
45
|
|
@@ -30,9 +49,9 @@ module Halitosis
|
|
30
49
|
#
|
31
50
|
# @return [Hash]
|
32
51
|
#
|
33
|
-
def
|
52
|
+
def symbolize_hash(hash)
|
34
53
|
if hash.respond_to?(:transform_keys)
|
35
|
-
hash.transform_keys(&:to_sym).transform_values(&method(:
|
54
|
+
hash.transform_keys(&:to_sym).transform_values(&method(:symbolize_hash))
|
36
55
|
else
|
37
56
|
hash
|
38
57
|
end
|
data/lib/halitosis/links.rb
CHANGED
@@ -19,9 +19,9 @@ module Halitosis
|
|
19
19
|
module InstanceMethods
|
20
20
|
# @return [Hash] the rendered hash with links, if any
|
21
21
|
#
|
22
|
-
def
|
23
|
-
if
|
24
|
-
decorate_render :links, super
|
22
|
+
def render_with_context(context)
|
23
|
+
if context.fetch(:include_links, true)
|
24
|
+
decorate_render :links, context, super
|
25
25
|
else
|
26
26
|
super
|
27
27
|
end
|
@@ -29,9 +29,9 @@ module Halitosis
|
|
29
29
|
|
30
30
|
# @return [Hash] links from fields
|
31
31
|
#
|
32
|
-
def links
|
33
|
-
render_fields(Field
|
34
|
-
value = field.value(
|
32
|
+
def links(context = build_context)
|
33
|
+
render_fields(Field, context) do |field, result|
|
34
|
+
value = field.value(context)
|
35
35
|
|
36
36
|
result[field.name] = value if value
|
37
37
|
end
|
data/lib/halitosis/meta.rb
CHANGED
@@ -19,9 +19,9 @@ module Halitosis
|
|
19
19
|
module InstanceMethods
|
20
20
|
# @return [Hash] the rendered hash with meta, if any
|
21
21
|
#
|
22
|
-
def
|
23
|
-
if
|
24
|
-
decorate_render :meta, super
|
22
|
+
def render_with_context(context)
|
23
|
+
if context.fetch(:include_meta, true)
|
24
|
+
decorate_render :meta, context, super
|
25
25
|
else
|
26
26
|
super
|
27
27
|
end
|
@@ -29,11 +29,11 @@ module Halitosis
|
|
29
29
|
|
30
30
|
# @return [Hash] meta from fields
|
31
31
|
#
|
32
|
-
def meta
|
33
|
-
render_fields(Field
|
34
|
-
value = field.value(
|
32
|
+
def meta(context = build_context)
|
33
|
+
render_fields(Field, context) do |field, result|
|
34
|
+
value = field.value(context)
|
35
35
|
|
36
|
-
result[field.name] = value
|
36
|
+
result[field.name] = value
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
@@ -19,9 +19,9 @@ module Halitosis
|
|
19
19
|
module InstanceMethods
|
20
20
|
# @return [Hash] the rendered hash with permissions, if any
|
21
21
|
#
|
22
|
-
def
|
23
|
-
if
|
24
|
-
decorate_render :permissions, super
|
22
|
+
def render_with_context(context)
|
23
|
+
if context.fetch(:include_permissions, true)
|
24
|
+
decorate_render :permissions, context, super
|
25
25
|
else
|
26
26
|
super
|
27
27
|
end
|
@@ -29,11 +29,11 @@ module Halitosis
|
|
29
29
|
|
30
30
|
# @return [Hash] permissions from fields
|
31
31
|
#
|
32
|
-
def permissions
|
33
|
-
render_fields(Field
|
34
|
-
value = field.value(
|
32
|
+
def permissions(context = build_context)
|
33
|
+
render_fields(Field, context) do |field, result|
|
34
|
+
value = field.value(context)
|
35
35
|
|
36
|
-
result[field.name] = value
|
36
|
+
result[field.name] = value || false
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
data/lib/halitosis/railtie.rb
CHANGED
@@ -16,6 +16,10 @@ module Halitosis
|
|
16
16
|
Halitosis.config.extensions << ::Rails.application.routes.url_helpers
|
17
17
|
end
|
18
18
|
|
19
|
+
initializer "halitosis.error_response" do |app|
|
20
|
+
app.config.action_dispatch.rescue_responses[InvalidQueryParameter.name] ||= :bad_request
|
21
|
+
end
|
22
|
+
|
19
23
|
initializer "halitosis.renderable" do |_app|
|
20
24
|
Halitosis.config.extensions << Renderable
|
21
25
|
end
|
@@ -21,10 +21,10 @@ module Halitosis
|
|
21
21
|
#
|
22
22
|
# @return [true, false]
|
23
23
|
#
|
24
|
-
def enabled?(
|
24
|
+
def enabled?(context)
|
25
25
|
return false unless super
|
26
26
|
|
27
|
-
opts =
|
27
|
+
opts = context.include_options
|
28
28
|
|
29
29
|
# Field name must appear in instance included option keys
|
30
30
|
return false unless opts.include?(name.to_s)
|
@@ -24,17 +24,20 @@ module Halitosis
|
|
24
24
|
module InstanceMethods
|
25
25
|
# @return [Hash] the rendered hash with relationships resources, if any
|
26
26
|
#
|
27
|
-
def
|
28
|
-
decorate_render :relationships, super
|
27
|
+
def render_with_context(context)
|
28
|
+
decorate_render :relationships, context, super
|
29
29
|
end
|
30
30
|
|
31
31
|
# @return [Hash] hash of rendered resources to include
|
32
32
|
#
|
33
|
-
def relationships
|
34
|
-
|
35
|
-
|
33
|
+
def relationships(context = build_context)
|
34
|
+
# Do not validation non-root collections (as they pass values directly to children)
|
35
|
+
validate_relationships!(context) unless collection?
|
36
36
|
|
37
|
-
|
37
|
+
render_fields(Field, context) do |field, result|
|
38
|
+
value = field.value(context)
|
39
|
+
|
40
|
+
child = relationships_child(field.name.to_s, context, value)
|
38
41
|
|
39
42
|
result[field.name] = child if child
|
40
43
|
end
|
@@ -43,15 +46,15 @@ module Halitosis
|
|
43
46
|
# @return [nil, Hash, Array<Hash>] either a single rendered child
|
44
47
|
# serializer or an array of them
|
45
48
|
#
|
46
|
-
def relationships_child(key, value)
|
49
|
+
def relationships_child(key, context, value)
|
47
50
|
return unless value
|
48
51
|
|
49
|
-
opts = child_relationship_opts(key)
|
52
|
+
opts = child_relationship_opts(key, context)
|
50
53
|
|
51
54
|
if value.is_a?(Array)
|
52
|
-
value.map { |item| render_child(item, opts) }.compact
|
55
|
+
value.map { |item| render_child(item, context, opts) }.compact
|
53
56
|
else
|
54
|
-
render_child(value, opts)
|
57
|
+
render_child(value, context, opts)
|
55
58
|
end
|
56
59
|
end
|
57
60
|
|
@@ -59,8 +62,8 @@ module Halitosis
|
|
59
62
|
#
|
60
63
|
# @return [Hash]
|
61
64
|
#
|
62
|
-
def child_relationship_opts(key)
|
63
|
-
opts = include_options.fetch(key, {})
|
65
|
+
def child_relationship_opts(key, context)
|
66
|
+
opts = context.include_options.fetch(key, {})
|
64
67
|
|
65
68
|
# Turn { :report => 1 } into { :report => {} } for child
|
66
69
|
opts = {} unless opts.is_a?(Hash)
|
@@ -68,10 +71,17 @@ module Halitosis
|
|
68
71
|
opts
|
69
72
|
end
|
70
73
|
|
71
|
-
|
72
|
-
|
73
|
-
def
|
74
|
-
|
74
|
+
private
|
75
|
+
|
76
|
+
def validate_relationships!(context)
|
77
|
+
opts = context.include_options.keys.map(&:to_s)
|
78
|
+
|
79
|
+
opts -= self.class.fields.for_type(Field).map { |field| field.name.to_s }
|
80
|
+
|
81
|
+
return if opts.none?
|
82
|
+
|
83
|
+
resource_label = [self.class.resource_type, "resource"].compact.join(" ")
|
84
|
+
raise Halitosis::InvalidQueryParameter.new("The #{resource_label} does not have a `#{opts.first}` relationship path.", "include")
|
75
85
|
end
|
76
86
|
end
|
77
87
|
end
|
data/lib/halitosis/resource.rb
CHANGED
@@ -14,8 +14,6 @@ module Halitosis
|
|
14
14
|
base.send :include, InstanceMethods
|
15
15
|
|
16
16
|
base.send :attr_reader, :resource
|
17
|
-
|
18
|
-
base.class.send :attr_accessor, :resource_name
|
19
17
|
end
|
20
18
|
|
21
19
|
module ClassMethods
|
@@ -24,22 +22,21 @@ module Halitosis
|
|
24
22
|
# @return [Module] self
|
25
23
|
#
|
26
24
|
def define_resource(name)
|
27
|
-
self.
|
25
|
+
self.resource_type = name.to_s
|
28
26
|
|
29
27
|
alias_method name, :resource
|
30
28
|
end
|
31
29
|
|
32
|
-
# Override standard
|
30
|
+
# Override standard attribute field for resource-based serializers
|
33
31
|
#
|
34
|
-
# @param name [Symbol, String] name of the
|
35
|
-
# @param options [nil, Hash]
|
32
|
+
# @param name [Symbol, String] name of the attribute
|
33
|
+
# @param options [nil, Hash] attribute options for field
|
36
34
|
#
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
field.procedure = proc { resource.send(name) }
|
41
|
-
end
|
35
|
+
def attribute(name, options = {}, &procedure)
|
36
|
+
unless procedure || options.key?(:value)
|
37
|
+
procedure = proc { resource.public_send(name) }
|
42
38
|
end
|
39
|
+
super(name, options, &procedure)
|
43
40
|
end
|
44
41
|
end
|
45
42
|
|
@@ -53,6 +50,23 @@ module Halitosis
|
|
53
50
|
|
54
51
|
super(**)
|
55
52
|
end
|
53
|
+
|
54
|
+
# @return [Hash] the rendered hash with resource, as a hash
|
55
|
+
#
|
56
|
+
def render_with_context(context)
|
57
|
+
if (include_root = context.fetch(:include_root) { context.depth.zero? })
|
58
|
+
{root_name(include_root, self.class.resource_type) => super}
|
59
|
+
else
|
60
|
+
super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def root_name(include_root, default)
|
67
|
+
return include_root.to_sym if include_root.is_a?(String) || include_root.is_a?(Symbol)
|
68
|
+
default.to_sym
|
69
|
+
end
|
56
70
|
end
|
57
71
|
end
|
58
72
|
end
|
data/lib/halitosis/version.rb
CHANGED
data/lib/halitosis.rb
CHANGED
@@ -15,9 +15,9 @@ require "json"
|
|
15
15
|
#
|
16
16
|
# resource :article
|
17
17
|
#
|
18
|
-
#
|
18
|
+
# attribute :id, required: true
|
19
19
|
#
|
20
|
-
#
|
20
|
+
# attribute :title
|
21
21
|
#
|
22
22
|
# link :self, -> { article_path(article) }
|
23
23
|
#
|
@@ -33,6 +33,11 @@ module Halitosis
|
|
33
33
|
base.extend ClassMethods
|
34
34
|
|
35
35
|
base.include Base
|
36
|
+
base.include Links
|
37
|
+
base.include Meta
|
38
|
+
base.include Permissions
|
39
|
+
base.include Attributes
|
40
|
+
base.include Relationships
|
36
41
|
|
37
42
|
config.extensions.each { |extension| base.send :include, extension }
|
38
43
|
end
|
@@ -68,11 +73,12 @@ module Halitosis
|
|
68
73
|
end
|
69
74
|
end
|
70
75
|
|
76
|
+
require_relative "halitosis/context"
|
71
77
|
require_relative "halitosis/base"
|
72
78
|
require_relative "halitosis/errors"
|
73
79
|
require_relative "halitosis/field"
|
74
80
|
require_relative "halitosis/fields"
|
75
|
-
require_relative "halitosis/
|
81
|
+
require_relative "halitosis/attributes"
|
76
82
|
require_relative "halitosis/links"
|
77
83
|
require_relative "halitosis/meta"
|
78
84
|
require_relative "halitosis/permissions"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: halitosis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Morrall
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Provides an interface for serializing resources as JSON with HAL-like
|
14
14
|
links and relationships.
|
@@ -26,10 +26,13 @@ files:
|
|
26
26
|
- README.md
|
27
27
|
- Rakefile
|
28
28
|
- lib/halitosis.rb
|
29
|
+
- lib/halitosis/attributes.rb
|
30
|
+
- lib/halitosis/attributes/field.rb
|
29
31
|
- lib/halitosis/base.rb
|
30
32
|
- lib/halitosis/collection.rb
|
31
33
|
- lib/halitosis/collection/field.rb
|
32
34
|
- lib/halitosis/configuration.rb
|
35
|
+
- lib/halitosis/context.rb
|
33
36
|
- lib/halitosis/errors.rb
|
34
37
|
- lib/halitosis/field.rb
|
35
38
|
- lib/halitosis/fields.rb
|
@@ -40,8 +43,6 @@ files:
|
|
40
43
|
- lib/halitosis/meta/field.rb
|
41
44
|
- lib/halitosis/permissions.rb
|
42
45
|
- lib/halitosis/permissions/field.rb
|
43
|
-
- lib/halitosis/properties.rb
|
44
|
-
- lib/halitosis/properties/field.rb
|
45
46
|
- lib/halitosis/railtie.rb
|
46
47
|
- lib/halitosis/relationships.rb
|
47
48
|
- lib/halitosis/relationships/field.rb
|
data/lib/halitosis/properties.rb
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Halitosis
|
4
|
-
module Properties
|
5
|
-
def self.included(base)
|
6
|
-
base.extend ClassMethods
|
7
|
-
|
8
|
-
base.send :include, InstanceMethods
|
9
|
-
end
|
10
|
-
|
11
|
-
module ClassMethods
|
12
|
-
# @param name [Symbol, String]
|
13
|
-
# @param options [nil, Hash]
|
14
|
-
#
|
15
|
-
# @return [Halitosis::Properties::Field]
|
16
|
-
#
|
17
|
-
def property(name, options = {}, &procedure)
|
18
|
-
fields.add(Field.new(name, options, procedure))
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
module InstanceMethods
|
23
|
-
# @return [Hash] the rendered hash with properties, if any
|
24
|
-
#
|
25
|
-
def render
|
26
|
-
super.merge(properties)
|
27
|
-
end
|
28
|
-
|
29
|
-
# @return [Hash] properties from fields
|
30
|
-
#
|
31
|
-
def properties
|
32
|
-
render_fields(Field.name) do |field, result|
|
33
|
-
result[field.name] = field.value(self)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
require "halitosis/properties/field"
|