reynard 0.0.3 → 0.0.7

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: 1655a999a92e83993f4fdba62a9eedc870b18ada0339278d1c899e24f087b3bc
4
- data.tar.gz: 926420c57d99767386d9aea1ef50b5bda317ea437a11dc3646bf3a5cc8a902be
3
+ metadata.gz: 464bcfde66e61c85b7dd949995fc3acd2db91048b80828bb4c8161c1bd5170d8
4
+ data.tar.gz: e219398e59d21c45e42ece44f1cadcbdb6c0ef4b5a5927cb627bafa249138b6a
5
5
  SHA512:
6
- metadata.gz: 6c9834fae550aae2557a6833e4491eed9d426c9fdb5b6c2cf31ff5c9fe80569a00070affdfb76e9c90611400f959df5a7e377dfe906a813655e6b28bc3ced4ac
7
- data.tar.gz: a22f676bb82b37d6bceb87028aeae10a78f2fd394cdef990eea7661842d1b0a2644ac20e5188f29730c62d54ab36498b1fe7112ebc1ffe872b3f3b6dfa848aff
6
+ metadata.gz: b3fb63f88b99b653541e92e948ba5e8cadb6edbbdb45bf6d939452d98663b7d40bf9782f5cb816665d847a0c807777e3604fef279430e30025e0228600c280e9
7
+ data.tar.gz: 616c0ad91e77e8d69fa66d48000079cfc530563f276e85c4bdca96a83f17e9a273a8f88ff5d8d2b5bef60160d1241263f30a83fb3be2ff065ef87974d997fadd
data/README.md CHANGED
@@ -57,6 +57,15 @@ employee = reynard.
57
57
  execute
58
58
  ```
59
59
 
60
+ When an operation requires a body, you can add it as structured data.
61
+
62
+ ```ruby
63
+ employee = reynard.
64
+ operation('createEmployee').
65
+ body(name: 'Sam Seven').
66
+ execute
67
+ ```
68
+
60
69
  ## Copyright and other legal
61
70
 
62
71
  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
@@ -24,8 +26,20 @@ class Reynard
24
26
  copy(params: @specification.build_grouped_params(@request_context.operation.node, params))
25
27
  end
26
28
 
29
+ def body(data)
30
+ return unless @request_context.operation
31
+
32
+ serialized_body = @specification.build_body(@request_context.operation.node, data)
33
+ return unless serialized_body
34
+
35
+ copy(
36
+ headers: @request_context.headers.merge(serialized_body.headers),
37
+ body: serialized_body.to_s
38
+ )
39
+ end
40
+
27
41
  def headers(headers)
28
- copy(headers: headers)
42
+ copy(headers: @request_context.headers.merge(headers))
29
43
  end
30
44
 
31
45
  def execute
@@ -35,7 +49,7 @@ class Reynard
35
49
  private
36
50
 
37
51
  def build_request_context
38
- RequestContext.new(base_url: @specification.default_base_url)
52
+ RequestContext.new(base_url: @specification.default_base_url, headers: {})
39
53
  end
40
54
 
41
55
  def copy(**properties)
@@ -53,13 +67,28 @@ class Reynard
53
67
  media_type = @specification.media_type(
54
68
  @request_context.operation.node,
55
69
  http_response.code,
56
- http_response['Content-Type'].split(';').first
70
+ http_response.content_type
57
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)
58
80
  ObjectBuilder.new(
59
81
  media_type: media_type,
60
82
  schema: @specification.schema(media_type.node),
61
83
  http_response: http_response
62
84
  ).call
63
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
64
93
  end
65
94
  end
@@ -15,7 +15,7 @@ class Reynard
15
15
  end
16
16
 
17
17
  def perform
18
- build_http.request(build_request)
18
+ Reynard.http.request(uri, build_request)
19
19
  end
20
20
 
21
21
  private
@@ -29,12 +29,6 @@ class Reynard
29
29
  end
30
30
  end
31
31
 
32
- def build_http
33
- http = Net::HTTP.new(uri.hostname, uri.port)
34
- http.set_debug_output($stderr) if ENV['DEBUG']
35
- http
36
- end
37
-
38
32
  def build_http_get
39
33
  Net::HTTP::Get.new(uri, @request_context.headers)
40
34
  end
data/lib/reynard/model.rb CHANGED
@@ -16,10 +16,14 @@ class Reynard
16
16
  # Until we can set accessors based on the schema
17
17
  def method_missing(attribute_name, *)
18
18
  instance_variable_get("@#{attribute_name}")
19
+ rescue NameError
20
+ raise NoMethodError, "undefined method `#{attribute_name}' for #{inspect}"
19
21
  end
20
22
 
21
23
  def respond_to_missing?(attribute_name, *)
22
- !!instance_variable_get("@#{attribute_name}")
24
+ !instance_variable_get("@#{attribute_name}").nil?
25
+ rescue NameError
26
+ false
23
27
  end
24
28
  end
25
29
  end
@@ -11,28 +11,52 @@ class Reynard
11
11
  @http_response = http_response
12
12
  end
13
13
 
14
- # Object.const_set(@media_type.schema_name, Class.new(Reynard::Model))
15
14
  def object_class
16
15
  if @media_type.schema_name
17
- self.class.model_class(@media_type.schema_name)
16
+ self.class.model_class(@media_type.schema_name, @schema.object_type)
17
+ else
18
+ OpenStruct
19
+ end
20
+ end
21
+
22
+ def item_object_class
23
+ if @schema.item_schema_name
24
+ self.class.model_class(@schema.item_schema_name, 'object')
18
25
  else
19
26
  OpenStruct
20
27
  end
21
28
  end
22
29
 
23
30
  def call
24
- case @media_type.media_type
25
- when 'application/json'
26
- object_class.new(MultiJson.load(@http_response.body))
31
+ if @schema.object_type == 'array'
32
+ array = object_class.new
33
+ data.each { |attributes| array << item_object_class.new(attributes) }
34
+ array
27
35
  else
28
- FailedRequest.new
36
+ object_class.new(data)
29
37
  end
30
38
  end
31
39
 
32
- def self.model_class(name)
40
+ def data
41
+ @data ||= MultiJson.load(@http_response.body)
42
+ end
43
+
44
+ def self.model_class(name, object_type)
45
+ model_class_get(name) || model_class_set(name, object_type)
46
+ end
47
+
48
+ def self.model_class_get(name)
33
49
  Reynard::Models.const_get(name)
34
50
  rescue NameError
35
- Reynard::Models.const_set(name, Class.new(Reynard::Model))
51
+ nil
52
+ end
53
+
54
+ def self.model_class_set(name, object_type)
55
+ if object_type == 'array'
56
+ Reynard::Models.const_set(name, Class.new(Array))
57
+ else
58
+ Reynard::Models.const_set(name, Class.new(Reynard::Model))
59
+ end
36
60
  end
37
61
  end
38
62
  end
@@ -7,6 +7,7 @@ class Reynard
7
7
  :operation,
8
8
  :headers,
9
9
  :params,
10
+ :body,
10
11
  keyword_init: true
11
12
  ) do
12
13
  def verb
@@ -3,11 +3,12 @@
3
3
  class Reynard
4
4
  # Holds reference and object type for a schema in the API specification.
5
5
  class Schema
6
- attr_reader :node, :object_type
6
+ attr_reader :node, :object_type, :item_schema_name
7
7
 
8
- def initialize(node:, object_type:)
8
+ def initialize(node:, object_type:, item_schema_name:)
9
9
  @node = node
10
10
  @object_type = object_type
11
+ @item_schema_name = item_schema_name
11
12
  end
12
13
  end
13
14
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Reynard
4
+ # Serializes a request body and returns headers appropriate for the request.
5
+ class SerializedBody
6
+ def initialize(content, data)
7
+ @content = content
8
+ @data = data
9
+ end
10
+
11
+ def content_type
12
+ 'application/json'
13
+ end
14
+
15
+ def headers
16
+ { 'Content-Type' => content_type }
17
+ end
18
+
19
+ def to_s
20
+ MultiJson.dump(@data)
21
+ end
22
+ end
23
+ end
@@ -33,6 +33,12 @@ class Reynard
33
33
  GroupedParameters.new(dig(*operation_node, 'parameters'), params).to_h
34
34
  end
35
35
 
36
+ # Returns a serialized body instance to serialize a request body and figure out the request
37
+ # headers.
38
+ def build_body(operation_node, data)
39
+ SerializedBody.new(dig(*operation_node, 'requestBody', 'content'), data)
40
+ end
41
+
36
42
  def operation(operation_name)
37
43
  dig('paths').each do |path, operations|
38
44
  operations.each do |verb, operation|
@@ -44,8 +50,8 @@ class Reynard
44
50
 
45
51
  def media_type(operation_node, response_code, media_type)
46
52
  responses = dig(*operation_node, 'responses')
47
- response_code = responses.key?(response_code) ? response_code : 'default'
48
- response = responses.dig(response_code, 'content', media_type)
53
+ response_code = 'default' unless responses.key?(response_code)
54
+ response, media_type = media_type_response(responses, response_code, media_type)
49
55
  return unless response
50
56
 
51
57
  MediaType.new(
@@ -54,11 +60,32 @@ class Reynard
54
60
  )
55
61
  end
56
62
 
63
+ def media_type_response(responses, response_code, media_type)
64
+ defined_responses = responses.dig(response_code, 'content')
65
+ return unless defined_responses&.any?
66
+
67
+ defined_responses.each do |expression, response|
68
+ return response, expression if self.class.media_type_matches?(media_type, expression)
69
+ end
70
+ nil
71
+ end
72
+
57
73
  def schema(media_type_node)
58
74
  schema = dig(*media_type_node, 'schema')
59
75
  return unless schema
60
76
 
61
- Schema.new(node: [*media_type_node, 'schema'], object_type: schema['type'])
77
+ Schema.new(
78
+ node: [*media_type_node, 'schema'],
79
+ object_type: schema['type'],
80
+ item_schema_name: item_schema_name(schema)
81
+ )
82
+ end
83
+
84
+ def self.media_type_matches?(media_type, expression)
85
+ return true unless media_type
86
+ return true if expression == media_type
87
+
88
+ false
62
89
  end
63
90
 
64
91
  private
@@ -89,6 +116,11 @@ class Reynard
89
116
  ref&.split('/')&.last
90
117
  end
91
118
 
119
+ def item_schema_name(schema)
120
+ ref = schema.dig('items', '$ref')
121
+ ref&.split('/')&.last
122
+ end
123
+
92
124
  def object_name(_schema)
93
125
  'Book'
94
126
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Reynard
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.7'
5
5
  end
data/lib/reynard.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'net/http/persistent'
3
4
  require 'forwardable'
4
5
  require 'multi_json'
5
6
  require 'rack'
@@ -14,6 +15,7 @@ class Reynard
14
15
  def_delegators :@specification, :servers
15
16
 
16
17
  autoload :Context, 'reynard/context'
18
+ autoload :GroupedParameters, 'reynard/grouped_parameters'
17
19
  autoload :Http, 'reynard/http'
18
20
  autoload :MediaType, 'reynard/media_type'
19
21
  autoload :Model, 'reynard/model'
@@ -22,16 +24,24 @@ class Reynard
22
24
  autoload :Operation, 'reynard/operation'
23
25
  autoload :RequestContext, 'reynard/request_context'
24
26
  autoload :Schema, 'reynard/schema'
27
+ autoload :SerializedBody, 'reynard/serialized_body'
25
28
  autoload :Server, 'reynard/server'
26
29
  autoload :Specification, 'reynard/specification'
27
30
  autoload :Template, 'reynard/template'
28
- autoload :GroupedParameters, 'reynard/grouped_parameters'
29
31
  autoload :VERSION, 'reynard/version'
30
32
 
31
33
  def initialize(filename:)
32
34
  @specification = Specification.new(filename: filename)
33
35
  end
34
36
 
37
+ def self.http
38
+ @http ||= begin
39
+ http = Net::HTTP::Persistent.new(name: 'Reynard')
40
+ http.debug_output = $stderr if ENV['DEBUG']
41
+ http
42
+ end
43
+ end
44
+
35
45
  private
36
46
 
37
47
  def build_context
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.3
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manfred Stienstra
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-05 00:00:00.000000000 Z
11
+ date: 2021-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-http-persistent
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rack
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,20 @@ dependencies:
80
94
  - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webrick
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
83
111
  description: |2
84
112
  Reynard is an OpenAPI client for Ruby. It operates directly on the OpenAPI specification without
85
113
  the need to generate any source code.
@@ -103,6 +131,7 @@ files:
103
131
  - lib/reynard/operation.rb
104
132
  - lib/reynard/request_context.rb
105
133
  - lib/reynard/schema.rb
134
+ - lib/reynard/serialized_body.rb
106
135
  - lib/reynard/server.rb
107
136
  - lib/reynard/specification.rb
108
137
  - lib/reynard/template.rb
@@ -111,7 +140,7 @@ homepage: https://github.com/Manfred/reynard
111
140
  licenses:
112
141
  - MIT
113
142
  metadata: {}
114
- post_install_message:
143
+ post_install_message:
115
144
  rdoc_options: []
116
145
  require_paths:
117
146
  - lib
@@ -126,8 +155,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
155
  - !ruby/object:Gem::Version
127
156
  version: '0'
128
157
  requirements: []
129
- rubygems_version: 3.2.22
130
- signing_key:
158
+ rubygems_version: 3.1.6
159
+ signing_key:
131
160
  specification_version: 4
132
161
  summary: Minimal OpenAPI client.
133
162
  test_files: []