reynard 0.0.7 → 0.2.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: 464bcfde66e61c85b7dd949995fc3acd2db91048b80828bb4c8161c1bd5170d8
4
- data.tar.gz: e219398e59d21c45e42ece44f1cadcbdb6c0ef4b5a5927cb627bafa249138b6a
3
+ metadata.gz: 95ca8eb7744c1550bfcf705a92d97331d9c558b08dec5f2369b166b285b38edc
4
+ data.tar.gz: a140a0dbe5f74c95f43d2aa785dd3c96a322eab2893b2d8a629abd3b9859724e
5
5
  SHA512:
6
- metadata.gz: b3fb63f88b99b653541e92e948ba5e8cadb6edbbdb45bf6d939452d98663b7d40bf9782f5cb816665d847a0c807777e3604fef279430e30025e0228600c280e9
7
- data.tar.gz: 616c0ad91e77e8d69fa66d48000079cfc530563f276e85c4bdca96a83f17e9a273a8f88ff5d8d2b5bef60160d1241263f30a83fb3be2ff065ef87974d997fadd
6
+ metadata.gz: 77148ca6897e1e7b0479348cfe536ff94afc5c1741b6fe59afea7940b442d7c46a71914868f572f3588b7d85d95f38446bd8d28885a22a032c91a86e71e3da28
7
+ data.tar.gz: eab59f1738c99af0f2c8fba6a4fb09f15a5a26f31f86de91125d9f299d2d50844060bd3c13db33b260f267a82e269175e0c69d2cdeb4f32d397e99bca74e9694
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
@@ -26,6 +26,12 @@ class Reynard
26
26
  build_http_get
27
27
  when 'post'
28
28
  build_http_post
29
+ when 'put'
30
+ build_http_put
31
+ when 'patch'
32
+ build_http_patch
33
+ when 'delete'
34
+ build_http_delete
29
35
  end
30
36
  end
31
37
 
@@ -38,6 +44,22 @@ class Reynard
38
44
  post.body = @request_context.body
39
45
  post
40
46
  end
47
+
48
+ def build_http_put
49
+ put = Net::HTTP::Put.new(uri, @request_context.headers)
50
+ put.body = @request_context.body
51
+ put
52
+ end
53
+
54
+ def build_http_patch
55
+ patch = Net::HTTP::Patch.new(uri, @request_context.headers)
56
+ patch.body = @request_context.body
57
+ patch
58
+ end
59
+
60
+ def build_http_delete
61
+ Net::HTTP::Delete.new(uri, @request_context.headers)
62
+ end
41
63
  end
42
64
  end
43
65
  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
+ OpenStruct.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
@@ -1,5 +1,7 @@
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
@@ -88,11 +90,20 @@ class Reynard
88
90
  false
89
91
  end
90
92
 
93
+ def self.normalize_model_name(name)
94
+ # 1. Unescape encoded characters to create an UTF-8 string
95
+ # 2. Replace all non-alphabetic characters with a space (not allowed in Ruby constant)
96
+ # 3. Camelcase
97
+ Rack::Utils.unescape_path(name)
98
+ .gsub(/[^[:alpha:]]/, ' ')
99
+ .gsub(/(\s+)([[:alpha:]])/) { Regexp.last_match(2).upcase }
100
+ end
101
+
91
102
  private
92
103
 
93
104
  def read
94
105
  File.open(@filename, encoding: 'UTF-8') do |file|
95
- YAML.safe_load(file)
106
+ YAML.safe_load(file, aliases: true)
96
107
  end
97
108
  end
98
109
 
@@ -105,7 +116,7 @@ class Reynard
105
116
  next unless cursor.respond_to?(:key?) && cursor&.key?('$ref')
106
117
 
107
118
  # We currenly only supply references inside the document starting with #/.
108
- path = cursor['$ref'][2..].split('/') + path
119
+ path = Rack::Utils.unescape_path(cursor['$ref'][2..]).split('/') + path
109
120
  cursor = data
110
121
  end
111
122
  cursor
@@ -113,16 +124,16 @@ class Reynard
113
124
 
114
125
  def schema_name(response)
115
126
  ref = response.dig('schema', '$ref')
116
- ref&.split('/')&.last
127
+ return unless ref
128
+
129
+ self.class.normalize_model_name(ref&.split('/')&.last)
117
130
  end
118
131
 
119
132
  def item_schema_name(schema)
120
133
  ref = schema.dig('items', '$ref')
121
- ref&.split('/')&.last
122
- end
134
+ return unless ref
123
135
 
124
- def object_name(_schema)
125
- 'Book'
136
+ self.class.normalize_model_name(ref&.split('/')&.last)
126
137
  end
127
138
  end
128
139
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Reynard
4
- VERSION = '0.0.7'
4
+ VERSION = '0.2.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.7
4
+ version: 0.2.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-09-17 00:00:00.000000000 Z
11
+ date: 2021-11-16 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: