openapi-sourcetools 0.7.0 → 0.8.0

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,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'task'
7
+ require 'yaml'
8
+
9
+
10
+ module OpenAPISourceTools
11
+ # Class that contains security scheme objects and what headers
12
+ # and parameters are added to the request when the scheme is used.
13
+ class SecuritySchemeInfo
14
+ include Comparable
15
+
16
+ attr_reader :headers, :parameters, :query_parameters, :cookies
17
+
18
+ def initialize(security_scheme = {}, scheme_templates = [])
19
+ @headers = {}
20
+ @parameters = {}
21
+ @query_parameters = {}
22
+ @cookies = {}
23
+ scheme_templates.each do |template|
24
+ s = template['scheme']
25
+ match = true
26
+ s.each do |k, v|
27
+ unless v == security_scheme[k]
28
+ match = false
29
+ break
30
+ end
31
+ end
32
+ next unless match
33
+ o = template['output']
34
+ fill_in(@headers, o['headers'] || {}, security_scheme)
35
+ fill_in(@parameters, o['parameters'] || {}, security_scheme)
36
+ fill_in(@query_parameters, o['query_parameters'] || {}, security_scheme)
37
+ fill_in(@cookies, o['cookies'] || {}, security_scheme)
38
+ break
39
+ end
40
+ end
41
+
42
+ def fill_in(output, source, scheme)
43
+ source.each do |k, v|
44
+ if k.start_with?('<') && k.end_with?('>')
45
+ scheme_key = k[1..-2]
46
+ scheme_value = scheme[scheme_key]
47
+ raise "Missing security scheme value for #{scheme_key}" if scheme_value.nil?
48
+ output[scheme_value] = v
49
+ else
50
+ output[k] = v
51
+ end
52
+ end
53
+ end
54
+
55
+ def merge!(other)
56
+ @headers.merge!(other.headers)
57
+ @parameters.merge!(other.parameters)
58
+ @query_parameters.merge!(other.query_parameters)
59
+ @cookies.merge!(other.cookies)
60
+ end
61
+
62
+ def merge(other)
63
+ out = SecuritySchemeInfo.new({}, [])
64
+ out.merge!(self)
65
+ out.merge!(other)
66
+ out
67
+ end
68
+
69
+ def <=>(other)
70
+ # Only really interested in equality.
71
+ @headers <=> other.headers || @parameters <=> other.parameters || @query_parameters <=> other.query_parameters || @cookies <=> other.cookies
72
+ end
73
+ end
74
+
75
+ class ScopedSecuritySchemeInfo
76
+ include Comparable
77
+
78
+ attr_reader :ssi, :scopes
79
+
80
+ def initialize(ssi, scopes)
81
+ @ssi = ssi
82
+ @scopes = scopes
83
+ end
84
+
85
+ def merge(other)
86
+ @ssi = @ssi.merge(other.ssi)
87
+ @scopes = @scopes.concat(other.scopes).uniq
88
+ end
89
+
90
+ def <=>(other)
91
+ # Only really interested in equality.
92
+ @ssi <=> other.ssi || @scopes <=> other.scopes
93
+ end
94
+ end
95
+
96
+ # Helper class for dealing with securitySchemes.
97
+ # Adds :security key to each operation object and to root.
98
+ # They contain the used securitySchemes in use and the security schemes
99
+ # in effect for that operation.
100
+ class SecuritySchemesTask
101
+ include TaskInterface
102
+
103
+ def default_configuration
104
+ # type apiKey, name, in.
105
+ # type http, scheme, bearerFormat.
106
+ # type mutualTLS, beyond the scope of code templates.
107
+ # type oauth2, flows object.
108
+ # type openIdConnect, openIdConnectUrl. More for login?
109
+ YAML.safe_load(%q(---
110
+ security_schemes:
111
+ - scheme:
112
+ type: apiKey
113
+ in: header
114
+ output:
115
+ headers:
116
+ '<name>': string
117
+ - scheme:
118
+ type: apiKey
119
+ in: query
120
+ output:
121
+ parameters:
122
+ '<name>': string
123
+ - scheme:
124
+ type: apiKey
125
+ in: query
126
+ output:
127
+ query_parameters:
128
+ '<name>': string
129
+ - scheme:
130
+ type: apiKey
131
+ in: cookie
132
+ output:
133
+ cookies:
134
+ '<name>': true
135
+ - scheme:
136
+ type: http
137
+ scheme: basic
138
+ output:
139
+ headers:
140
+ Authorization: string
141
+ - scheme:
142
+ type: http
143
+ scheme: bearer
144
+ output:
145
+ headers:
146
+ Authorization: string
147
+ - scheme:
148
+ type: mutualTLS
149
+ output: {}
150
+ - scheme:
151
+ type: oauth2
152
+ output:
153
+ headers:
154
+ Authorization: string
155
+ - scheme:
156
+ type: openIdConnect
157
+ output:
158
+ headers:
159
+ Authorization: string
160
+ ))
161
+ end
162
+
163
+ def convert_security_schemes(doc)
164
+ ss = doc.dig('components', 'securitySchemes')
165
+ return nil if ss.nil?
166
+ # Should create unique objects. Different name may lead to same object.
167
+ out = {}
168
+ ss.each do |name, security_scheme|
169
+ out[name] = SecuritySchemeInfo.new(security_scheme)
170
+ end
171
+ out
172
+ end
173
+
174
+ def operation_object_security(doc, schemes)
175
+ # Find all operation objects and security in effect.
176
+ all_ops = []
177
+ seen_secs = Set.new
178
+ root_sec = doc['security'] || []
179
+ not_method = %w[parameters servers summary description]
180
+ doc['paths'].each_value do |path_item|
181
+ path_sec = path_item['security'] || root_sec
182
+ path_item.each do |method, op|
183
+ next if not_method.include?(method)
184
+ op_sec = op['security'] || path_sec
185
+ all_ops.push([ op, op_sec ])
186
+ op_sec.each do |security_requirement|
187
+ names = security_requirement.keys
188
+ seen_secs.merge(names)
189
+ if names.empty? && !schemes.key?('')
190
+ schemes[''] = SecuritySchemeInfo.new
191
+ end
192
+ end
193
+ end
194
+ end
195
+ # Check that all seen secs names have a scheme. Report all in one place.
196
+ missing = false
197
+ seen_secs.to_a.sort!.each do |name|
198
+ unless schemes.key?(name)
199
+ missing = true
200
+ warn("#/components/securitySchemes is missing: #{name}")
201
+ end
202
+ end
203
+ return 1 if missing
204
+ # Now we know all individual parts are available.
205
+ # Map security arrays of objects to arrays of arrays.
206
+ all_scopeds = [] # For having just one instance for unique data combination.
207
+ all_ops.each do |pair|
208
+ sec = pair.second.map do |sec_req|
209
+ keys = sec_req.keys.sort!
210
+ values = keys.map { |name| sec_req[name].sort! }
211
+ if keys.empty?
212
+ keys = [ '' ]
213
+ values = [ [] ]
214
+ end
215
+ s3is = []
216
+ keys.size.times do |idx|
217
+ name = keys[idx]
218
+ scopes = values[idx]
219
+ s3i = ScopedSecuritySchemeInfo.new(schemes[name], scopes)
220
+ idx = all_scopeds.index(s3i)
221
+ if idx.nil?
222
+ all_scopeds.push(s3i)
223
+ else
224
+ s3i = all_scopeds(idx) # Use the same instance everywhere.
225
+ end
226
+ s3is.push(s3i)
227
+ end
228
+ s3is
229
+ end
230
+ pair.first[:security] = sec # Arrays of ScopedSecuritySchemeInfo.
231
+ # When individual objects are not needed, provide merged together items.
232
+ all_merged = []
233
+ pair.first[:security_merged] = sec.map do |s3is| # ScopedSecuritySchemeInfos.
234
+ m = s3is.first
235
+ s3is[1..].each do |s3i|
236
+ m = m.merge(s3i)
237
+ end
238
+ idx = all_merged.index(m)
239
+ if idx.nil?
240
+ all_merged.push(m)
241
+ else
242
+ m = all_merged[idx] # Use the same instance everywhere.
243
+ end
244
+ m
245
+ end
246
+ end
247
+ all_merged.map(&:ssi).uniq.sort!
248
+ end
249
+
250
+ def generate(_context_binding)
251
+ # Get security_schemes from config, append defaults to it.
252
+ scheme_templates = gen.configuration['security_schemes'] || []
253
+ scheme_templates.concat(default_configuration['security_schemes'])
254
+ simple_schemes = convert_security_schemes(Gen.doc)
255
+ # For all operation objects, add :security array with all used schemes.
256
+ merged_schemes = operation_object_security(Gen.doc, simple_schemes || {})
257
+ return merged_schemes if merged_schemes.is_a?(Integer)
258
+ Gen.doc[:securitySchemes] = simple_schemes unless simple_schemes.nil?
259
+ Gen.doc[:securitySchemes_merged] = merged_schemes unless merged_schemes.empty?
260
+ end
261
+
262
+ def discard
263
+ true
264
+ end
265
+
266
+ COMPONENTS = '#/components/'
267
+ end
268
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'common'
7
+ require 'erb'
8
+
9
+
10
+ module OpenAPISourceTools
11
+
12
+ # Required interface for tasks, with default implementation for some methods.
13
+ module TaskInterface
14
+ def generate(context_binding)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def output_name
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def discard
23
+ false
24
+ end
25
+
26
+ def executable
27
+ false
28
+ end
29
+
30
+ def system
31
+ false
32
+ end
33
+ end
34
+
35
+ # Loads template or takes it as argument. Renders template using ERB.
36
+ class Task
37
+ include TaskInterface
38
+
39
+ attr_reader :src, :template, :template_name
40
+ attr_accessor :name, :executable, :discard, :x
41
+
42
+ def initialize(src, template, template_name)
43
+ @src = src
44
+ @template = template
45
+ @template_name = template_name
46
+ if @template.nil?
47
+ raise ArgumentError, 'template_name or template must be given' if @template_name.nil?
48
+ begin
49
+ @template = File.read(@template_name)
50
+ rescue Errno::ENOENT
51
+ raise StandardError, "Could not load #{@template_name}"
52
+ rescue StandardError => e
53
+ raise StandardError, "#{e}\nFailed to read #{@template_name}"
54
+ end
55
+ end
56
+ @name = nil
57
+ @executable = false
58
+ @discard = false
59
+ @x = nil
60
+ end
61
+
62
+ # If this is overridden to perform some processing but not to produce output,
63
+ # set @discard = true and return value will be ignored. No other methods are
64
+ # called in that case.
65
+ def internal_generate(context_binding)
66
+ ERB.new(@template).result(context_binding)
67
+ end
68
+
69
+ # You can override this instead of internal_generate if you do not need the
70
+ # exception handling.
71
+ def generate(context_binding)
72
+ n = @template_name.nil? ? '' : "#{@template_name} "
73
+ internal_generate(context_binding)
74
+ rescue SyntaxError => e
75
+ OpenAPISourceTools::Common.aargh("Template #{n}syntax error: #{e.full_message}", 5)
76
+ rescue Exception => e # Some unexpected error.
77
+ OpenAPISourceTools::Common.aargh("Template #{n}error: #{e.full_message}", 6)
78
+ end
79
+
80
+ # This is only called when generate produced output that is not discarded.
81
+ def output_name
82
+ return @name unless @name.nil?
83
+ # Using template name may show where name assignment is missing.
84
+ # Name assignment may also be missing in the task creation stage.
85
+ return File.basename(@template_name) unless @template_name.nil?
86
+ nil
87
+ end
88
+ end
89
+
90
+ # A task that provides contents to be written to a named file.
91
+ # Optionaly the file may be made executable.
92
+ class WriteTask
93
+ include TaskInterface
94
+
95
+ attr_reader :name, :contents, :executable
96
+
97
+ def initialize(name, contents, executable = false)
98
+ raise ArgumentError, 'name and contents must be given' if name.nil? || contents.nil?
99
+ @name = name
100
+ @contents = contents
101
+ @executable = executable
102
+ end
103
+
104
+ def generate(_context_binding)
105
+ @contents
106
+ end
107
+
108
+ def output_name
109
+ @name
110
+ end
111
+ end
112
+
113
+ # Sets Gen.x to empty hash. Inserted after gem tasks.
114
+ class RestoreProcessorStorage
115
+ include TaskInterface
116
+
117
+ attr_accessor :x # Allows setting the current value when setup code finishes.
118
+
119
+ # Sets initial default value.
120
+ def initialize(x = {})
121
+ @x = x
122
+ Gen.x = @x
123
+ end
124
+
125
+ def generate(_context_binding)
126
+ Gen.x = @x # Restore whatever was current when setup code finished.
127
+ end
128
+
129
+ def discard
130
+ true
131
+ end
132
+
133
+ def system
134
+ true
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ module OpenAPISourceTools
7
+ NAME = 'openapi-sourcetools'
8
+ VERSION = '0.8.0'
9
+
10
+ def self.info(separator = ': ')
11
+ "#{NAME}#{separator}#{VERSION}"
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'sourcetools/task'
7
+ require_relative 'sourcetools/config'
8
+ require_relative 'sourcetools/version'
9
+ # Other modules or classes are exposed via Gen attributes as class instances as needed.
10
+ # Docs is only needed for run-time storage of whatever loaders can handle.
11
+ # Loaders array is exposed and can be added to at run-time.
12
+ # Helper instance is accessible via Gen.h.
13
+ # Output is exposed via Gen.o and Gen.output. Note that if you assign to one,
14
+ # the other will not change.
15
+ # The rest are for internal implementation.
metadata CHANGED
@@ -1,20 +1,40 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi-sourcetools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismo Kärkkäinen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-07 00:00:00.000000000 Z
12
- dependencies: []
13
- description: |2
11
+ date: 2025-01-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: deep_merge
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.2.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.2'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.2.2
33
+ description: |-
34
+ Tools for handling API specification in OpenAPI format.
35
+ Programs to replace of duplicate definitions with references. Other checks.
14
36
 
15
- Tools for handling API specification in OpenAPI format. Replacement of
16
- duplicate definitions with references. Other checks. Does not validate
17
- the document against OpenAPI format specification.
37
+ Does not validate the document against OpenAPI format specification.
18
38
  email: ismokarkkainen@icloud.com
19
39
  executables:
20
40
  - openapi-addheaders
@@ -41,15 +61,19 @@ files:
41
61
  - bin/openapi-merge
42
62
  - bin/openapi-modifypaths
43
63
  - bin/openapi-processpaths
44
- - lib/apiobjects.rb
45
- - lib/common.rb
46
- - lib/docs.rb
47
- - lib/gen.rb
48
- - lib/generate.rb
49
- - lib/helper.rb
50
- - lib/loaders.rb
51
- - lib/output.rb
52
- - lib/task.rb
64
+ - lib/openapi/sourcetools.rb
65
+ - lib/openapi/sourcetools/apiobjects.rb
66
+ - lib/openapi/sourcetools/common.rb
67
+ - lib/openapi/sourcetools/config.rb
68
+ - lib/openapi/sourcetools/docs.rb
69
+ - lib/openapi/sourcetools/gen.rb
70
+ - lib/openapi/sourcetools/generate.rb
71
+ - lib/openapi/sourcetools/helper.rb
72
+ - lib/openapi/sourcetools/loaders.rb
73
+ - lib/openapi/sourcetools/output.rb
74
+ - lib/openapi/sourcetools/securityschemes.rb
75
+ - lib/openapi/sourcetools/task.rb
76
+ - lib/openapi/sourcetools/version.rb
53
77
  homepage: https://xn--ismo-krkkinen-gfbd.fi/openapi-sourcetools/index.html
54
78
  licenses:
55
79
  - UPL-1.0
@@ -63,14 +87,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
87
  requirements:
64
88
  - - ">="
65
89
  - !ruby/object:Gem::Version
66
- version: 3.0.0
90
+ version: 3.2.5
67
91
  required_rubygems_version: !ruby/object:Gem::Requirement
68
92
  requirements:
69
93
  - - ">="
70
94
  - !ruby/object:Gem::Version
71
95
  version: '0'
72
96
  requirements: []
73
- rubygems_version: 3.2.33
97
+ rubygems_version: 3.4.19
74
98
  signing_key:
75
99
  specification_version: 4
76
100
  summary: Tools for creating source code from API specification.