openapi_kit 0.1.0.pre.1

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.
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/codec/contract"
5
+
6
+ module OpenAPIKit
7
+ module Codec
8
+ module Float
9
+ extend T::Sig
10
+ extend T::Generic
11
+ extend Contract
12
+
13
+ Value = type_template { { fixed: ::Float } }
14
+
15
+ sig { override.params(value: OpenAPIKit::Wire).returns(::Float) }
16
+ def self.from_wire(value)
17
+ return value.to_f if value.is_a?(::Numeric)
18
+
19
+ if value.is_a?(::String)
20
+ begin
21
+ return Kernel.Float(value)
22
+ rescue ArgumentError, TypeError
23
+ raise DecodeError.new("expected a number, got #{value.inspect}")
24
+ end
25
+ end
26
+
27
+ raise DecodeError.new("expected a number, got #{value.inspect}")
28
+ end
29
+
30
+ sig { override.params(value: ::Float).returns(OpenAPIKit::Wire) }
31
+ def self.to_wire(value) = value
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/codec/contract"
5
+
6
+ module OpenAPIKit
7
+ module Codec
8
+ module Integer
9
+ extend T::Sig
10
+ extend T::Generic
11
+ extend Contract
12
+
13
+ Value = type_template { { fixed: ::Integer } }
14
+
15
+ sig { override.params(value: OpenAPIKit::Wire).returns(::Integer) }
16
+ def self.from_wire(value)
17
+ return value if value.is_a?(::Integer)
18
+ return value.to_i if value.is_a?(::String) && value.match?(/\A[+-]?\d+\z/)
19
+
20
+ raise DecodeError.new("expected an integer, got #{value.inspect}")
21
+ end
22
+
23
+ sig { override.params(value: ::Integer).returns(OpenAPIKit::Wire) }
24
+ def self.to_wire(value) = value
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/codec/contract"
5
+
6
+ module OpenAPIKit
7
+ module Codec
8
+ module String
9
+ extend T::Sig
10
+ extend T::Generic
11
+ extend Contract
12
+
13
+ Value = type_template { { fixed: ::String } }
14
+
15
+ sig { override.params(value: OpenAPIKit::Wire).returns(::String) }
16
+ def self.from_wire(value)
17
+ return value if value.is_a?(::String)
18
+
19
+ raise DecodeError.new("expected a string, got #{value.inspect}")
20
+ end
21
+
22
+ sig { override.params(value: ::String).returns(OpenAPIKit::Wire) }
23
+ def self.to_wire(value) = value
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/codec/contract"
5
+
6
+ module OpenAPIKit
7
+ module Codec
8
+ module Uuid
9
+ extend T::Sig
10
+ extend T::Generic
11
+ extend Contract
12
+
13
+ Value = type_template { { fixed: ::String } }
14
+
15
+ PATTERN = T.let(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/, Regexp)
16
+
17
+ sig { override.params(value: OpenAPIKit::Wire).returns(::String) }
18
+ def self.from_wire(value)
19
+ return value if value.is_a?(::String) && value.match?(PATTERN)
20
+
21
+ raise DecodeError.new("expected a UUID, got #{value.inspect}")
22
+ end
23
+
24
+ sig { override.params(value: ::String).returns(OpenAPIKit::Wire) }
25
+ def self.to_wire(value) = value
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/codec/contract"
5
+ require "openapi_kit/codec/string"
6
+ require "openapi_kit/codec/integer"
7
+ require "openapi_kit/codec/float"
8
+ require "openapi_kit/codec/decimal"
9
+ require "openapi_kit/codec/boolean"
10
+ require "openapi_kit/codec/date_time"
11
+ require "openapi_kit/codec/date"
12
+ require "openapi_kit/codec/uuid"
13
+ require "openapi_kit/codec/byte"
@@ -0,0 +1,171 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "action_dispatch"
5
+
6
+ module OpenAPIKit
7
+ module Decode
8
+ extend T::Sig
9
+
10
+ sig do
11
+ type_parameters(:T)
12
+ .params(raw: T::Hash[String, T.untyped], key: String,
13
+ block: T.proc.params(value: T.untyped).returns(T.type_parameter(:T)))
14
+ .returns(T.type_parameter(:T))
15
+ end
16
+ def self.required(raw, key, &block)
17
+ raise DecodeError.new("is required", json_pointer: "/#{key}") unless raw.key?(key)
18
+
19
+ value = raw[key]
20
+ raise DecodeError.new("must not be null", json_pointer: "/#{key}") if value.nil?
21
+
22
+ at("/#{key}") { block.call(value) }
23
+ end
24
+
25
+ sig do
26
+ type_parameters(:T)
27
+ .params(raw: T::Hash[String, T.untyped], key: String,
28
+ block: T.proc.params(value: T.untyped).returns(T.type_parameter(:T)))
29
+ .returns(T.nilable(T.type_parameter(:T)))
30
+ end
31
+ def self.required_nullable(raw, key, &block)
32
+ raise DecodeError.new("is required", json_pointer: "/#{key}") unless raw.key?(key)
33
+
34
+ value = raw[key]
35
+ return nil if value.nil?
36
+
37
+ at("/#{key}") { block.call(value) }
38
+ end
39
+
40
+ sig do
41
+ type_parameters(:T)
42
+ .params(raw: T::Hash[String, T.untyped], key: String, fallback: T.type_parameter(:T),
43
+ block: T.proc.params(value: T.untyped).returns(T.type_parameter(:T)))
44
+ .returns(T.type_parameter(:T))
45
+ end
46
+ def self.defaulted(raw, key, fallback, &block)
47
+ value = raw[key]
48
+ return fallback if value.nil?
49
+
50
+ at("/#{key}") { block.call(value) }
51
+ end
52
+
53
+ sig do
54
+ type_parameters(:T)
55
+ .params(raw: T::Hash[String, T.untyped], key: String,
56
+ block: T.proc.params(value: T.untyped).returns(T.type_parameter(:T)))
57
+ .returns(T.nilable(T.type_parameter(:T)))
58
+ end
59
+ def self.optional(raw, key, &block)
60
+ value = raw[key]
61
+ return nil if value.nil?
62
+
63
+ at("/#{key}") { block.call(value) }
64
+ end
65
+
66
+ sig do
67
+ type_parameters(:T)
68
+ .params(raw: T::Hash[String, T.untyped], key: String,
69
+ block: T.proc.params(value: T.untyped).returns(T.type_parameter(:T)))
70
+ .returns(OpenAPIKit::Optional[T.nilable(T.type_parameter(:T))])
71
+ end
72
+ def self.optional_nullable(raw, key, &block)
73
+ return OpenAPIKit::ABSENT unless raw.key?(key)
74
+
75
+ value = raw[key]
76
+ return Present[T.nilable(T.type_parameter(:T))].new(nil) if value.nil?
77
+
78
+ Present[T.nilable(T.type_parameter(:T))].new(at("/#{key}") { block.call(value) })
79
+ end
80
+
81
+ sig do
82
+ type_parameters(:T)
83
+ .params(raw: T.untyped, block: T.proc.params(item: T.untyped).returns(T.type_parameter(:T)))
84
+ .returns(T::Array[T.type_parameter(:T)])
85
+ end
86
+ def self.each(raw, &block)
87
+ array(raw).each_with_index.map { |item, index| at("/#{index}") { block.call(item) } }
88
+ end
89
+
90
+ sig do
91
+ type_parameters(:T)
92
+ .params(raw: T.untyped, block: T.proc.params(item: T.untyped).returns(T.type_parameter(:T)))
93
+ .returns(T::Hash[String, T.type_parameter(:T)])
94
+ end
95
+ def self.values(raw, &block)
96
+ object(raw).to_h { |key, item| [key, at("/#{key}") { block.call(item) }] }
97
+ end
98
+
99
+ sig do
100
+ type_parameters(:T)
101
+ .params(prefix: String, block: T.proc.returns(T.type_parameter(:T)))
102
+ .returns(T.type_parameter(:T))
103
+ end
104
+ def self.at(prefix, &block)
105
+ block.call
106
+ rescue DecodeError => e
107
+ raise e.at(prefix)
108
+ rescue ArgumentError, TypeError => e
109
+ raise DecodeError.new(e.message, json_pointer: prefix)
110
+ end
111
+
112
+ sig do
113
+ params(names: T::Array[String],
114
+ lookup: T.proc.params(name: String).returns(T.untyped))
115
+ .returns(T::Hash[String, T.untyped])
116
+ end
117
+ def self.gather(names, &lookup)
118
+ names.each_with_object({}) do |name, found|
119
+ value = lookup.call(name)
120
+ found[name] = value unless value.nil?
121
+ end
122
+ end
123
+
124
+ sig { params(raw: T.untyped).returns(T::Hash[String, T.untyped]) }
125
+ def self.object(raw)
126
+ return raw if raw.is_a?(Hash)
127
+
128
+ raise DecodeError.new("expected an object, got #{raw.class}")
129
+ end
130
+
131
+ sig do
132
+ params(value: T.untyped, name: String,
133
+ candidates: T::Array[T.proc.params(value: T.untyped).returns(T.untyped)])
134
+ .returns(T.untyped)
135
+ end
136
+ def self.first_of(value, name, candidates)
137
+ candidates.each do |candidate|
138
+ return candidate.call(value)
139
+ rescue DecodeError
140
+ next
141
+ end
142
+
143
+ raise DecodeError.new("did not match any member of #{name}")
144
+ end
145
+
146
+ sig do
147
+ type_parameters(:Enum)
148
+ .params(enum: T.all(T::Class[T.type_parameter(:Enum)], T.class_of(T::Enum)), value: T.untyped)
149
+ .returns(T.type_parameter(:Enum))
150
+ end
151
+ def self.enum(enum, value)
152
+ enum.try_deserialize(value) ||
153
+ raise(DecodeError.new("expected one of #{enum.values.map(&:serialize).join(", ")}, " \
154
+ "got #{value.inspect}"))
155
+ end
156
+
157
+ sig { params(value: T.untyped).returns(::ActionDispatch::Http::UploadedFile) }
158
+ def self.file(value)
159
+ return value if value.is_a?(::ActionDispatch::Http::UploadedFile)
160
+
161
+ raise DecodeError.new("expected an uploaded file, got #{value.class}")
162
+ end
163
+
164
+ sig { params(raw: T.untyped).returns(T::Array[T.untyped]) }
165
+ def self.array(raw)
166
+ return raw if raw.is_a?(Array)
167
+
168
+ raise DecodeError.new("expected an array, got #{raw.class}")
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ class Error < StandardError; end
6
+
7
+ class ConfigError < Error; end
8
+
9
+ class SchemaError < Error; end
10
+
11
+ class SecurityError < Error; end
12
+
13
+ class DecodeError < Error
14
+ extend T::Sig
15
+
16
+ ROOT = T.let("", String)
17
+
18
+ sig { returns(String) }
19
+ attr_reader :json_pointer
20
+
21
+ sig { returns(String) }
22
+ attr_reader :detail
23
+
24
+ sig { params(detail: String, json_pointer: String).void }
25
+ def initialize(detail, json_pointer: ROOT)
26
+ @detail = detail
27
+ @json_pointer = json_pointer
28
+ super(json_pointer == ROOT ? detail : "#{json_pointer}: #{detail}")
29
+ end
30
+
31
+ sig { params(prefix: String).returns(DecodeError) }
32
+ def at(prefix) = DecodeError.new(detail, json_pointer: "#{prefix}#{json_pointer}")
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "action_dispatch"
5
+
6
+ require "openapi_kit/wire"
7
+
8
+ module OpenAPIKit
9
+ module Form
10
+ Parts = T.type_alias do
11
+ T::Hash[::String, T.any(OpenAPIKit::Wire, ::ActionDispatch::Http::UploadedFile)]
12
+ end
13
+
14
+ module Contract
15
+ extend T::Sig
16
+ extend T::Generic
17
+ interface!
18
+
19
+ Value = type_member
20
+
21
+ sig { abstract.params(parts: OpenAPIKit::Form::Parts).returns(Value) }
22
+ def from_parts(parts); end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,67 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Optional
6
+ extend T::Sig
7
+ extend T::Generic
8
+ include Kernel
9
+ abstract!
10
+ sealed!
11
+
12
+ Value = type_member
13
+
14
+ sig { abstract.params(fallback: Value).returns(Value) }
15
+ def value_or(fallback); end
16
+ end
17
+
18
+ class Present
19
+ extend T::Sig
20
+ extend T::Generic
21
+ include Optional
22
+
23
+ Value = type_member
24
+
25
+ sig { returns(Value) }
26
+ attr_reader :value
27
+
28
+ sig { params(value: Value).void }
29
+ def initialize(value)
30
+ @value = value
31
+ end
32
+
33
+ sig { override.params(fallback: Value).returns(Value) }
34
+ def value_or(fallback) = value
35
+
36
+ sig { params(other: T.untyped).returns(T::Boolean) }
37
+ def ==(other) = other.is_a?(Present) && T.unsafe(other).value == T.unsafe(self).value
38
+
39
+ sig { params(other: T.untyped).returns(T::Boolean) }
40
+ def eql?(other) = self == other
41
+
42
+ sig { returns(Integer) }
43
+ def hash = [Present, T.unsafe(self).value].hash
44
+ end
45
+
46
+ class Absent
47
+ extend T::Sig
48
+ extend T::Generic
49
+ include Optional
50
+
51
+ Value = type_member
52
+
53
+ sig { override.params(fallback: Value).returns(Value) }
54
+ def value_or(fallback) = fallback
55
+
56
+ sig { params(other: T.untyped).returns(T::Boolean) }
57
+ def ==(other) = other.is_a?(Absent)
58
+
59
+ sig { params(other: T.untyped).returns(T::Boolean) }
60
+ def eql?(other) = self == other
61
+
62
+ sig { returns(Integer) }
63
+ def hash = Absent.hash
64
+ end
65
+
66
+ ABSENT = T.let(Absent.new.freeze, Absent[T.untyped])
67
+ end
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/body"
5
+ require "openapi_kit/response"
6
+
7
+ module OpenAPIKit
8
+ module Rendering
9
+ extend T::Sig
10
+
11
+ Controller = T.type_alias do
12
+ T.all(::ActionController::Metal, ::ActionController::Head, ::ActionController::Rendering)
13
+ end
14
+
15
+ sig { params(result: OpenAPIKit::Response).void }
16
+ def render_response(result)
17
+ T.bind(self, Controller)
18
+
19
+ case (body = result.to_body)
20
+ when OpenAPIKit::Body::Empty
21
+ head(result.status)
22
+ when OpenAPIKit::Body::Json
23
+ render(json: body.wire, status: result.status, content_type: result.content_type)
24
+ when OpenAPIKit::Body::Stream
25
+ response.headers["Content-Type"] = result.content_type.to_s
26
+ response.status = result.status
27
+ self.response_body = body
28
+ when OpenAPIKit::Body::File
29
+ response.headers["Content-Type"] = result.content_type.to_s
30
+ response.headers["Content-Length"] = body.size.to_s
31
+ response.status = result.status
32
+ response.send_file(body.path.to_s)
33
+ else T.absurd(body)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/body"
5
+
6
+ module OpenAPIKit
7
+ module Response
8
+ extend T::Sig
9
+ extend T::Helpers
10
+ interface!
11
+
12
+ sig { abstract.returns(::Integer) }
13
+ def status; end
14
+
15
+ sig { abstract.returns(OpenAPIKit::Body) }
16
+ def to_body; end
17
+
18
+ sig { abstract.returns(T.nilable(::String)) }
19
+ def content_type; end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "openapi_kit/version"
7
+ require "openapi_kit/errors"
8
+ require "openapi_kit/wire"
9
+ require "openapi_kit/form"
10
+ require "openapi_kit/body"
11
+ require "openapi_kit/response"
12
+ require "openapi_kit/optional"
13
+ require "openapi_kit/codec"
14
+ require "openapi_kit/decode"
15
+ require "openapi_kit/security"
16
+ require "openapi_kit/rendering"
@@ -0,0 +1,122 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "action_dispatch"
5
+
6
+ module OpenAPIKit
7
+ module Security
8
+ extend T::Sig
9
+
10
+ sig do
11
+ type_parameters(:Principal)
12
+ .params(attempts: T::Array[T.proc.returns(T.nilable(T.type_parameter(:Principal)))])
13
+ .returns(T.nilable(T.type_parameter(:Principal)))
14
+ end
15
+ def self.first_of(attempts) = attempts.lazy.filter_map(&:call).first
16
+
17
+ sig { params(scheme: Scheme, request: ::ActionDispatch::Request).returns(T.nilable(String)) }
18
+ def self.credential(scheme, request)
19
+ case scheme
20
+ when ApiKey then api_key(scheme, request)
21
+ when Http then authorization(request, scheme.scheme)
22
+ when OAuth2, OpenIDConnect then authorization(request, "bearer")
23
+ else T.absurd(scheme)
24
+ end
25
+ end
26
+
27
+ sig { params(scheme: Http, request: ::ActionDispatch::Request).returns(T.nilable([String, String])) }
28
+ def self.basic(scheme, request)
29
+ encoded = credential(scheme, request)
30
+ return nil if encoded.nil?
31
+
32
+ decoded = begin
33
+ encoded.unpack1("m0")
34
+ rescue ArgumentError
35
+ nil
36
+ end
37
+ user, password = decoded.to_s.split(":", 2)
38
+ user.nil? || password.nil? ? nil : [user, password]
39
+ end
40
+
41
+ sig { params(scheme: ApiKey, request: ::ActionDispatch::Request).returns(T.nilable(String)) }
42
+ def self.api_key(scheme, request)
43
+ location = scheme.location
44
+ case location
45
+ when ApiKeyLocation::Header then present(request.headers[scheme.parameter_name])
46
+ when ApiKeyLocation::Query then present(request.query_parameters[scheme.parameter_name])
47
+ when ApiKeyLocation::Cookie then present(request.cookies[scheme.parameter_name])
48
+ else T.absurd(location)
49
+ end
50
+ end
51
+
52
+ sig { params(request: ::ActionDispatch::Request, scheme: String).returns(T.nilable(String)) }
53
+ def self.authorization(request, scheme)
54
+ header = present(request.headers["Authorization"])
55
+ return nil if header.nil?
56
+
57
+ prefix = "#{scheme} "
58
+ return nil unless header.downcase.start_with?(prefix.downcase)
59
+
60
+ present(header[prefix.length..])
61
+ end
62
+
63
+ sig { params(value: T.untyped).returns(T.nilable(String)) }
64
+ def self.present(value)
65
+ string = value.to_s
66
+ string.empty? ? nil : string
67
+ end
68
+
69
+ module Scheme
70
+ extend T::Sig
71
+ extend T::Helpers
72
+ include Kernel
73
+ sealed!
74
+
75
+ sig { params(scheme: Scheme).returns(String) }
76
+ def self.name_of(scheme)
77
+ case scheme
78
+ when ApiKey, Http, OAuth2, OpenIDConnect then scheme.name
79
+ else T.absurd(scheme)
80
+ end
81
+ end
82
+ end
83
+
84
+ class ApiKeyLocation < T::Enum
85
+ enums do
86
+ Query = new("query")
87
+ Header = new("header")
88
+ Cookie = new("cookie")
89
+ end
90
+ end
91
+
92
+ class ApiKey < T::Struct
93
+ include Scheme
94
+ const :name, String
95
+ const :location, ApiKeyLocation
96
+ const :parameter_name, String
97
+ const :extensions, T::Hash[String, T.untyped], default: {}
98
+ end
99
+
100
+ class Http < T::Struct
101
+ include Scheme
102
+ const :name, String
103
+ const :scheme, String
104
+ const :bearer_format, T.nilable(String), default: nil
105
+ const :extensions, T::Hash[String, T.untyped], default: {}
106
+ end
107
+
108
+ class OAuth2 < T::Struct
109
+ include Scheme
110
+ const :name, String
111
+ const :scopes, T::Hash[String, String], default: {}
112
+ const :extensions, T::Hash[String, T.untyped], default: {}
113
+ end
114
+
115
+ class OpenIDConnect < T::Struct
116
+ include Scheme
117
+ const :name, String
118
+ const :url, String
119
+ const :extensions, T::Hash[String, T.untyped], default: {}
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ VERSION = "0.1.0.pre.1"
6
+ end
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ # The parsed value model every supported media type leaves behind: application/json and
6
+ # +json, form-urlencoded, and a multipart body's text fields all arrive as these shapes,
7
+ # as do path, query and header values. A codec converts between one of them and a Ruby
8
+ # type, both ways. An uploaded file is the one thing outside it, so it never reaches a
9
+ # codec.
10
+ Wire = T.type_alias do
11
+ T.any(NilClass, String, Integer, Float, T::Boolean,
12
+ T::Array[T.untyped], T::Hash[String, T.untyped])
13
+ end
14
+ end
@@ -0,0 +1,4 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/runtime"