reynard 0.0.8 → 0.3.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: fcfd5e665e0b3fcc8c593d81c15db9560e8abc0125e9a254aeb98d3f53b6fb8a
4
- data.tar.gz: 61d7d812f5b5eacf55c9be3fb6463cf1adafeb5e7935d2d0fc0b20a33a4a27b3
3
+ metadata.gz: bb3f70f5fd68cbe19747d08687715165595d29a6b81da3329a975cac30bd3cbe
4
+ data.tar.gz: 73020e57c19507af7ac13b9bc509d3a4b7ac3ae9b74866d7f783cc816436039f
5
5
  SHA512:
6
- metadata.gz: 05a42b85120cb3e519c4fcc3bad7c23a17e5c1a007072b22843421619b1460dabf378750fc83eea406a44a3ec0e08c80b309d53bdce2da0df09d6f3a67643685
7
- data.tar.gz: d8e2ce1c77b097a4a3e4a64d8b3fd2d67bc0e8737307e9429452e4d794aedad7c34e7b44a76a1f4ac945f459a4f0497e67d354ebb37f2cb1fdece9e187981efd
6
+ metadata.gz: d5289ec587b945a0a858537569a4543e9bbfb3daa6dcb8e11cc409d620b89c1d85406d4a1fa0d20dcccc4c00eddcc6ff4b03f5ae24feff865f4cb62661f09638
7
+ data.tar.gz: af30bf905849a2589e091b0b9655e52375db08ea00f0b28365029900b4101c5d4b94822b30a17b3a1b8e35329886b61cf857fa6d8de1ff6a16fd56ca0dc2a904
data/README.md CHANGED
@@ -40,7 +40,7 @@ reynard.base_url('http://test.example.com/v1')
40
40
  You also have access to all servers in the specification so you can automatically select one however you want.
41
41
 
42
42
  ```ruby
43
- base_url = @reynard.servers.map(&:url).find do |url|
43
+ base_url = reynard.servers.map(&:url).find do |url|
44
44
  /staging/.match(url)
45
45
  end
46
46
  reynard.base_url(base_url)
@@ -51,7 +51,7 @@ reynard.base_url(base_url)
51
51
  Assuming there is an operation called `employeeByUuid` you can it as shown below.
52
52
 
53
53
  ```ruby
54
- employee = reynard.
54
+ response = reynard.
55
55
  operation('employeeByUuid').
56
56
  params(uuid: uuid).
57
57
  execute
@@ -60,12 +60,41 @@ employee = reynard.
60
60
  When an operation requires a body, you can add it as structured data.
61
61
 
62
62
  ```ruby
63
- employee = reynard.
63
+ response = reynard.
64
64
  operation('createEmployee').
65
65
  body(name: 'Sam Seven').
66
66
  execute
67
67
  ```
68
68
 
69
+ In case the response matches a response in the specification it will attempt to build an object using the specified schema.
70
+
71
+ ```ruby
72
+ response.object.name #=> 'Sam Seven'
73
+ ```
74
+
75
+ The response object shared much of its interface with `Net::HTTP::Response`.
76
+
77
+ ```ruby
78
+ response.code #=> '200'
79
+ response.content_type #=> 'application/json'
80
+ response['Content-Type'] #=> 'application/json'
81
+ response.body #=> '{"name":"Sam Seven"}'
82
+ ```
83
+
84
+ ## Mocking
85
+
86
+ You can mock Reynard requests by changing the HTTP implementation. The class **must** implement a single `request` method that accepts an URI and net/http request object. It **must** return a net/http response object or an object with the exact same interface.
87
+
88
+ ```ruby
89
+ Reynard.http = MyMock.new
90
+
91
+ class MyMock
92
+ def request(uri, net_http_request)
93
+ Net::HTTPResponse::CODE_TO_OBJ['404'].new('HTTP/1.1', '200', 'OK')
94
+ end
95
+ end
96
+ ```
97
+
69
98
  ## Copyright and other legal
70
99
 
71
100
  See LICENCE.
@@ -43,7 +43,7 @@ class Reynard
43
43
  end
44
44
 
45
45
  def execute
46
- build_object(build_request.perform)
46
+ build_response(build_request.perform)
47
47
  end
48
48
 
49
49
  private
@@ -63,32 +63,12 @@ class Reynard
63
63
  Reynard::Http::Request.new(request_context: @request_context)
64
64
  end
65
65
 
66
- def build_object(http_response)
67
- media_type = @specification.media_type(
68
- @request_context.operation.node,
69
- http_response.code,
70
- http_response.content_type
71
- )
72
- if media_type
73
- build_object_with_media_type(http_response, media_type)
74
- else
75
- build_object_without_media_type(http_response)
76
- end
77
- end
78
-
79
- def build_object_with_media_type(http_response, media_type)
80
- ObjectBuilder.new(
81
- media_type: media_type,
82
- schema: @specification.schema(media_type.node),
66
+ def build_response(http_response)
67
+ Reynard::Http::Response.new(
68
+ specification: @specification,
69
+ request_context: @request_context,
83
70
  http_response: http_response
84
- ).call
85
- end
86
-
87
- def build_object_without_media_type(http_response)
88
- # Try to parse the response as JSON and give up otherwise.
89
- OpenStruct.new(MultiJson.load(http_response.body))
90
- rescue StandardError
91
- http_response.body
71
+ )
92
72
  end
93
73
  end
94
74
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Reynard
4
+ class Http
5
+ # Wraps an HTTP response and returns an object when it can find a definition for the response
6
+ # in the specification.
7
+ class Response
8
+ extend Forwardable
9
+ def_delegators :@http_response, :code, :content_type, :[], :body
10
+
11
+ def initialize(specification:, request_context:, http_response:)
12
+ @specification = specification
13
+ @request_context = request_context
14
+ @http_response = http_response
15
+ end
16
+
17
+ # Instantiates an object based on the schema that fits the response.
18
+ def object
19
+ return @object if defined?(@object)
20
+
21
+ @object = build_object
22
+ end
23
+
24
+ private
25
+
26
+ def build_object
27
+ media_type = @specification.media_type(
28
+ @request_context.operation.node,
29
+ @http_response.code,
30
+ @http_response.content_type
31
+ )
32
+ if media_type
33
+ build_object_with_media_type(media_type)
34
+ else
35
+ build_object_without_media_type
36
+ end
37
+ end
38
+
39
+ def build_object_with_media_type(media_type)
40
+ ObjectBuilder.new(
41
+ media_type: media_type,
42
+ schema: @specification.schema(media_type.node),
43
+ http_response: @http_response
44
+ ).call
45
+ end
46
+
47
+ def build_object_without_media_type
48
+ # Try to parse the response as JSON and give up otherwise.
49
+ Reynard::Model.new(MultiJson.load(@http_response.body))
50
+ rescue StandardError
51
+ @http_response.body
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/reynard/http.rb CHANGED
@@ -3,5 +3,6 @@
3
3
  class Reynard
4
4
  class Http
5
5
  autoload :Request, 'reynard/http/request'
6
+ autoload :Response, 'reynard/http/response'
6
7
  end
7
8
  end
@@ -15,7 +15,7 @@ class Reynard
15
15
  if @media_type.schema_name
16
16
  self.class.model_class(@media_type.schema_name, @schema.object_type)
17
17
  else
18
- OpenStruct
18
+ Reynard::Model
19
19
  end
20
20
  end
21
21
 
@@ -23,7 +23,7 @@ class Reynard
23
23
  if @schema.item_schema_name
24
24
  self.class.model_class(@schema.item_schema_name, 'object')
25
25
  else
26
- OpenStruct
26
+ Reynard::Model
27
27
  end
28
28
  end
29
29
 
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rack'
4
+
3
5
  class Reynard
4
6
  # Wraps the YAML representation of an OpenAPI specification.
5
7
  class Specification
8
+ VERBS = %w[get put post delete options head patch trace].freeze
9
+
6
10
  def initialize(filename:)
7
11
  @filename = filename
8
12
  @data = read
@@ -30,7 +34,15 @@ class Reynard
30
34
  def build_grouped_params(operation_node, params)
31
35
  return {} unless params
32
36
 
33
- GroupedParameters.new(dig(*operation_node, 'parameters'), params).to_h
37
+ GroupedParameters.new(
38
+ [
39
+ # Parameters can be shared between methods on a path or be specific to an operation. The
40
+ # parameters on the operation level override those at the path level.
41
+ dig(*operation_node, 'parameters'),
42
+ dig(*operation_node[..-2], 'parameters')
43
+ ].compact.flatten,
44
+ params
45
+ ).to_h
34
46
  end
35
47
 
36
48
  # Returns a serialized body instance to serialize a request body and figure out the request
@@ -41,7 +53,7 @@ class Reynard
41
53
 
42
54
  def operation(operation_name)
43
55
  dig('paths').each do |path, operations|
44
- operations.each do |verb, operation|
56
+ operations.slice(*VERBS).each do |verb, operation|
45
57
  return Operation.new(node: ['paths', path, verb]) if operation_name == operation['operationId']
46
58
  end
47
59
  end
@@ -88,6 +100,15 @@ class Reynard
88
100
  false
89
101
  end
90
102
 
103
+ def self.normalize_model_name(name)
104
+ # 1. Unescape encoded characters to create an UTF-8 string
105
+ # 2. Replace all non-alphabetic characters with a space (not allowed in Ruby constant)
106
+ # 3. Camelcase
107
+ Rack::Utils.unescape_path(name)
108
+ .gsub(/[^[:alpha:]]/, ' ')
109
+ .gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
110
+ end
111
+
91
112
  private
92
113
 
93
114
  def read
@@ -105,7 +126,7 @@ class Reynard
105
126
  next unless cursor.respond_to?(:key?) && cursor&.key?('$ref')
106
127
 
107
128
  # We currenly only supply references inside the document starting with #/.
108
- path = cursor['$ref'][2..].split('/') + path
129
+ path = Rack::Utils.unescape_path(cursor['$ref'][2..]).split('/') + path
109
130
  cursor = data
110
131
  end
111
132
  cursor
@@ -113,16 +134,16 @@ class Reynard
113
134
 
114
135
  def schema_name(response)
115
136
  ref = response.dig('schema', '$ref')
116
- ref&.split('/')&.last
137
+ return unless ref
138
+
139
+ self.class.normalize_model_name(ref&.split('/')&.last)
117
140
  end
118
141
 
119
142
  def item_schema_name(schema)
120
143
  ref = schema.dig('items', '$ref')
121
- ref&.split('/')&.last
122
- end
144
+ return unless ref
123
145
 
124
- def object_name(_schema)
125
- 'Book'
146
+ self.class.normalize_model_name(ref&.split('/')&.last)
126
147
  end
127
148
  end
128
149
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Reynard
4
- VERSION = '0.0.8'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/reynard.rb CHANGED
@@ -34,6 +34,14 @@ class Reynard
34
34
  @specification = Specification.new(filename: filename)
35
35
  end
36
36
 
37
+ # Assign an object that follows Reynard's internal request interface to mock requests or use a
38
+ # different HTTP client.
39
+ class << self
40
+ attr_writer :http
41
+ end
42
+
43
+ # Returns Reynard's global request interface. This is a global object to allow persistent
44
+ # connections, caching, and other features that need a persistent object in the process.
37
45
  def self.http
38
46
  @http ||= begin
39
47
  http = Net::HTTP::Persistent.new(name: 'Reynard')
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.0.8
4
+ version: 0.3.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: 2021-11-01 00:00:00.000000000 Z
11
+ date: 2021-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json
@@ -124,6 +124,7 @@ files:
124
124
  - lib/reynard/grouped_parameters.rb
125
125
  - lib/reynard/http.rb
126
126
  - lib/reynard/http/request.rb
127
+ - lib/reynard/http/response.rb
127
128
  - lib/reynard/media_type.rb
128
129
  - lib/reynard/model.rb
129
130
  - lib/reynard/models.rb
@@ -139,7 +140,8 @@ files:
139
140
  homepage: https://github.com/Manfred/reynard
140
141
  licenses:
141
142
  - MIT
142
- metadata: {}
143
+ metadata:
144
+ rubygems_mfa_required: 'true'
143
145
  post_install_message:
144
146
  rdoc_options: []
145
147
  require_paths: