reynard 0.0.6 → 0.1.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: e0d45ac57b8de184f9386bde8d78ccd4cea04f430f809f19c5af6f64ef77ec63
4
- data.tar.gz: 6da06eac247b2fdc1f88ad8bfa76f11cf85d2c6852bec1d26ed73cb82dfb1edf
3
+ metadata.gz: d703f01c7c60de48b48ae223cd9c2e3a94a8639bb866eca234298331ad746d2a
4
+ data.tar.gz: 9e88b9e21dc1b1d00357fcf8bdf2b6952676c5237369d7d950e97b379727e731
5
5
  SHA512:
6
- metadata.gz: e9813aac2ddb3316fc4048bf8f058365f5549fb096b8241488092a1124915810e15cd9e49a713f7a3a979f2c2bf88e11ce34fea41e3cf24046bd44143cd1d665
7
- data.tar.gz: 77f1d12245ef452b312196e00918a60cfcca452fdb5d5658832406e370c5628e660813faf756b3404e0705dfe9e6b90f59248a1b35273d8b4dcafbe79ba66640
6
+ metadata.gz: b1cccaf532a955569236a07c5c8861519cf58c0c2abe3cdf218a8a57619a41d650e9f7226ccdb1a6ef1fddfe412cd9780032dcf8f68ecf995cd2176bb8774642
7
+ data.tar.gz: 56acfbbabfea8a77e76cbee676bd0b397a608994f03f774022fd54d4e65fad06730de36aa9b122767edddb229f854eff9e8819d8a93d506ee5af66d799d18e82
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)
@@ -66,6 +66,20 @@ employee = reynard.
66
66
  execute
67
67
  ```
68
68
 
69
+ ## Mocking
70
+
71
+ 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.
72
+
73
+ ```ruby
74
+ Reynard.http = MyMock.new
75
+
76
+ class MyMock
77
+ def request(uri, net_http_request)
78
+ Net::HTTPResponse::CODE_TO_OBJ['404'].new('HTTP/1.1', '200', 'OK')
79
+ end
80
+ end
81
+ ```
82
+
69
83
  ## Copyright and other legal
70
84
 
71
85
  See LICENCE.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ostruct'
4
+
3
5
  class Reynard
4
6
  # Exposes a public interface to build a request context.
5
7
  class Context
@@ -67,11 +69,26 @@ class Reynard
67
69
  http_response.code,
68
70
  http_response.content_type
69
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)
70
80
  ObjectBuilder.new(
71
81
  media_type: media_type,
72
82
  schema: @specification.schema(media_type.node),
73
83
  http_response: http_response
74
84
  ).call
75
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
92
+ end
76
93
  end
77
94
  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
@@ -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
@@ -61,7 +63,10 @@ class Reynard
61
63
  end
62
64
 
63
65
  def media_type_response(responses, response_code, media_type)
64
- responses.dig(response_code, 'content').each do |expression, response|
66
+ defined_responses = responses.dig(response_code, 'content')
67
+ return unless defined_responses&.any?
68
+
69
+ defined_responses.each do |expression, response|
65
70
  return response, expression if self.class.media_type_matches?(media_type, expression)
66
71
  end
67
72
  nil
@@ -85,11 +90,20 @@ class Reynard
85
90
  false
86
91
  end
87
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
+
88
102
  private
89
103
 
90
104
  def read
91
105
  File.open(@filename, encoding: 'UTF-8') do |file|
92
- YAML.safe_load(file)
106
+ YAML.safe_load(file, aliases: true)
93
107
  end
94
108
  end
95
109
 
@@ -102,7 +116,7 @@ class Reynard
102
116
  next unless cursor.respond_to?(:key?) && cursor&.key?('$ref')
103
117
 
104
118
  # We currenly only supply references inside the document starting with #/.
105
- path = cursor['$ref'][2..].split('/') + path
119
+ path = Rack::Utils.unescape_path(cursor['$ref'][2..]).split('/') + path
106
120
  cursor = data
107
121
  end
108
122
  cursor
@@ -110,16 +124,16 @@ class Reynard
110
124
 
111
125
  def schema_name(response)
112
126
  ref = response.dig('schema', '$ref')
113
- ref&.split('/')&.last
127
+ return unless ref
128
+
129
+ self.class.normalize_model_name(ref&.split('/')&.last)
114
130
  end
115
131
 
116
132
  def item_schema_name(schema)
117
133
  ref = schema.dig('items', '$ref')
118
- ref&.split('/')&.last
119
- end
134
+ return unless ref
120
135
 
121
- def object_name(_schema)
122
- 'Book'
136
+ self.class.normalize_model_name(ref&.split('/')&.last)
123
137
  end
124
138
  end
125
139
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Reynard
4
- VERSION = '0.0.6'
4
+ VERSION = '0.1.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.6
4
+ version: 0.1.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-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json