reynard 0.7.0 → 0.8.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 +66 -0
- data/lib/reynard/context.rb +4 -1
- data/lib/reynard/external.rb +6 -2
- data/lib/reynard/http/response.rb +3 -1
- data/lib/reynard/inflector.rb +30 -0
- data/lib/reynard/model.rb +29 -5
- data/lib/reynard/object_builder.rb +9 -5
- data/lib/reynard/specification.rb +4 -3
- data/lib/reynard/version.rb +1 -1
- data/lib/reynard.rb +4 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96421332b89c0816b77704aba7b07f267424c0a238fd5b74c8b91aa9d3943c6d
|
4
|
+
data.tar.gz: a6a714f984bd420fe2254b747972ba804968cc4df6ca534b38c50c1cf2b2687b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cbb02d762e3c38b05b59ec25f8320f3a5830b56aabe376249f2347a098b43cff31935829d9dd831edc383a62531a06b78e735d886bf4f2329475e8785820211
|
7
|
+
data.tar.gz: 767b9ba9f0f5eb3fae98d414f778e481fd6b3c0a928128cec29b3a409db44481057afc44c9ac029a98892b71c3d3e09fd80286e0b2e942e47aef97b2921fab22
|
data/README.md
CHANGED
@@ -152,6 +152,72 @@ For example, in case of an array item it would look at `books` and singularize i
|
|
152
152
|
|
153
153
|
If you run into issues where Reynard doesn't properly build an object for a nested resource, it's probably because of a naming issue. It's advised to add a `title` property to the schema definition with a unique name in that case.
|
154
154
|
|
155
|
+
### Properties and model attributes
|
156
|
+
|
157
|
+
Reynard provides access to JSON properties on the model in a number of ways. There are some restrictions because of Ruby, so it's good to understand them.
|
158
|
+
|
159
|
+
Let's assume there is a payload for an `Author` model that looks like this:
|
160
|
+
|
161
|
+
```json
|
162
|
+
{"first_name":"Marcél","lastName":"Marcellus","1st-class":false}
|
163
|
+
```
|
164
|
+
|
165
|
+
Reynard attemps to give access to these properties as much as possible by sanitizing and normalizing them, so you can do the following:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
response.object.first_name #=> "Marcél"
|
169
|
+
response.object.last_name #=> "Marcellus"
|
170
|
+
```
|
171
|
+
|
172
|
+
But it's also possible to use the original casing for `lastName`.
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
response.object.lastName #=> "Marcellus"
|
176
|
+
```
|
177
|
+
|
178
|
+
However, a method can't start with a number and can't contain dashes in Ruby so the following is not possible:
|
179
|
+
|
180
|
+
```
|
181
|
+
# Not valid Ruby syntax:
|
182
|
+
response.object.1st-class
|
183
|
+
```
|
184
|
+
|
185
|
+
There are two alternatives for accessing this property:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
# The preferred solution for accessing raw property values is through the
|
189
|
+
# parsed JSON on the response object.
|
190
|
+
response.parsed_body["1st-class"]
|
191
|
+
# When you are processing nested models and you don't have access to the
|
192
|
+
# response object, you can chose to use the `[]` method.
|
193
|
+
response.object["1st-class"]
|
194
|
+
# Don't use `send` to access the property, this may not work in future
|
195
|
+
# versions.
|
196
|
+
response.object.send("1st-class")
|
197
|
+
```
|
198
|
+
|
199
|
+
#### Mapping properties
|
200
|
+
|
201
|
+
In case you are forced to access a property through a method, you could chose to map irregular property names to method names globally for all models:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
reynard.snake_cases({ "1st-class" => "first_class" })
|
205
|
+
```
|
206
|
+
|
207
|
+
This will allow you to access the property through the `first_class` method without changing the behavior of the rest of the object.
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
response.object.first_class #=> false
|
211
|
+
response.object["1st-class"] #=> false
|
212
|
+
```
|
213
|
+
|
214
|
+
Don't use this to map common property names that would work fine otherwise, because you could make things really confusing.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
# Don't do this.
|
218
|
+
reynard.snake_cases({ "name" => "naem" })
|
219
|
+
```
|
220
|
+
|
155
221
|
## Logging
|
156
222
|
|
157
223
|
When you want to know what the Reynard client is doing you can enable logging.
|
data/lib/reynard/context.rb
CHANGED
@@ -8,8 +8,9 @@ class Reynard
|
|
8
8
|
extend Forwardable
|
9
9
|
def_delegators :@request_context, :verb, :path, :full_path, :url
|
10
10
|
|
11
|
-
def initialize(specification:, request_context: nil)
|
11
|
+
def initialize(specification:, inflector:, request_context: nil)
|
12
12
|
@specification = specification
|
13
|
+
@inflector = inflector
|
13
14
|
@request_context = request_context || build_request_context
|
14
15
|
end
|
15
16
|
|
@@ -59,6 +60,7 @@ class Reynard
|
|
59
60
|
def copy(**properties)
|
60
61
|
self.class.new(
|
61
62
|
specification: @specification,
|
63
|
+
inflector: @inflector,
|
62
64
|
request_context: @request_context.copy(**properties)
|
63
65
|
)
|
64
66
|
end
|
@@ -70,6 +72,7 @@ class Reynard
|
|
70
72
|
def build_response(http_response)
|
71
73
|
Reynard::Http::Response.new(
|
72
74
|
specification: @specification,
|
75
|
+
inflector: @inflector,
|
73
76
|
request_context: @request_context,
|
74
77
|
http_response: http_response
|
75
78
|
)
|
data/lib/reynard/external.rb
CHANGED
@@ -8,6 +8,7 @@ class Reynard
|
|
8
8
|
def initialize(path:, ref:)
|
9
9
|
@path = path
|
10
10
|
@relative_path, @anchor = ref.split('#', 2)
|
11
|
+
@filename = File.expand_path(@relative_path, @path)
|
11
12
|
end
|
12
13
|
|
13
14
|
def path
|
@@ -22,11 +23,14 @@ class Reynard
|
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
26
|
+
def filesystem_path
|
27
|
+
File.dirname(@filename)
|
28
|
+
end
|
29
|
+
|
25
30
|
private
|
26
31
|
|
27
32
|
def filename
|
28
|
-
filename
|
29
|
-
return filename if filename.start_with?(@path)
|
33
|
+
return @filename if @filename.start_with?(@path)
|
30
34
|
|
31
35
|
raise 'You are only allowed to reference files relative to the specification file.'
|
32
36
|
end
|
@@ -8,8 +8,9 @@ class Reynard
|
|
8
8
|
extend Forwardable
|
9
9
|
def_delegators :@http_response, :code, :content_type, :[], :body
|
10
10
|
|
11
|
-
def initialize(specification:, request_context:, http_response:)
|
11
|
+
def initialize(specification:, inflector:, request_context:, http_response:)
|
12
12
|
@specification = specification
|
13
|
+
@inflector = inflector
|
13
14
|
@request_context = request_context
|
14
15
|
@http_response = http_response
|
15
16
|
end
|
@@ -71,6 +72,7 @@ class Reynard
|
|
71
72
|
def build_object_with_media_type(media_type)
|
72
73
|
ObjectBuilder.new(
|
73
74
|
schema: @specification.schema(media_type.node),
|
75
|
+
inflector: @inflector,
|
74
76
|
parsed_body: parsed_body
|
75
77
|
).call
|
76
78
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Reynard
|
4
|
+
# Transforms property names so they are value Ruby identifiers or more readable to users.
|
5
|
+
class Inflector
|
6
|
+
def initialize
|
7
|
+
@snake_case = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Registers additional exceptions to the regular snake-case algorithm. Registering is additive
|
11
|
+
# so you can call this multiple times without losing previously registered exceptions.
|
12
|
+
def snake_cases(exceptions)
|
13
|
+
@snake_case.merge!(exceptions)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the string in snake-case, taking previously registered exceptions into account.
|
17
|
+
def snake_case(property)
|
18
|
+
@snake_case[property] || self.class.snake_case(property)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the string in snake-case.
|
22
|
+
def self.snake_case(property)
|
23
|
+
property
|
24
|
+
.to_s
|
25
|
+
.gsub(/([A-Z])(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { (Regexp.last_match(1) || Regexp.last_match(2)) << '_' }
|
26
|
+
.tr("'\"-", '___')
|
27
|
+
.downcase
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/reynard/model.rb
CHANGED
@@ -3,30 +3,41 @@
|
|
3
3
|
class Reynard
|
4
4
|
# Superclass for dynamic classes generated by the object builder.
|
5
5
|
class Model
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@attributes, :[]
|
8
|
+
|
6
9
|
class << self
|
7
10
|
# Holds references to the full schema for the model if available.
|
8
11
|
attr_accessor :schema
|
12
|
+
# The inflector to use on properties.
|
13
|
+
attr_writer :inflector
|
9
14
|
end
|
10
15
|
|
11
16
|
def initialize(attributes)
|
17
|
+
@attributes = {}
|
18
|
+
@snake_cases = self.class.snake_cases(attributes.keys)
|
12
19
|
self.attributes = attributes
|
13
20
|
end
|
14
21
|
|
15
22
|
def attributes=(attributes)
|
16
23
|
attributes.each do |name, value|
|
17
|
-
|
24
|
+
@attributes[name.to_s] = self.class.cast(name, value)
|
18
25
|
end
|
19
26
|
end
|
20
27
|
|
21
28
|
# Until we can set accessors based on the schema
|
22
29
|
def method_missing(attribute_name, *)
|
23
|
-
|
24
|
-
|
30
|
+
attribute_name = attribute_name.to_s
|
31
|
+
@attributes[attribute_name] || @attributes.fetch(@snake_cases.fetch(attribute_name))
|
32
|
+
rescue KeyError
|
25
33
|
raise NoMethodError, "undefined method `#{attribute_name}' for #{inspect}"
|
26
34
|
end
|
27
35
|
|
28
36
|
def respond_to_missing?(attribute_name, *)
|
29
|
-
|
37
|
+
attribute_name = attribute_name.to_s
|
38
|
+
return true if @attributes.key?(attribute_name)
|
39
|
+
|
40
|
+
@snake_cases.key?(attribute_name) && @attributes.key?(@snake_cases[attribute_name])
|
30
41
|
rescue NameError
|
31
42
|
false
|
32
43
|
end
|
@@ -37,7 +48,20 @@ class Reynard
|
|
37
48
|
property = schema.property_schema(name)
|
38
49
|
return value unless property
|
39
50
|
|
40
|
-
Reynard::ObjectBuilder.new(schema: property, parsed_body: value).call
|
51
|
+
Reynard::ObjectBuilder.new(schema: property, inflector: inflector, parsed_body: value).call
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.inflector
|
55
|
+
@inflector ||= Inflector.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.snake_cases(property_names)
|
59
|
+
property_names.each_with_object({}) do |property_name, snake_cases|
|
60
|
+
snake_case = inflector.snake_case(property_name)
|
61
|
+
next if snake_case == property_name
|
62
|
+
|
63
|
+
snake_cases[snake_case] = property_name
|
64
|
+
end
|
41
65
|
end
|
42
66
|
end
|
43
67
|
end
|
@@ -7,8 +7,9 @@ class Reynard
|
|
7
7
|
class ObjectBuilder
|
8
8
|
attr_reader :schema, :parsed_body
|
9
9
|
|
10
|
-
def initialize(schema:, parsed_body:, model_name: nil)
|
10
|
+
def initialize(schema:, inflector:, parsed_body:, model_name: nil)
|
11
11
|
@schema = schema
|
12
|
+
@inflector = inflector
|
12
13
|
@parsed_body = parsed_body
|
13
14
|
@model_name = model_name
|
14
15
|
end
|
@@ -21,7 +22,8 @@ class Reynard
|
|
21
22
|
return @model_class if defined?(@model_class)
|
22
23
|
|
23
24
|
@model_class =
|
24
|
-
self.class.model_class_get(model_name) ||
|
25
|
+
self.class.model_class_get(model_name) ||
|
26
|
+
self.class.model_class_set(model_name, schema, @inflector)
|
25
27
|
end
|
26
28
|
|
27
29
|
def call
|
@@ -41,11 +43,11 @@ class Reynard
|
|
41
43
|
nil
|
42
44
|
end
|
43
45
|
|
44
|
-
def self.model_class_set(model_name, schema)
|
46
|
+
def self.model_class_set(model_name, schema, inflector)
|
45
47
|
if schema.type == 'array'
|
46
48
|
array_model_class_set(model_name)
|
47
49
|
else
|
48
|
-
object_model_class_set(model_name, schema)
|
50
|
+
object_model_class_set(model_name, schema, inflector)
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
@@ -55,11 +57,12 @@ class Reynard
|
|
55
57
|
::Reynard::Models.const_set(model_name, Class.new(Array))
|
56
58
|
end
|
57
59
|
|
58
|
-
def self.object_model_class_set(model_name, schema)
|
60
|
+
def self.object_model_class_set(model_name, schema, inflector)
|
59
61
|
return Reynard::Model unless model_name
|
60
62
|
|
61
63
|
model_class = Class.new(Reynard::Model)
|
62
64
|
model_class.schema = schema
|
65
|
+
model_class.inflector = inflector
|
63
66
|
::Reynard::Models.const_set(model_name, model_class)
|
64
67
|
end
|
65
68
|
|
@@ -73,6 +76,7 @@ class Reynard
|
|
73
76
|
parsed_body.each do |item|
|
74
77
|
array << self.class.new(
|
75
78
|
schema: item_schema,
|
79
|
+
inflector: @inflector,
|
76
80
|
parsed_body: item
|
77
81
|
).call
|
78
82
|
end
|
@@ -14,7 +14,7 @@ class Reynard
|
|
14
14
|
|
15
15
|
# Digs a value out of the specification, taking $ref into account.
|
16
16
|
def dig(*path)
|
17
|
-
dig_into(@data, @data, path.dup)
|
17
|
+
dig_into(@data, @data, path.dup, File.dirname(@filename))
|
18
18
|
end
|
19
19
|
|
20
20
|
def servers
|
@@ -103,7 +103,7 @@ class Reynard
|
|
103
103
|
# rubocop:disable Metrics/AbcSize
|
104
104
|
# rubocop:disable Metrics/CyclomaticComplexity
|
105
105
|
# rubocop:disable Metrics/MethodLength
|
106
|
-
def dig_into(data, cursor, path)
|
106
|
+
def dig_into(data, cursor, path, filesystem_path)
|
107
107
|
while path.length.positive?
|
108
108
|
cursor = cursor[path.first]
|
109
109
|
return unless cursor
|
@@ -118,7 +118,8 @@ class Reynard
|
|
118
118
|
cursor = data
|
119
119
|
# References another file, with an optional anchor to an element in the data.
|
120
120
|
when %r{\A\./}
|
121
|
-
external = External.new(path:
|
121
|
+
external = External.new(path: filesystem_path, ref: cursor['$ref'])
|
122
|
+
filesystem_path = external.filesystem_path
|
122
123
|
path = external.path + path
|
123
124
|
cursor = external.data
|
124
125
|
end
|
data/lib/reynard/version.rb
CHANGED
data/lib/reynard.rb
CHANGED
@@ -13,11 +13,13 @@ class Reynard
|
|
13
13
|
extend Forwardable
|
14
14
|
def_delegators :build_context, :logger, :base_url, :operation, :headers, :params
|
15
15
|
def_delegators :@specification, :servers
|
16
|
+
def_delegators :@inflector, :snake_cases
|
16
17
|
|
17
18
|
autoload :Context, 'reynard/context'
|
18
19
|
autoload :External, 'reynard/external'
|
19
20
|
autoload :GroupedParameters, 'reynard/grouped_parameters'
|
20
21
|
autoload :Http, 'reynard/http'
|
22
|
+
autoload :Inflector, 'reynard/inflector'
|
21
23
|
autoload :Logger, 'reynard/logger'
|
22
24
|
autoload :MediaType, 'reynard/media_type'
|
23
25
|
autoload :Model, 'reynard/model'
|
@@ -34,6 +36,7 @@ class Reynard
|
|
34
36
|
|
35
37
|
def initialize(filename:)
|
36
38
|
@specification = Specification.new(filename: filename)
|
39
|
+
@inflector = Inflector.new
|
37
40
|
end
|
38
41
|
|
39
42
|
class << self
|
@@ -61,6 +64,6 @@ class Reynard
|
|
61
64
|
private
|
62
65
|
|
63
66
|
def build_context
|
64
|
-
Context.new(specification: @specification)
|
67
|
+
Context.new(specification: @specification, inflector: @inflector)
|
65
68
|
end
|
66
69
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reynard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manfred Stienstra
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multi_json
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- lib/reynard/http.rb
|
71
71
|
- lib/reynard/http/request.rb
|
72
72
|
- lib/reynard/http/response.rb
|
73
|
+
- lib/reynard/inflector.rb
|
73
74
|
- lib/reynard/media_type.rb
|
74
75
|
- lib/reynard/model.rb
|
75
76
|
- lib/reynard/models.rb
|