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,183 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Codegen
6
+ module Emit
7
+ class Security
8
+ extend T::Sig
9
+ include Emitter
10
+
11
+ sig { params(document: Model::Document, config: Config).void }
12
+ def initialize(document:, config:)
13
+ @document = document
14
+ @config = config
15
+ end
16
+
17
+ sig { params(name: String).returns(String) }
18
+ def self.constant(name) = Naming.snake(name).upcase
19
+
20
+ sig { params(name: String).returns(String) }
21
+ def self.module_name(name) = Naming.pascal(name)
22
+
23
+ # The principal a protected operation carries. An anonymous alternative means the
24
+ # request may arrive without one.
25
+ sig do
26
+ params(requirements: T::Array[Model::SecurityRequirement], config: Config).returns(String)
27
+ end
28
+ def self.principal_type(requirements, config)
29
+ type = T.must(config.principal)
30
+ requirements.any?(&:anonymous?) ? "T.nilable(#{type})" : type
31
+ end
32
+
33
+ sig { override.returns(T::Array[SourceFile]) }
34
+ def render
35
+ return [] if @document.security_schemes.empty?
36
+
37
+ [
38
+ Source.file(path: "#{@config.module_path}/security.rb",
39
+ modules: @config.modules + ["Security"]) { |buffer| emit_body(buffer) }
40
+ ]
41
+ end
42
+
43
+ private
44
+
45
+ sig { params(buffer: Buffer).void }
46
+ def emit_body(buffer)
47
+ buffer.line("extend T::Sig")
48
+
49
+ @document.security_schemes.each do |scheme|
50
+ buffer.blank
51
+ buffer.line("#{Security.constant(Model::SecurityScheme.name_of(scheme))} = T.let(")
52
+ buffer.indent do
53
+ type, arguments = constructor(scheme)
54
+ extensions = Model::SecurityScheme.extensions_of(scheme)
55
+ arguments += [["extensions", extensions.inspect]] unless extensions.empty?
56
+ buffer.nest_call("#{type}.new", arguments)
57
+ buffer.line(type)
58
+ end
59
+ buffer.line(")")
60
+ end
61
+
62
+ buffer.blank
63
+ emit_catalogue(buffer)
64
+
65
+ authenticated.each do |scheme|
66
+ buffer.blank
67
+ emit_authenticator(buffer, scheme)
68
+ end
69
+ end
70
+
71
+ # One interface per scheme, filled by a registry slot the same way handlers are.
72
+ # Returning nil means this alternative was not satisfied, so the next one is
73
+ # tried. Raise to refuse outright.
74
+ sig { params(buffer: Buffer, scheme: Model::SecurityScheme).void }
75
+ def emit_authenticator(buffer, scheme)
76
+ name = Model::SecurityScheme.name_of(scheme)
77
+
78
+ type, = constructor(scheme)
79
+
80
+ buffer.nest("module #{Security.module_name(name)}") do
81
+ buffer.line("extend T::Sig")
82
+ buffer.line("extend T::Helpers")
83
+ buffer.line("abstract!")
84
+ buffer.blank
85
+ buffer.line("sig { returns(#{type}) }")
86
+ buffer.line("def scheme = #{Security.constant(name)}")
87
+ buffer.blank
88
+ emit_credential(buffer, scheme)
89
+ buffer.blank
90
+ emit_authenticate(buffer)
91
+ end
92
+ end
93
+
94
+ sig { params(buffer: Buffer, scheme: Model::SecurityScheme).void }
95
+ def emit_credential(buffer, scheme)
96
+ buffer.line("sig { params(request: ::ActionDispatch::Request).returns(T.nilable(::String)) }")
97
+ buffer.line("def credential(request) = ::OpenAPIKit::Security.credential(scheme, request)")
98
+ return unless basic?(scheme)
99
+
100
+ buffer.blank
101
+ buffer.line("sig do")
102
+ buffer.indent do
103
+ buffer.line("params(request: ::ActionDispatch::Request)")
104
+ buffer.line(" .returns(T.nilable([::String, ::String]))")
105
+ end
106
+ buffer.line("end")
107
+ buffer.line("def basic_credential(request) = ::OpenAPIKit::Security.basic(scheme, request)")
108
+ end
109
+
110
+ sig { params(buffer: Buffer).void }
111
+ def emit_authenticate(buffer)
112
+ buffer.line("sig do")
113
+ buffer.indent do
114
+ buffer.line("abstract.params(request: ::ActionDispatch::Request, scopes: T::Array[::String])")
115
+ buffer.line(" .returns(T.nilable(#{T.must(@config.principal)}))")
116
+ end
117
+ buffer.line("end")
118
+ buffer.line("def authenticate(request:, scopes:); end")
119
+ end
120
+
121
+ # Undecoded base64 is no use, so a basic scheme gets the decoded pair instead.
122
+ sig { params(scheme: Model::SecurityScheme).returns(T::Boolean) }
123
+ def basic?(scheme) = scheme.is_a?(Model::HttpScheme) && scheme.scheme == "basic"
124
+
125
+ sig { params(name: String).returns(T::Boolean) }
126
+ def used?(name)
127
+ @document.operations.any? do |operation|
128
+ @document.security_for(operation).any? { |requirement| requirement.schemes.key?(name) }
129
+ end
130
+ end
131
+
132
+ sig { returns(T::Array[Model::SecurityScheme]) }
133
+ def authenticated
134
+ return [] if @config.principal.nil?
135
+
136
+ @document.security_schemes.select { |scheme| used?(Model::SecurityScheme.name_of(scheme)) }
137
+ end
138
+
139
+ sig { params(buffer: Buffer).void }
140
+ def emit_catalogue(buffer)
141
+ buffer.line("SCHEMES = T.let(")
142
+ buffer.indent do
143
+ buffer.line("{")
144
+ buffer.indent do
145
+ @document.security_schemes.each do |scheme|
146
+ name = Model::SecurityScheme.name_of(scheme)
147
+ buffer.line("#{name.inspect} => #{Security.constant(name)},")
148
+ end
149
+ end
150
+ buffer.line("}.freeze,")
151
+ buffer.line("T::Hash[::String, ::OpenAPIKit::Security::Scheme]")
152
+ end
153
+ buffer.line(")")
154
+ end
155
+
156
+ sig do
157
+ params(scheme: Model::SecurityScheme)
158
+ .returns([String, T::Array[[String, String]]])
159
+ end
160
+ def constructor(scheme)
161
+ case scheme
162
+ when Model::ApiKeyScheme
163
+ location = "::OpenAPIKit::Security::ApiKeyLocation::#{Naming.pascal(scheme.location.serialize)}"
164
+ ["::OpenAPIKit::Security::ApiKey",
165
+ [["name", scheme.name.inspect], ["location", location],
166
+ ["parameter_name", scheme.parameter_name.inspect]]]
167
+ when Model::HttpScheme
168
+ ["::OpenAPIKit::Security::Http",
169
+ [["name", scheme.name.inspect], ["scheme", scheme.scheme.inspect],
170
+ ["bearer_format", scheme.bearer_format.inspect]]]
171
+ when Model::OAuth2Scheme
172
+ ["::OpenAPIKit::Security::OAuth2",
173
+ [["name", scheme.name.inspect], ["scopes", scheme.scopes.inspect]]]
174
+ when Model::OpenIDConnectScheme
175
+ ["::OpenAPIKit::Security::OpenIDConnect",
176
+ [["name", scheme.name.inspect], ["url", scheme.url.inspect]]]
177
+ else T.absurd(scheme)
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,30 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Codegen
6
+ module Emit
7
+ class SourceFile < T::Struct
8
+ const :path, String
9
+ const :contents, String
10
+ end
11
+
12
+ module Source
13
+ extend T::Sig
14
+
15
+ sig do
16
+ params(path: String, modules: T::Array[String], block: T.proc.params(buffer: Buffer).void)
17
+ .returns(SourceFile)
18
+ end
19
+ def self.file(path:, modules:, &block)
20
+ buffer = Buffer.new
21
+ buffer.line("# typed: strict")
22
+ buffer.line("# frozen_string_literal: true")
23
+ buffer.blank
24
+ buffer.nest_modules(modules) { block.call(buffer) }
25
+ SourceFile.new(path: path, contents: buffer.to_s)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,153 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Codegen
6
+ module Emit
7
+ class Types
8
+ extend T::Sig
9
+ include Emitter
10
+
11
+ sig { params(document: Model::Document, registry: TypeRegistry, config: Config).void }
12
+ def initialize(document:, registry:, config:)
13
+ @document = document
14
+ @registry = registry
15
+ @config = config
16
+ @codecs = T.let(Codecs.new(document: document, registry: registry, config: config), Codecs)
17
+ @forms = T.let(Forms.new(registry: registry), Forms)
18
+ end
19
+
20
+ sig { override.returns(T::Array[SourceFile]) }
21
+ def render
22
+ declared_types.map do |type|
23
+ name = Model::TypeDef.name_of(type)
24
+ Source.file(path: "#{@config.module_path}/types/#{Naming.snake(name)}.rb",
25
+ modules: @config.modules + ["Types"]) do |buffer|
26
+ emit_declaration(buffer, type)
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ sig { returns(T::Array[Model::TypeDef]) }
34
+ def declared_types = @document.types.grep_v(Model::AliasDef)
35
+
36
+ sig { params(buffer: Buffer, type: Model::TypeDef).void }
37
+ def emit_declaration(buffer, type)
38
+ case type
39
+ when Model::EnumDef then emit_enum(buffer, type)
40
+ when Model::UnionDef then emit_union(buffer, type)
41
+ when Model::ObjectDef then emit_object(buffer, type)
42
+ when Model::FormDef then emit_form(buffer, type)
43
+ when Model::AliasDef then nil
44
+ else T.absurd(type)
45
+ end
46
+ end
47
+
48
+ sig { params(buffer: Buffer, type: Model::EnumDef).void }
49
+ def emit_enum(buffer, type)
50
+ buffer.nest("class #{type.name} < T::Enum") do
51
+ buffer.nest("enums do") do
52
+ type.members.each { |member| buffer.line("#{member.constant} = new(#{member.value.inspect})") }
53
+ end
54
+ buffer.blank
55
+ @codecs.emit(buffer, type)
56
+ end
57
+ end
58
+
59
+ sig { params(buffer: Buffer, type: Model::UnionDef).void }
60
+ def emit_union(buffer, type)
61
+ members = type.members.map { |member| @registry.sorbet_type(member) }.uniq
62
+ inner = members.one? ? T.must(members.first) : "T.any(#{members.join(", ")})"
63
+
64
+ buffer.nest("module #{type.name}") do
65
+ buffer.line("Value = T.type_alias { #{inner} }")
66
+ buffer.blank
67
+ @codecs.emit(buffer, type)
68
+ end
69
+ end
70
+
71
+ sig { params(buffer: Buffer, type: Model::ObjectDef).void }
72
+ def emit_object(buffer, type)
73
+ buffer.nest("class #{type.name} < T::Struct") do
74
+ buffer.line("extend T::Sig")
75
+ buffer.blank
76
+ type.properties.each { |property| buffer.line(prop_line(property)) }
77
+ extra = type.additional_properties
78
+ buffer.line("const :additional_properties, #{additional_type(extra)}, factory: -> { {} }") if extra
79
+ buffer.blank
80
+ emit_equality(buffer, type.name)
81
+ buffer.blank
82
+ @codecs.emit(buffer, type)
83
+ end
84
+ end
85
+
86
+ sig { params(buffer: Buffer, type: Model::FormDef).void }
87
+ def emit_form(buffer, type)
88
+ buffer.nest("class #{type.name} < T::Struct") do
89
+ buffer.line("extend T::Sig")
90
+ buffer.blank
91
+ type.properties.each { |property| buffer.line(prop_line(property)) }
92
+ extra = type.additional_properties
93
+ buffer.line("const :additional_properties, #{additional_type(extra)}, factory: -> { {} }") if extra
94
+ buffer.blank
95
+ emit_equality(buffer, type.name)
96
+ buffer.blank
97
+ @forms.emit(buffer, type)
98
+ end
99
+ end
100
+
101
+ sig { params(buffer: Buffer, name: String).void }
102
+ def emit_equality(buffer, name)
103
+ buffer.line("sig { params(other: T.untyped).returns(T::Boolean) }")
104
+ buffer.line("def ==(other) = other.instance_of?(#{name}) && other.serialize == serialize")
105
+ buffer.blank
106
+ buffer.line("sig { params(other: T.untyped).returns(T::Boolean) }")
107
+ buffer.line("def eql?(other) = self == other")
108
+ buffer.blank
109
+ buffer.line("sig { returns(::Integer) }")
110
+ buffer.line("def hash = [self.class, serialize].hash")
111
+ end
112
+
113
+ sig { params(schema: Model::Schema).returns(String) }
114
+ def additional_type(schema) = "T::Hash[::String, #{@registry.sorbet_type(schema)}]"
115
+
116
+ sig { params(property: Model::Property).returns(String) }
117
+ def prop_line(property)
118
+ meta = Model::Schema.meta(property.schema)
119
+ declaration = "const :#{property.identifier}, #{prop_type(property)}"
120
+ default = meta.default
121
+
122
+ if default && !property.required
123
+ clause = Defaults.clause(schema: property.schema, default: default, registry: @registry)
124
+ return "#{declaration}, #{clause}"
125
+ end
126
+ return "#{declaration}, default: ::OpenAPIKit::ABSENT" if optional_nullable?(property)
127
+
128
+ declaration
129
+ end
130
+
131
+ sig { params(property: Model::Property).returns(String) }
132
+ def prop_type(property)
133
+ meta = Model::Schema.meta(property.schema)
134
+ base = if Model::Schema.file?(property.schema)
135
+ TypeRegistry::UPLOADED_FILE
136
+ else
137
+ @registry.sorbet_type(property.schema)
138
+ end
139
+
140
+ return "::OpenAPIKit::Optional[T.nilable(#{base})]" if optional_nullable?(property)
141
+ return "T.nilable(#{base})" if Defaults.nilable?(required: property.required, meta: meta)
142
+
143
+ base
144
+ end
145
+
146
+ sig { params(property: Model::Property).returns(T::Boolean) }
147
+ def optional_nullable?(property)
148
+ Decode.optional_nullable?(required: property.required, meta: Model::Schema.meta(property.schema))
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,48 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenAPIKit
5
+ module Codegen
6
+ class Generator
7
+ extend T::Sig
8
+
9
+ sig { returns(T::Array[String]) }
10
+ attr_reader :warnings
11
+
12
+ sig { params(config: Config).void }
13
+ def initialize(config:)
14
+ @config = config
15
+ @warnings = T.let([], T::Array[String])
16
+ end
17
+
18
+ sig { returns(T::Array[Emit::SourceFile]) }
19
+ def sources
20
+ loader = Loader.new(config: @config)
21
+ document = loader.parse
22
+ registry = TypeRegistry.for(document, @config)
23
+
24
+ files = emitters(document, registry).flat_map(&:render)
25
+ @warnings = loader.warnings + registry.warnings
26
+ files
27
+ end
28
+
29
+ sig { returns(T::Array[Pathname]) }
30
+ def generate = Writer.new(output: @config.output).write_all(sources)
31
+
32
+ private
33
+
34
+ sig { params(document: Model::Document, registry: TypeRegistry).returns(T::Array[Emit::Emitter]) }
35
+ def emitters(document, registry)
36
+ [
37
+ Emit::Types.new(document: document, registry: registry, config: @config),
38
+ Emit::Operations.new(document: document, registry: registry, config: @config),
39
+ Emit::Handlers.new(document: document, config: @config),
40
+ Emit::Security.new(document: document, config: @config),
41
+ Emit::Registry.new(document: document, config: @config),
42
+ Emit::Routes.new(document: document, config: @config),
43
+ Emit::Controllers.new(document: document, registry: registry, config: @config)
44
+ ]
45
+ end
46
+ end
47
+ end
48
+ end