reynard 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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.