reynard 0.9.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52cb53bbff626e0dce10485bf5cc023639b024024a237e34a73d09ae632ef9db
4
- data.tar.gz: 1b66f322e4f7bfe5d0a5bd63096f218994bd55ed747a2fb2e1fa72c4af57ef30
3
+ metadata.gz: 9d2a2059bfff7777ad1d0b122d83afff99bc6af883a35f8970091fa3197fd541
4
+ data.tar.gz: 05d9297e7d4d86e43f733f5c385ac2ea93a16b681d4f51e09bea9e20e97a00bc
5
5
  SHA512:
6
- metadata.gz: 15a3077ed0ee0c2331cab0182adae4b3970d83a4113f82d2d8aa4e528edd93678dd10a7e346d54e8d3e659dd5ad3509415f9de714a61a013e1937c3781eb686d
7
- data.tar.gz: 80f49722dbde52f83aa9260aea1f71ccc6b97c7871f0818a71b4d42b4a79683cc404a0fa0ad8714f0d4fa4d3def54d2e140be1b35abe215699098da25c9b7b00
6
+ metadata.gz: 8b0ccb3d8e187780cdca33e65588b4da8d835910f247f618e5b67114e58cda744745826ff9e8c223742be55c003ea6df904b18a6fee9bc7e2b7b466c9739f751
7
+ data.tar.gz: dd5bf8691ae5c72ea7462e313992671ad82125fb0de3228d09d6a26fa367734cd14498de7bc02dda73684b255774b40e5991464c606609bc16517528f75b4219
@@ -15,7 +15,7 @@ class Reynard
15
15
  end
16
16
 
17
17
  def base_url(base_url)
18
- copy(base_url: 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: 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: http_response
77
+ http_response:
78
78
  )
79
79
  end
80
80
  end
@@ -73,7 +73,7 @@ class Reynard
73
73
  ObjectBuilder.new(
74
74
  schema: @specification.schema(media_type.node),
75
75
  inflector: @inflector,
76
- parsed_body: parsed_body
76
+ parsed_body:
77
77
  ).call
78
78
  end
79
79
 
data/lib/reynard/model.rb CHANGED
@@ -76,7 +76,7 @@ class Reynard
76
76
  property = schema.property_schema(name)
77
77
  return value unless property
78
78
 
79
- ::Reynard::ObjectBuilder.new(schema: property, inflector: inflector, parsed_body: value).call
79
+ ::Reynard::ObjectBuilder.new(schema: property, inflector:, parsed_body: value).call
80
80
  end
81
81
 
82
82
  def self.inflector
@@ -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
@@ -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
- return @model_name if defined?(@model_name)
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
- # Returns a model name based on the schema's title or $ref.
83
- def find_model_name
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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Reynard
4
+ class Specification
5
+ # Describes a query for a node in a specification.
6
+ class Query
7
+ attr_reader :type
8
+
9
+ def initialize(type: nil)
10
+ @type = type
11
+ end
12
+ end
13
+ end
14
+ 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Reynard
4
- VERSION = '0.9.0'
4
+ VERSION = '0.10.0'
5
5
  end
data/lib/reynard.rb CHANGED
@@ -34,7 +34,7 @@ class Reynard
34
34
  autoload :VERSION, 'reynard/version'
35
35
 
36
36
  def initialize(filename:)
37
- @specification = Specification.new(filename: filename)
37
+ @specification = Specification.new(filename:)
38
38
  @inflector = Inflector.new
39
39
  end
40
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.9.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: 2024-02-28 00:00:00.000000000 Z
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.0'
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.5.6
109
+ rubygems_version: 3.5.16
107
110
  signing_key:
108
111
  specification_version: 4
109
112
  summary: Minimal OpenAPI client.