openapi_kit-codegen 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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +385 -0
  4. data/exe/openapi_kit +6 -0
  5. data/lib/openapi_kit/codegen/cli.rb +72 -0
  6. data/lib/openapi_kit/codegen/config.rb +114 -0
  7. data/lib/openapi_kit/codegen/emit/buffer.rb +92 -0
  8. data/lib/openapi_kit/codegen/emit/codecs.rb +221 -0
  9. data/lib/openapi_kit/codegen/emit/controllers.rb +263 -0
  10. data/lib/openapi_kit/codegen/emit/decode.rb +43 -0
  11. data/lib/openapi_kit/codegen/emit/defaults.rb +42 -0
  12. data/lib/openapi_kit/codegen/emit/emitter.rb +17 -0
  13. data/lib/openapi_kit/codegen/emit/forms.rb +56 -0
  14. data/lib/openapi_kit/codegen/emit/handlers.rb +65 -0
  15. data/lib/openapi_kit/codegen/emit/literal.rb +28 -0
  16. data/lib/openapi_kit/codegen/emit/operations.rb +277 -0
  17. data/lib/openapi_kit/codegen/emit/registry.rb +101 -0
  18. data/lib/openapi_kit/codegen/emit/routes.rb +77 -0
  19. data/lib/openapi_kit/codegen/emit/security.rb +183 -0
  20. data/lib/openapi_kit/codegen/emit/source_file.rb +30 -0
  21. data/lib/openapi_kit/codegen/emit/types.rb +153 -0
  22. data/lib/openapi_kit/codegen/generator.rb +48 -0
  23. data/lib/openapi_kit/codegen/loader.rb +730 -0
  24. data/lib/openapi_kit/codegen/model/document.rb +302 -0
  25. data/lib/openapi_kit/codegen/model/schema.rb +110 -0
  26. data/lib/openapi_kit/codegen/model/type_def.rb +89 -0
  27. data/lib/openapi_kit/codegen/naming.rb +78 -0
  28. data/lib/openapi_kit/codegen/ruby_type.rb +48 -0
  29. data/lib/openapi_kit/codegen/type_registry.rb +325 -0
  30. data/lib/openapi_kit/codegen/writer.rb +92 -0
  31. data/lib/openapi_kit/codegen.rb +37 -0
  32. data/lib/openapi_kit-codegen.rb +4 -0
  33. metadata +118 -0
@@ -0,0 +1,302 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Codegen
6
+ module Model
7
+ class HttpMethod < T::Enum
8
+ enums do
9
+ Get = new("get")
10
+ Put = new("put")
11
+ Post = new("post")
12
+ Delete = new("delete")
13
+ Options = new("options")
14
+ Head = new("head")
15
+ Patch = new("patch")
16
+ Trace = new("trace")
17
+ end
18
+ end
19
+
20
+ class PathStyle < T::Enum
21
+ enums do
22
+ Simple = new("simple")
23
+ Label = new("label")
24
+ Matrix = new("matrix")
25
+ end
26
+ end
27
+
28
+ class QueryStyle < T::Enum
29
+ enums do
30
+ Form = new("form")
31
+ SpaceDelimited = new("spaceDelimited")
32
+ PipeDelimited = new("pipeDelimited")
33
+ DeepObject = new("deepObject")
34
+ end
35
+ end
36
+
37
+ class ParameterInfo < T::Struct
38
+ const :name, String
39
+ const :identifier, Symbol
40
+ const :schema, Schema
41
+ const :description, T.nilable(String), default: nil
42
+ const :deprecated, T::Boolean, default: false
43
+ end
44
+
45
+ module Parameter
46
+ extend T::Sig
47
+ extend T::Helpers
48
+ include Kernel
49
+ sealed!
50
+
51
+ sig { params(parameter: Parameter).returns(ParameterInfo) }
52
+ def self.info(parameter)
53
+ case parameter
54
+ when PathParameter, QueryParameter, HeaderParameter, CookieParameter then parameter.info
55
+ else T.absurd(parameter)
56
+ end
57
+ end
58
+
59
+ sig { params(parameter: Parameter).returns(T::Boolean) }
60
+ def self.required?(parameter)
61
+ case parameter
62
+ when PathParameter then true
63
+ when QueryParameter, HeaderParameter, CookieParameter then parameter.required
64
+ else T.absurd(parameter)
65
+ end
66
+ end
67
+ end
68
+
69
+ class PathParameter < T::Struct
70
+ include Parameter
71
+ const :info, ParameterInfo
72
+ const :style, PathStyle, default: PathStyle::Simple
73
+ const :explode, T::Boolean, default: false
74
+ end
75
+
76
+ class QueryParameter < T::Struct
77
+ include Parameter
78
+ const :info, ParameterInfo
79
+ const :required, T::Boolean, default: false
80
+ const :style, QueryStyle, default: QueryStyle::Form
81
+ const :explode, T::Boolean, default: true
82
+ const :allow_reserved, T::Boolean, default: false
83
+ end
84
+
85
+ class HeaderParameter < T::Struct
86
+ include Parameter
87
+ const :info, ParameterInfo
88
+ const :required, T::Boolean, default: false
89
+ const :explode, T::Boolean, default: false
90
+ end
91
+
92
+ class CookieParameter < T::Struct
93
+ include Parameter
94
+ const :info, ParameterInfo
95
+ const :required, T::Boolean, default: false
96
+ const :explode, T::Boolean, default: true
97
+ end
98
+
99
+ class Content < T::Struct
100
+ extend T::Sig
101
+
102
+ const :media_type, String
103
+ const :schema, T.nilable(Schema), default: nil
104
+
105
+ sig { params(media_type: String).returns(T::Boolean) }
106
+ def self.multipart?(media_type)
107
+ T.must(media_type.split(";").first).strip.downcase == "multipart/form-data"
108
+ end
109
+
110
+ sig { returns(T::Boolean) }
111
+ def multipart? = Content.multipart?(media_type)
112
+ end
113
+
114
+ class RequestBody < T::Struct
115
+ const :contents, T::Array[Content]
116
+ const :required, T::Boolean, default: false
117
+ const :description, T.nilable(String), default: nil
118
+ end
119
+
120
+ class Header < T::Struct
121
+ const :name, String
122
+ const :identifier, Symbol
123
+ const :schema, Schema
124
+ const :required, T::Boolean, default: false
125
+ const :description, T.nilable(String), default: nil
126
+ end
127
+
128
+ module Status
129
+ extend T::Sig
130
+ extend T::Helpers
131
+ include Kernel
132
+ sealed!
133
+
134
+ NAMES = T.let(
135
+ {
136
+ 200 => "Ok", 201 => "Created", 202 => "Accepted", 203 => "NonAuthoritativeInformation",
137
+ 204 => "NoContent", 205 => "ResetContent", 206 => "PartialContent",
138
+ 301 => "MovedPermanently", 302 => "Found", 303 => "SeeOther", 304 => "NotModified",
139
+ 307 => "TemporaryRedirect", 308 => "PermanentRedirect",
140
+ 400 => "BadRequest", 401 => "Unauthorized", 402 => "PaymentRequired", 403 => "Forbidden",
141
+ 404 => "NotFound", 405 => "MethodNotAllowed", 406 => "NotAcceptable", 409 => "Conflict",
142
+ 410 => "Gone", 412 => "PreconditionFailed", 413 => "ContentTooLarge",
143
+ 415 => "UnsupportedMediaType", 418 => "ImATeapot", 422 => "UnprocessableContent",
144
+ 425 => "TooEarly", 428 => "PreconditionRequired", 429 => "TooManyRequests",
145
+ 500 => "InternalServerError", 501 => "NotImplemented", 502 => "BadGateway",
146
+ 503 => "ServiceUnavailable", 504 => "GatewayTimeout"
147
+ }.freeze,
148
+ T::Hash[Integer, String]
149
+ )
150
+
151
+ sig { params(status: Status).returns(String) }
152
+ def self.constant(status)
153
+ case status
154
+ when StatusCode then NAMES[status.code] || "Status#{status.code}"
155
+ when StatusRange then "Status#{status.hundreds}xx"
156
+ when DefaultStatus then "Default"
157
+ else T.absurd(status)
158
+ end
159
+ end
160
+
161
+ sig { params(raw: String).returns(Status) }
162
+ def self.parse(raw)
163
+ return DefaultStatus.new if raw == "default"
164
+ return StatusRange.new(hundreds: T.must(raw[0]).to_i) if raw.match?(/\A[1-5]XX\z/i)
165
+ return StatusCode.new(code: raw.to_i) if raw.match?(/\A[1-5]\d\d\z/)
166
+
167
+ raise SchemaError,
168
+ "#{raw.inspect} is not a valid response status. Use a three-digit code " \
169
+ "(200), a range (4XX) or `default`."
170
+ end
171
+ end
172
+
173
+ module SecurityScheme
174
+ extend T::Sig
175
+ extend T::Helpers
176
+ include Kernel
177
+ sealed!
178
+
179
+ sig { params(scheme: SecurityScheme).returns(String) }
180
+ def self.name_of(scheme)
181
+ case scheme
182
+ when ApiKeyScheme, HttpScheme, OAuth2Scheme, OpenIDConnectScheme then scheme.name
183
+ else T.absurd(scheme)
184
+ end
185
+ end
186
+
187
+ sig { params(scheme: SecurityScheme).returns(T::Hash[String, T.untyped]) }
188
+ def self.extensions_of(scheme)
189
+ case scheme
190
+ when ApiKeyScheme, HttpScheme, OAuth2Scheme, OpenIDConnectScheme then scheme.extensions
191
+ else T.absurd(scheme)
192
+ end
193
+ end
194
+ end
195
+
196
+ class StatusCode < T::Struct
197
+ include Status
198
+ const :code, Integer
199
+ end
200
+
201
+ class StatusRange < T::Struct
202
+ include Status
203
+ const :hundreds, Integer
204
+ end
205
+
206
+ class DefaultStatus < T::Struct
207
+ include Status
208
+ end
209
+
210
+ class Response < T::Struct
211
+ const :status, Status
212
+ const :contents, T::Array[Content]
213
+ const :headers, T::Array[Header], default: []
214
+ const :description, T.nilable(String), default: nil
215
+ end
216
+
217
+ class SecurityRequirement < T::Struct
218
+ extend T::Sig
219
+ const :schemes, T::Hash[String, T::Array[String]]
220
+
221
+ sig { returns(T::Boolean) }
222
+ def anonymous? = schemes.empty?
223
+ end
224
+
225
+ class ApiKeyLocation < T::Enum
226
+ enums do
227
+ Query = new("query")
228
+ Header = new("header")
229
+ Cookie = new("cookie")
230
+ end
231
+ end
232
+
233
+ class ApiKeyScheme < T::Struct
234
+ include SecurityScheme
235
+ const :name, String
236
+ const :location, ApiKeyLocation
237
+ const :parameter_name, String
238
+ const :description, T.nilable(String), default: nil
239
+ const :extensions, T::Hash[String, T.untyped], default: {}
240
+ end
241
+
242
+ class HttpScheme < T::Struct
243
+ include SecurityScheme
244
+ const :name, String
245
+ const :scheme, String
246
+ const :bearer_format, T.nilable(String), default: nil
247
+ const :description, T.nilable(String), default: nil
248
+ const :extensions, T::Hash[String, T.untyped], default: {}
249
+ end
250
+
251
+ class OAuth2Scheme < T::Struct
252
+ include SecurityScheme
253
+ const :name, String
254
+ const :scopes, T::Hash[String, String], default: {}
255
+ const :description, T.nilable(String), default: nil
256
+ const :extensions, T::Hash[String, T.untyped], default: {}
257
+ end
258
+
259
+ class OpenIDConnectScheme < T::Struct
260
+ include SecurityScheme
261
+ const :name, String
262
+ const :url, String
263
+ const :description, T.nilable(String), default: nil
264
+ const :extensions, T::Hash[String, T.untyped], default: {}
265
+ end
266
+
267
+ class Operation < T::Struct
268
+ extend T::Sig
269
+ const :id, String
270
+ const :http_method, HttpMethod
271
+ const :path, String
272
+ const :tag, String
273
+ const :parameters, T::Array[Parameter], default: []
274
+ const :request_body, T.nilable(RequestBody), default: nil
275
+ const :responses, T::Array[Response], default: []
276
+ const :security, T.nilable(T::Array[SecurityRequirement]), default: nil
277
+ const :summary, T.nilable(String), default: nil
278
+ const :description, T.nilable(String), default: nil
279
+ const :deprecated, T::Boolean, default: false
280
+ const :extensions, T::Hash[String, T.untyped], default: {}
281
+ end
282
+
283
+ class Document < T::Struct
284
+ extend T::Sig
285
+ const :title, String
286
+ const :version, String
287
+ const :types, T::Array[TypeDef], default: []
288
+ const :operations, T::Array[Operation], default: []
289
+ const :security_schemes, T::Array[SecurityScheme], default: []
290
+ const :security, T::Array[SecurityRequirement], default: []
291
+
292
+ # An operation that declares no security inherits the document's.
293
+ sig { params(operation: Operation).returns(T::Array[SecurityRequirement]) }
294
+ def security_for(operation) = operation.security || security
295
+ const :raw, T::Hash[String, T.untyped], default: {}
296
+
297
+ sig { returns(T::Array[String]) }
298
+ def tags = operations.map(&:tag).uniq
299
+ end
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Codegen
6
+ module Model
7
+ class Default < T::Struct
8
+ const :value, T.untyped
9
+ end
10
+
11
+ class Meta < T::Struct
12
+ const :description, T.nilable(String), default: nil
13
+ const :nullable, T::Boolean, default: false
14
+ const :deprecated, T::Boolean, default: false
15
+ const :default, T.nilable(Default), default: nil
16
+ const :read_only, T::Boolean, default: false
17
+ const :write_only, T::Boolean, default: false
18
+ const :extensions, T::Hash[String, T.untyped], default: {}
19
+ const :ruby_type, T.nilable(RubyType), default: nil
20
+ end
21
+
22
+ module Schema
23
+ extend T::Sig
24
+ extend T::Helpers
25
+ include Kernel
26
+ sealed!
27
+
28
+ sig { params(schema: Schema).returns(Meta) }
29
+ def self.meta(schema)
30
+ case schema
31
+ when Ref, StringSchema, IntegerSchema, NumberSchema, BooleanSchema, List, Freeform, Untyped
32
+ schema.meta
33
+ else T.absurd(schema)
34
+ end
35
+ end
36
+
37
+ sig { params(schema: Schema).returns(T::Boolean) }
38
+ def self.file?(schema)
39
+ return false unless schema.is_a?(StringSchema) && schema.format == "binary"
40
+
41
+ meta(schema).ruby_type.nil?
42
+ end
43
+ end
44
+
45
+ class Ref < T::Struct
46
+ include Schema
47
+ const :name, String
48
+ const :meta, Meta, factory: -> { Meta.new }
49
+ end
50
+
51
+ class StringSchema < T::Struct
52
+ include Schema
53
+ const :format, T.nilable(String), default: nil
54
+ const :min_length, T.nilable(Integer), default: nil
55
+ const :max_length, T.nilable(Integer), default: nil
56
+ const :pattern, T.nilable(String), default: nil
57
+ const :meta, Meta, factory: -> { Meta.new }
58
+ end
59
+
60
+ class IntegerSchema < T::Struct
61
+ include Schema
62
+ const :format, T.nilable(String), default: nil
63
+ const :minimum, T.nilable(Integer), default: nil
64
+ const :maximum, T.nilable(Integer), default: nil
65
+ const :exclusive_minimum, T::Boolean, default: false
66
+ const :exclusive_maximum, T::Boolean, default: false
67
+ const :multiple_of, T.nilable(Numeric), default: nil
68
+ const :meta, Meta, factory: -> { Meta.new }
69
+ end
70
+
71
+ class NumberSchema < T::Struct
72
+ include Schema
73
+ const :format, T.nilable(String), default: nil
74
+ const :minimum, T.nilable(Numeric), default: nil
75
+ const :maximum, T.nilable(Numeric), default: nil
76
+ const :exclusive_minimum, T::Boolean, default: false
77
+ const :exclusive_maximum, T::Boolean, default: false
78
+ const :multiple_of, T.nilable(Numeric), default: nil
79
+ const :meta, Meta, factory: -> { Meta.new }
80
+ end
81
+
82
+ class BooleanSchema < T::Struct
83
+ include Schema
84
+ const :meta, Meta, factory: -> { Meta.new }
85
+ end
86
+
87
+ class List < T::Struct
88
+ include Schema
89
+ const :items, Schema
90
+ const :min_items, T.nilable(Integer), default: nil
91
+ const :max_items, T.nilable(Integer), default: nil
92
+ const :unique_items, T::Boolean, default: false
93
+ const :meta, Meta, factory: -> { Meta.new }
94
+ end
95
+
96
+ class Freeform < T::Struct
97
+ include Schema
98
+ const :values, T.nilable(Schema), default: nil
99
+ const :min_properties, T.nilable(Integer), default: nil
100
+ const :max_properties, T.nilable(Integer), default: nil
101
+ const :meta, Meta, factory: -> { Meta.new }
102
+ end
103
+
104
+ class Untyped < T::Struct
105
+ include Schema
106
+ const :meta, Meta, factory: -> { Meta.new }
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,89 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Codegen
6
+ module Model
7
+ class Property < T::Struct
8
+ const :name, String
9
+ const :identifier, Symbol
10
+ const :schema, Schema
11
+ const :required, T::Boolean, default: false
12
+ end
13
+
14
+ module UnionTag
15
+ extend T::Helpers
16
+ include Kernel
17
+ sealed!
18
+ end
19
+
20
+ class Tagged < T::Struct
21
+ include UnionTag
22
+ const :property_name, String
23
+ const :mapping, T::Hash[String, String]
24
+ end
25
+
26
+ class Untagged < T::Struct
27
+ include UnionTag
28
+ end
29
+
30
+ module TypeDef
31
+ extend T::Sig
32
+ extend T::Helpers
33
+ include Kernel
34
+ sealed!
35
+
36
+ sig { params(type_def: TypeDef).returns(String) }
37
+ def self.name_of(type_def)
38
+ case type_def
39
+ when ObjectDef, FormDef, EnumDef, UnionDef, AliasDef then type_def.name
40
+ else T.absurd(type_def)
41
+ end
42
+ end
43
+ end
44
+
45
+ class ObjectDef < T::Struct
46
+ include TypeDef
47
+ const :name, String
48
+ const :properties, T::Array[Property]
49
+ const :additional_properties, T.nilable(Schema), default: nil
50
+ const :meta, Meta, factory: -> { Meta.new }
51
+ end
52
+
53
+ class FormDef < T::Struct
54
+ include TypeDef
55
+ const :name, String
56
+ const :properties, T::Array[Property]
57
+ const :additional_properties, T.nilable(Schema), default: nil
58
+ const :meta, Meta, factory: -> { Meta.new }
59
+ end
60
+
61
+ class EnumMember < T::Struct
62
+ const :constant, String
63
+ const :value, T.any(String, Integer)
64
+ end
65
+
66
+ class EnumDef < T::Struct
67
+ include TypeDef
68
+ const :name, String
69
+ const :members, T::Array[EnumMember]
70
+ const :meta, Meta, factory: -> { Meta.new }
71
+ end
72
+
73
+ class UnionDef < T::Struct
74
+ include TypeDef
75
+ const :name, String
76
+ const :members, T::Array[Schema]
77
+ const :tag, UnionTag
78
+ const :meta, Meta, factory: -> { Meta.new }
79
+ end
80
+
81
+ class AliasDef < T::Struct
82
+ include TypeDef
83
+ const :name, String
84
+ const :target, Schema
85
+ const :meta, Meta, factory: -> { Meta.new }
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,78 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Codegen
6
+ module Naming
7
+ extend T::Sig
8
+
9
+ RESERVED = T.let(
10
+ %w[
11
+ BEGIN END alias and begin break case class def defined? do else elsif end ensure
12
+ false for if in module next nil not or redo rescue retry return self super then
13
+ true undef unless until when while yield __FILE__ __LINE__ __ENCODING__
14
+ ].freeze,
15
+ T::Array[String]
16
+ )
17
+
18
+ sig { params(string: String).returns(T::Array[String]) }
19
+ def self.words(string)
20
+ string
21
+ .to_s
22
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1 \2') # HTTPSConnection -> HTTPS Connection
23
+ .gsub(/([a-z\d])([A-Z])/, '\1 \2') # gameDomain -> game Domain
24
+ .split(/[^a-zA-Z0-9]+/)
25
+ .reject(&:empty?)
26
+ end
27
+
28
+ sig { params(string: String).returns(String) }
29
+ def self.snake(string) = words(string).map(&:downcase).join("_")
30
+
31
+ sig { params(string: String).returns(String) }
32
+ def self.pascal(string) = words(string).map(&:capitalize).join
33
+
34
+ SHADOWED = T.let(
35
+ %w[
36
+ __id__ __send__ as_json class class_eval clone deep_dup define_singleton_method
37
+ deserialize display dup enum_for extend freeze gem hash inspect instance_eval
38
+ instance_exec instance_values instance_variable_get instance_variable_names
39
+ instance_variable_set instance_variables itself method methods object_id presence
40
+ presence_in pretty_inspect pretty_print pretty_print_cycle pretty_print_inspect
41
+ pretty_print_instance_variables private_methods protected_methods public_method
42
+ public_methods public_send remove_instance_variable send serialize singleton_class
43
+ singleton_method singleton_methods tap then to_enum to_json to_param to_query to_s
44
+ to_yaml try with with_options yield_self
45
+ ].to_set.freeze,
46
+ T::Set[String]
47
+ )
48
+
49
+ sig { params(string: String).returns(Symbol) }
50
+ def self.identifier(string)
51
+ name = snake(string)
52
+ name = "value" if name.empty?
53
+ name = "n#{name}" if name.match?(/\A\d/)
54
+ name = "#{name}_" if RESERVED.include?(name) || SHADOWED.include?(name)
55
+ name.to_sym
56
+ end
57
+
58
+ sig { params(string: String).returns(String) }
59
+ def self.constant(string)
60
+ name = pascal(string)
61
+ name = "Value" if name.empty?
62
+ name = "Value#{name}" if name.match?(/\A\d/)
63
+ name
64
+ end
65
+
66
+ sig { params(value: T.untyped).returns(String) }
67
+ def self.enum_member(value)
68
+ case value
69
+ when ::String then constant(value)
70
+ when ::Integer then "Value#{value.negative? ? "Minus#{value.abs}" : value}"
71
+ when true then "True"
72
+ when false then "False"
73
+ else constant(value.to_s)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,48 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Codegen
6
+ class RubyType < T::Struct
7
+ extend T::Sig
8
+
9
+ CONSTANT_PATH = T.let(/\A(::)?[A-Z]\w*(::[A-Z]\w*)*\z/, Regexp)
10
+
11
+ const :type, String
12
+ const :codec, String
13
+
14
+ sig { params(where: String, value: T.untyped).returns(RubyType) }
15
+ def self.parse(where, value)
16
+ unless value.is_a?(Hash)
17
+ raise ConfigError,
18
+ "#{where} must be a mapping with `type` and `codec`, got #{value.inspect}.\n " \
19
+ "#{where}:\n type: \"::YourType\"\n codec: \"YourApp::YourTypeCodec\""
20
+ end
21
+
22
+ unknown = value.keys.map(&:to_s) - %w[type codec]
23
+ unless unknown.empty?
24
+ raise ConfigError,
25
+ "#{where} has unknown key#{"s" if unknown.size > 1} #{unknown.sort.join(", ")}. " \
26
+ "Valid keys: type, codec."
27
+ end
28
+
29
+ missing = %w[type codec].reject { |key| value[key] }
30
+ unless missing.empty?
31
+ raise ConfigError,
32
+ "#{where} is missing #{missing.join(" and ")}. Both are required: `type` is what " \
33
+ "appears in signatures, `codec` is what converts it: a module extending, or an " \
34
+ "instance of a class including, OpenAPIKit::Codec."
35
+ end
36
+
37
+ codec = value.fetch("codec").to_s
38
+ unless codec.match?(CONSTANT_PATH)
39
+ raise ConfigError,
40
+ "#{where}.codec is #{codec.inspect}, which is not a Ruby constant path. " \
41
+ "It must name a constant holding a codec, e.g. \"YourApp::YourTypeCodec\"."
42
+ end
43
+
44
+ new(type: value.fetch("type").to_s, codec: codec)
45
+ end
46
+ end
47
+ end
48
+ end