reynard 0.8.2 → 0.10.0
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.
- checksums.yaml +4 -4
- data/lib/reynard/context.rb +3 -3
- data/lib/reynard/http/response.rb +1 -1
- data/lib/reynard/model.rb +34 -13
- data/lib/reynard/schema/model_naming.rb +127 -0
- data/lib/reynard/schema.rb +5 -60
- data/lib/reynard/specification/finder.rb +31 -0
- data/lib/reynard/specification/query.rb +14 -0
- data/lib/reynard/specification.rb +12 -0
- data/lib/reynard/version.rb +1 -1
- data/lib/reynard.rb +1 -2
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d2a2059bfff7777ad1d0b122d83afff99bc6af883a35f8970091fa3197fd541
|
4
|
+
data.tar.gz: 05d9297e7d4d86e43f733f5c385ac2ea93a16b681d4f51e09bea9e20e97a00bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b0ccb3d8e187780cdca33e65588b4da8d835910f247f618e5b67114e58cda744745826ff9e8c223742be55c003ea6df904b18a6fee9bc7e2b7b466c9739f751
|
7
|
+
data.tar.gz: dd5bf8691ae5c72ea7462e313992671ad82125fb0de3228d09d6a26fa367734cd14498de7bc02dda73684b255774b40e5991464c606609bc16517528f75b4219
|
data/lib/reynard/context.rb
CHANGED
@@ -15,7 +15,7 @@ class Reynard
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def base_url(base_url)
|
18
|
-
copy(base_url:
|
18
|
+
copy(base_url:)
|
19
19
|
end
|
20
20
|
|
21
21
|
def operation(operation_name)
|
@@ -44,7 +44,7 @@ class Reynard
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def logger(logger)
|
47
|
-
copy(logger:
|
47
|
+
copy(logger:)
|
48
48
|
end
|
49
49
|
|
50
50
|
def execute
|
@@ -74,7 +74,7 @@ class Reynard
|
|
74
74
|
specification: @specification,
|
75
75
|
inflector: @inflector,
|
76
76
|
request_context: @request_context,
|
77
|
-
http_response:
|
77
|
+
http_response:
|
78
78
|
)
|
79
79
|
end
|
80
80
|
end
|
data/lib/reynard/model.rb
CHANGED
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
class Reynard
|
4
4
|
# Superclass for dynamic classes generated by the object builder.
|
5
|
-
class Model
|
6
|
-
extend Forwardable
|
7
|
-
def_delegators :@attributes, :[]
|
5
|
+
class Model < BasicObject
|
6
|
+
extend ::Forwardable
|
7
|
+
def_delegators :@attributes, :[], :empty?
|
8
8
|
|
9
9
|
class << self
|
10
10
|
# Holds references to the full schema for the model if available.
|
@@ -14,19 +14,28 @@ class Reynard
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def initialize(attributes)
|
17
|
-
if attributes.respond_to?(:each)
|
17
|
+
if attributes.respond_to?(:each) && attributes.respond_to?(:keys)
|
18
18
|
@attributes = {}
|
19
19
|
@snake_cases = self.class.snake_cases(attributes.keys)
|
20
20
|
self.attributes = attributes
|
21
21
|
else
|
22
|
-
raise(
|
23
|
-
ArgumentError,
|
24
|
-
|
25
|
-
"`#{attributes.inspect}'"
|
22
|
+
::Kernel.raise(
|
23
|
+
::ArgumentError,
|
24
|
+
self.class.attributes_error_message(attributes)
|
26
25
|
)
|
27
26
|
end
|
28
27
|
end
|
29
28
|
|
29
|
+
# We rely on these methods for various reasons so we re-introduce them at the expense of
|
30
|
+
# allowing them to be used as attribute name for the model.
|
31
|
+
%i[class is_a? nil? object_id kind_of? respond_to? send].each do |method|
|
32
|
+
define_method(method, ::Kernel.method(method))
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)}>"
|
37
|
+
end
|
38
|
+
|
30
39
|
def attributes=(attributes)
|
31
40
|
attributes.each do |name, value|
|
32
41
|
@attributes[name.to_s] = self.class.cast(name, value)
|
@@ -35,14 +44,16 @@ class Reynard
|
|
35
44
|
|
36
45
|
# Until we can set accessors based on the schema
|
37
46
|
def method_missing(attribute_name, *)
|
47
|
+
return false unless @attributes
|
48
|
+
|
38
49
|
attribute_name = attribute_name.to_s
|
39
50
|
if @attributes.key?(attribute_name)
|
40
51
|
@attributes[attribute_name]
|
41
52
|
else
|
42
53
|
@attributes.fetch(@snake_cases.fetch(attribute_name))
|
43
54
|
end
|
44
|
-
rescue KeyError
|
45
|
-
raise NoMethodError, "undefined method `#{attribute_name}' for #{inspect}"
|
55
|
+
rescue ::KeyError
|
56
|
+
::Kernel.raise ::NoMethodError, "undefined method `#{attribute_name}' for #{inspect}"
|
46
57
|
end
|
47
58
|
|
48
59
|
def respond_to_missing?(attribute_name, *)
|
@@ -50,10 +61,14 @@ class Reynard
|
|
50
61
|
return true if @attributes.key?(attribute_name)
|
51
62
|
|
52
63
|
@snake_cases.key?(attribute_name) && @attributes.key?(@snake_cases[attribute_name])
|
53
|
-
rescue NameError
|
64
|
+
rescue ::NameError
|
54
65
|
false
|
55
66
|
end
|
56
67
|
|
68
|
+
def try(attribute_name)
|
69
|
+
respond_to_missing?(attribute_name) ? send(attribute_name) : nil
|
70
|
+
end
|
71
|
+
|
57
72
|
def self.cast(name, value)
|
58
73
|
return if value.nil?
|
59
74
|
return value unless schema
|
@@ -61,11 +76,11 @@ class Reynard
|
|
61
76
|
property = schema.property_schema(name)
|
62
77
|
return value unless property
|
63
78
|
|
64
|
-
Reynard::ObjectBuilder.new(schema: property, inflector
|
79
|
+
::Reynard::ObjectBuilder.new(schema: property, inflector:, parsed_body: value).call
|
65
80
|
end
|
66
81
|
|
67
82
|
def self.inflector
|
68
|
-
@inflector ||= Inflector.new
|
83
|
+
@inflector ||= ::Reynard::Inflector.new
|
69
84
|
end
|
70
85
|
|
71
86
|
def self.snake_cases(property_names)
|
@@ -76,5 +91,11 @@ class Reynard
|
|
76
91
|
snake_cases[snake_case] = property_name
|
77
92
|
end
|
78
93
|
end
|
94
|
+
|
95
|
+
def self.attributes_error_message(attributes)
|
96
|
+
'Models must be intialized with an enumerable object that behaves like a Hash, got: ' \
|
97
|
+
"`#{attributes.inspect}'. Usually this means the schema defined in the OpenAPI " \
|
98
|
+
"specification doesn't fit the payload in the HTTP response."
|
99
|
+
end
|
79
100
|
end
|
80
101
|
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Reynard
|
4
|
+
class Schema
|
5
|
+
# Rummages through the specification to find a suitable model name for a response object.
|
6
|
+
class ModelNaming
|
7
|
+
def initialize(specification:, node:)
|
8
|
+
@specification = specification
|
9
|
+
@node = node
|
10
|
+
end
|
11
|
+
|
12
|
+
def model_name
|
13
|
+
model_name = determine_model_name
|
14
|
+
if class_type == :array
|
15
|
+
"#{model_name}Collection"
|
16
|
+
else
|
17
|
+
model_name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.title_model_name(model_name)
|
22
|
+
return unless model_name
|
23
|
+
|
24
|
+
model_name
|
25
|
+
.gsub(/[^[:alpha:]]/, ' ')
|
26
|
+
.gsub(/\s{2,}/, ' ')
|
27
|
+
.gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
|
28
|
+
.strip
|
29
|
+
end
|
30
|
+
|
31
|
+
# Extracts a model name from a ref when there is a usable value.
|
32
|
+
#
|
33
|
+
# ref_model_name("#/components/schemas/Library") => "Library"
|
34
|
+
def self.ref_model_name(ref)
|
35
|
+
return unless ref
|
36
|
+
|
37
|
+
normalize_ref_model_name(ref.split('/')&.last)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.normalize_ref_model_name(model_name)
|
41
|
+
# 1. Unescape encoded characters to create an UTF-8 string
|
42
|
+
# 2. Remove extensions for regularly used external schema files
|
43
|
+
# 3. Replace all non-alphabetic characters with a space (not allowed in Ruby constant)
|
44
|
+
# 4. Camelcase
|
45
|
+
Rack::Utils.unescape_path(model_name)
|
46
|
+
.gsub(/(.yml|.yaml|.json)\Z/, '')
|
47
|
+
.gsub(/[^[:alpha:]]/, ' ')
|
48
|
+
.gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
|
49
|
+
.gsub(/\A(.)/) { Regexp.last_match(1).upcase }
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.singularize(name)
|
53
|
+
case name
|
54
|
+
when /ies$/
|
55
|
+
"#{name[0..-4]}y"
|
56
|
+
else
|
57
|
+
name.chomp('s')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def class_type
|
64
|
+
@specification.dig(*@node).key?('items') ? :array : :object
|
65
|
+
end
|
66
|
+
|
67
|
+
def determine_model_name
|
68
|
+
title_model_name || ref_model_name || node_model_name
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns a model name when it was explicitly set using the title property in the specification.
|
72
|
+
def title_model_name
|
73
|
+
title = @specification.dig(*@node, 'title')
|
74
|
+
return unless title
|
75
|
+
|
76
|
+
self.class.title_model_name(title)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns a model name based on the schema's $ref value, usually this contains a usable
|
80
|
+
# identifier at the end like /books.yml or /Books.
|
81
|
+
def ref_model_name
|
82
|
+
parent = @specification.dig(*@node[..-2])
|
83
|
+
ref = parent.dig('schema', '$ref') || parent.dig('items', '$ref')
|
84
|
+
return unless ref
|
85
|
+
|
86
|
+
self.class.ref_model_name(ref)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns a model name based on the node path to schema in the specification.
|
90
|
+
def node_model_name
|
91
|
+
self.class.title_model_name(node_path_name.capitalize.gsub(/[_-]/, ' '))
|
92
|
+
end
|
93
|
+
|
94
|
+
def node_path_name
|
95
|
+
if node_anyonymous?
|
96
|
+
request_path_model_name
|
97
|
+
elsif @node.last == 'items'
|
98
|
+
# Use the property name as the model name for its items, for example in the case of
|
99
|
+
# schema > properties > birds > items => bird.
|
100
|
+
self.class.singularize(@node.at(-2))
|
101
|
+
else
|
102
|
+
# Usually this means we are dealing with a property's name a not a model, for example in
|
103
|
+
# the case of schema > properties > color => color.
|
104
|
+
@node.last
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns true when the node path doesn't have identifyable segments other than the request
|
109
|
+
# path for the resource.
|
110
|
+
#
|
111
|
+
# For example, when the last part of the path looks like this:
|
112
|
+
# get > responses > 200 > content > application|json > schema
|
113
|
+
def node_anyonymous?
|
114
|
+
@node.last == 'schema' || @node.last(2) == %w[schema items]
|
115
|
+
end
|
116
|
+
|
117
|
+
# Finds the first segment starting from the end of the request path that is not a parameter
|
118
|
+
# and transforms that to make a model name.
|
119
|
+
#
|
120
|
+
# For example:
|
121
|
+
# /books/{id} => "book"
|
122
|
+
def request_path_model_name
|
123
|
+
self.class.singularize(@node[1].split('/').reverse.find { |part| !part.start_with?('{') })
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/reynard/schema.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
class Reynard
|
4
4
|
# Holds a references to a schema definition in the specification.
|
5
5
|
class Schema
|
6
|
+
autoload :ModelNaming, 'reynard/schema/model_naming'
|
7
|
+
|
6
8
|
attr_reader :node, :namespace
|
7
9
|
|
8
10
|
def initialize(specification:, node:, namespace: nil)
|
@@ -18,9 +20,7 @@ class Reynard
|
|
18
20
|
end
|
19
21
|
|
20
22
|
def model_name
|
21
|
-
|
22
|
-
|
23
|
-
@model_name = find_model_name
|
23
|
+
@model_name || model_naming.model_name
|
24
24
|
end
|
25
25
|
|
26
26
|
# Returns the schema for items when the current schema is an array.
|
@@ -46,65 +46,10 @@ class Reynard
|
|
46
46
|
)
|
47
47
|
end
|
48
48
|
|
49
|
-
def self.title_model_name(model_name)
|
50
|
-
return unless model_name
|
51
|
-
|
52
|
-
model_name
|
53
|
-
.gsub(/[^[:alpha:]]/, ' ')
|
54
|
-
.gsub(/\s{2,}/, ' ')
|
55
|
-
.gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
|
56
|
-
.strip
|
57
|
-
end
|
58
|
-
|
59
|
-
# Extracts a model name from a ref when there is a usable value.
|
60
|
-
#
|
61
|
-
# ref_model_name("#/components/schemas/Library") => "Library"
|
62
|
-
def self.ref_model_name(ref)
|
63
|
-
return unless ref
|
64
|
-
|
65
|
-
normalize_ref_model_name(ref.split('/')&.last)
|
66
|
-
end
|
67
|
-
|
68
|
-
def self.normalize_ref_model_name(model_name)
|
69
|
-
# 1. Unescape encoded characters to create an UTF-8 string
|
70
|
-
# 2. Remove extensions for regularly used external schema files
|
71
|
-
# 3. Replace all non-alphabetic characters with a space (not allowed in Ruby constant)
|
72
|
-
# 4. Camelcase
|
73
|
-
Rack::Utils.unescape_path(model_name)
|
74
|
-
.gsub(/(.yml|.yaml|.json)\Z/, '')
|
75
|
-
.gsub(/[^[:alpha:]]/, ' ')
|
76
|
-
.gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
|
77
|
-
.gsub(/\A(.)/) { Regexp.last_match(1).upcase }
|
78
|
-
end
|
79
|
-
|
80
49
|
private
|
81
50
|
|
82
|
-
|
83
|
-
|
84
|
-
title_model_name || ref_model_name || node_model_name
|
85
|
-
end
|
86
|
-
|
87
|
-
def title_model_name
|
88
|
-
title = @specification.dig(*node, 'title')
|
89
|
-
return unless title
|
90
|
-
|
91
|
-
self.class.title_model_name(title)
|
92
|
-
end
|
93
|
-
|
94
|
-
def ref_model_name
|
95
|
-
parent = @specification.dig(*node[..-2])
|
96
|
-
ref = parent.dig('schema', '$ref') || parent.dig('items', '$ref')
|
97
|
-
return unless ref
|
98
|
-
|
99
|
-
self.class.ref_model_name(ref)
|
100
|
-
end
|
101
|
-
|
102
|
-
def node_model_name
|
103
|
-
self.class.title_model_name(node_property_name.capitalize.gsub(/[_-]/, ' '))
|
104
|
-
end
|
105
|
-
|
106
|
-
def node_property_name
|
107
|
-
node.last == 'items' ? node.at(-2).chomp('s') : node.last
|
51
|
+
def model_naming
|
52
|
+
ModelNaming.new(specification: @specification, node: @node)
|
108
53
|
end
|
109
54
|
end
|
110
55
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Reynard
|
4
|
+
class Specification
|
5
|
+
# Finds nodes in a specification that match a query.
|
6
|
+
class Finder
|
7
|
+
def initialize(specification:, query:)
|
8
|
+
@specification = specification
|
9
|
+
@query = query
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_each(&)
|
13
|
+
find_into([], &)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def find_into(path, &block)
|
19
|
+
data = @specification.dig(*path)
|
20
|
+
|
21
|
+
yield path if data.respond_to?(:key?) && (data.key?('type') && (@query.type == data['type']))
|
22
|
+
|
23
|
+
return unless data.respond_to?(:each_key)
|
24
|
+
|
25
|
+
data.each_key do |key|
|
26
|
+
find_into([*path, key], &block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -5,6 +5,9 @@ require 'rack'
|
|
5
5
|
class Reynard
|
6
6
|
# Wraps the YAML representation of an OpenAPI specification.
|
7
7
|
class Specification
|
8
|
+
autoload :Finder, 'reynard/specification/finder'
|
9
|
+
autoload :Query, 'reynard/specification/query'
|
10
|
+
|
8
11
|
VERBS = %w[get put post delete options head patch trace].freeze
|
9
12
|
|
10
13
|
def initialize(filename:)
|
@@ -17,6 +20,15 @@ class Reynard
|
|
17
20
|
dig_into(@data, @data, path.dup, File.dirname(@filename))
|
18
21
|
end
|
19
22
|
|
23
|
+
# Yields all nodes in the specification matching the specified type.
|
24
|
+
#
|
25
|
+
# specification.find_each(type: 'object') {}
|
26
|
+
#
|
27
|
+
# Please don't use this in a hot paths in production, primarily meant for testing and tooling.
|
28
|
+
def find_each(type:, &block)
|
29
|
+
Finder.new(specification: self, query: Query.new(type:)).find_each(&block)
|
30
|
+
end
|
31
|
+
|
20
32
|
def servers
|
21
33
|
dig('servers').map { |attributes| Server.new(attributes) }
|
22
34
|
end
|
data/lib/reynard/version.rb
CHANGED
data/lib/reynard.rb
CHANGED
@@ -20,7 +20,6 @@ class Reynard
|
|
20
20
|
autoload :GroupedParameters, 'reynard/grouped_parameters'
|
21
21
|
autoload :Http, 'reynard/http'
|
22
22
|
autoload :Inflector, 'reynard/inflector'
|
23
|
-
autoload :Logger, 'reynard/logger'
|
24
23
|
autoload :MediaType, 'reynard/media_type'
|
25
24
|
autoload :Model, 'reynard/model'
|
26
25
|
autoload :Models, 'reynard/models'
|
@@ -35,7 +34,7 @@ class Reynard
|
|
35
34
|
autoload :VERSION, 'reynard/version'
|
36
35
|
|
37
36
|
def initialize(filename:)
|
38
|
-
@specification = Specification.new(filename:
|
37
|
+
@specification = Specification.new(filename:)
|
39
38
|
@inflector = Inflector.new
|
40
39
|
end
|
41
40
|
|
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.10.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:
|
11
|
+
date: 2024-08-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multi_json
|
@@ -78,9 +78,12 @@ files:
|
|
78
78
|
- lib/reynard/operation.rb
|
79
79
|
- lib/reynard/request_context.rb
|
80
80
|
- lib/reynard/schema.rb
|
81
|
+
- lib/reynard/schema/model_naming.rb
|
81
82
|
- lib/reynard/serialized_body.rb
|
82
83
|
- lib/reynard/server.rb
|
83
84
|
- lib/reynard/specification.rb
|
85
|
+
- lib/reynard/specification/finder.rb
|
86
|
+
- lib/reynard/specification/query.rb
|
84
87
|
- lib/reynard/template.rb
|
85
88
|
- lib/reynard/version.rb
|
86
89
|
homepage: https://github.com/Manfred/reynard
|
@@ -96,14 +99,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
99
|
requirements:
|
97
100
|
- - ">"
|
98
101
|
- !ruby/object:Gem::Version
|
99
|
-
version: '3.
|
102
|
+
version: '3.1'
|
100
103
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
104
|
requirements:
|
102
105
|
- - ">="
|
103
106
|
- !ruby/object:Gem::Version
|
104
107
|
version: '0'
|
105
108
|
requirements: []
|
106
|
-
rubygems_version: 3.
|
109
|
+
rubygems_version: 3.5.16
|
107
110
|
signing_key:
|
108
111
|
specification_version: 4
|
109
112
|
summary: Minimal OpenAPI client.
|