reynard 0.0.3 → 0.0.7

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: 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: []