infrawrench-sdk 0.1.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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # infrawrench-sdk v0.1.1 | MIT | Copyright (c) 2026 Infrawrench LLC
4
+ # https://github.com/Infrawrench/Infrawrench
5
+ #
6
+ # Generated from the Infrawrench API OpenAPI 3.1 spec (API version 0.1.1).
7
+ #
8
+ # DO NOT EDIT. Regenerate with:
9
+ # pnpm --filter @infrawrench/web generate:sdk
10
+ #
11
+ # Internal routes are absent by construction: the generator consumes the same
12
+ # published spec that /openapi.json serves, which drops every operation
13
+ # marked x-internal.
14
+
15
+ require_relative "version"
16
+ require_relative "transport"
17
+ require_relative "client"
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ # infrawrench-sdk v0.1.1 | MIT | Copyright (c) 2026 Infrawrench LLC
4
+ # https://github.com/Infrawrench/Infrawrench
5
+ #
6
+ # Generated from the Infrawrench API OpenAPI 3.1 spec (API version 0.1.1).
7
+ #
8
+ # DO NOT EDIT. Regenerate with:
9
+ # pnpm --filter @infrawrench/web generate:sdk
10
+ #
11
+ # Internal routes are absent by construction: the generator consumes the same
12
+ # published spec that /openapi.json serves, which drops every operation
13
+ # marked x-internal.
14
+ require "json"
15
+ require "net/http"
16
+ require "securerandom"
17
+ require "uri"
18
+
19
+ module Infrawrench
20
+ # Replaced with the first server advertised by the spec.
21
+ DEFAULT_BASE_URL = "https://app.infrawrench.com"
22
+
23
+ # Replaced with the path parameter the client can carry as configuration
24
+ # (`orgId`), or `nil` if the API has no such parameter.
25
+ SCOPE_PARAM = "orgId"
26
+
27
+ # Base class for everything this gem raises, so `rescue Infrawrench::Error`
28
+ # catches transport misconfiguration and HTTP failures alike.
29
+ class Error < StandardError; end
30
+
31
+ # Raised before anything is sent: a required path parameter has no value from
32
+ # either the call site or the client's configuration.
33
+ class ConfigurationError < Error; end
34
+
35
+ # Raised for any non-2xx response.
36
+ #
37
+ # Branch on {#code} rather than on the message — it carries the API's
38
+ # machine-readable discriminator (e.g. `reauthentication_required` on a
39
+ # step-up 403) whenever the response body has one.
40
+ class ApiError < Error
41
+ # @return [Integer] HTTP status code.
42
+ attr_reader :status
43
+ # @return [String, nil] Machine-readable error code, when the API sent one.
44
+ attr_reader :code
45
+ # @return [Object] Parsed response body — a Hash for JSON, else the raw String.
46
+ attr_reader :body
47
+ # @return [String] HTTP method of the failed request, e.g. `"GET"`.
48
+ attr_reader :http_method
49
+ # @return [String] Fully resolved URL of the failed request.
50
+ attr_reader :url
51
+
52
+ def initialize(status:, message:, code:, body:, http_method:, url:)
53
+ super(message)
54
+ @status = status
55
+ @code = code
56
+ @body = body
57
+ @http_method = http_method
58
+ @url = url
59
+ end
60
+ end
61
+
62
+ # A file to send in a `multipart/form-data` request.
63
+ #
64
+ # Call sites rarely need this: a plain String of bytes or any IO-like object
65
+ # is accepted wherever a file is expected, and {.coerce} wraps it. Construct
66
+ # one explicitly only to control the filename or content type the server sees.
67
+ class Upload
68
+ # @return [String] The file's bytes.
69
+ attr_reader :bytes
70
+ # @return [String] Filename sent in the part's Content-Disposition.
71
+ attr_reader :filename
72
+ # @return [String] Content-Type sent for the part.
73
+ attr_reader :content_type
74
+
75
+ def initialize(content, filename: "file", content_type: "application/octet-stream")
76
+ @bytes = (content.respond_to?(:read) ? content.read : content.to_s).b
77
+ @filename = filename
78
+ @content_type = content_type
79
+ end
80
+
81
+ # Wrap whatever a caller passed for a file field.
82
+ #
83
+ # A `File` keeps its basename, because a server that stores the upload under
84
+ # the name it was given should get the name the user actually chose.
85
+ #
86
+ # @param value [Upload, IO, String]
87
+ # @return [Upload]
88
+ def self.coerce(value)
89
+ return value if value.is_a?(Upload)
90
+ return new(value) unless value.respond_to?(:read)
91
+
92
+ path = value.respond_to?(:path) ? value.path : nil
93
+ path ? new(value, filename: File.basename(path)) : new(value)
94
+ end
95
+ end
96
+
97
+ # Request plumbing shared by every namespace.
98
+ #
99
+ # The generated namespace classes each hold one of these; reach for
100
+ # {APIV1Client} instead unless you need the resolved {#base_url}.
101
+ class Transport
102
+ # Net::HTTP models each verb as its own request class, so the generated
103
+ # method strings have to be mapped rather than interpolated.
104
+ METHOD_CLASSES = {
105
+ "GET" => Net::HTTP::Get,
106
+ "POST" => Net::HTTP::Post,
107
+ "PUT" => Net::HTTP::Put,
108
+ "PATCH" => Net::HTTP::Patch,
109
+ "DELETE" => Net::HTTP::Delete
110
+ }.freeze
111
+ private_constant :METHOD_CLASSES
112
+
113
+ # Everything RFC 3986 lets a path segment carry unescaped.
114
+ PATH_SAFE = /[^a-zA-Z0-9\-._~]/n.freeze
115
+ private_constant :PATH_SAFE
116
+
117
+ # @return [String] Normalized base URL, without a trailing slash.
118
+ attr_reader :base_url
119
+
120
+ # @param base_url [String, nil] Base URL of the deployment. Defaults to the production API.
121
+ # @param api_key [String, nil] API key or WorkOS access token, sent as `Authorization: Bearer <key>`.
122
+ # @param org_id [String, nil] Default organization id, filled in for every org-scoped call.
123
+ # @param headers [Hash{String => String}] Headers merged into every request. Per-call headers win.
124
+ # @param timeout [Numeric, nil] Read timeout in seconds. Net::HTTP's default when nil.
125
+ # @param open_timeout [Numeric, nil] Connect timeout in seconds.
126
+ # @param http_handler [#call, nil] Replaces the network call entirely. Receives
127
+ # `(URI, Net::HTTPRequest)` and must return something that answers `code`,
128
+ # `body` and `[]`. This is the seam for tests and for routing through a
129
+ # pre-configured connection pool.
130
+ def initialize(base_url: nil, api_key: nil, org_id: nil, headers: {}, timeout: nil,
131
+ open_timeout: nil, http_handler: nil)
132
+ @base_url = (base_url || DEFAULT_BASE_URL).sub(%r{/+\z}, "")
133
+ @api_key = api_key
134
+ @defaults = SCOPE_PARAM.nil? ? {} : { SCOPE_PARAM => org_id }
135
+ @headers = headers.transform_keys(&:to_s)
136
+ @timeout = timeout
137
+ @open_timeout = open_timeout
138
+ @http_handler = http_handler
139
+ end
140
+
141
+ # Perform one call. The generated methods are thin wrappers around this.
142
+ #
143
+ # @param http_method [String] Uppercase verb.
144
+ # @param path [String] URL template with `{param}` placeholders.
145
+ # @param path_params [Hash{String => Object}, nil] Values for those placeholders, by wire name.
146
+ # @param query [Hash{String => Object}, nil] Query parameters; nil values are dropped, Arrays repeat.
147
+ # @param body [Object, nil] JSON request body. Omitted when nil.
148
+ # @param form [Hash{String => Object}, nil] `multipart/form-data` fields. Mutually exclusive with body.
149
+ # @param form_files [Array<String>] Which form fields are file parts rather than scalars.
150
+ # @param accept [Symbol] `:json`, `:binary` or `:empty` — what the endpoint returns.
151
+ # @param request_options [Hash, nil] Per-call `:headers`, `:timeout`, `:open_timeout`.
152
+ # @return [Object, nil] Parsed JSON, raw bytes, or nil.
153
+ # @raise [ApiError] On any non-2xx response.
154
+ # @raise [ConfigurationError] When a path parameter has no value.
155
+ def request(http_method:, path:, path_params: nil, query: nil, body: nil, form: nil,
156
+ form_files: [], accept: :json, request_options: nil)
157
+ options = request_options || {}
158
+ url = "#{@base_url}#{resolve_path(http_method, path, path_params)}#{serialize_query(query)}"
159
+ uri = URI.parse(url)
160
+
161
+ request_class = METHOD_CLASSES.fetch(http_method)
162
+ req = request_class.new(uri)
163
+ req["accept"] = accept == :binary ? "application/octet-stream" : "application/json"
164
+ @headers.each { |name, value| req[name] = value }
165
+ (options[:headers] || {}).each { |name, value| req[name.to_s] = value }
166
+ req["authorization"] = "Bearer #{@api_key}" unless @api_key.nil? || @api_key.empty?
167
+
168
+ if form
169
+ boundary = SecureRandom.hex(16)
170
+ req["content-type"] = "multipart/form-data; boundary=#{boundary}"
171
+ req.body = encode_multipart(form, form_files, boundary)
172
+ elsif !body.nil?
173
+ req["content-type"] = "application/json"
174
+ req.body = JSON.generate(body)
175
+ end
176
+
177
+ response = perform(uri, req, options)
178
+ status = response.code.to_i
179
+ raise build_error(response, status, http_method, url) unless (200..299).cover?(status)
180
+
181
+ decode(response, status, accept)
182
+ end
183
+
184
+ private
185
+
186
+ def perform(uri, req, options)
187
+ return @http_handler.call(uri, req) if @http_handler
188
+
189
+ http = Net::HTTP.new(uri.hostname, uri.port)
190
+ http.use_ssl = uri.scheme == "https"
191
+ read_timeout = options.fetch(:timeout, @timeout)
192
+ open_timeout = options.fetch(:open_timeout, @open_timeout)
193
+ http.read_timeout = read_timeout if read_timeout
194
+ http.open_timeout = open_timeout if open_timeout
195
+ http.start { |connection| connection.request(req) }
196
+ end
197
+
198
+ # Fill `{param}` placeholders from the call, falling back to client config.
199
+ def resolve_path(http_method, path, path_params)
200
+ path.gsub(/\{([^}]+)\}/) do
201
+ name = Regexp.last_match(1)
202
+ value = path_params && path_params.key?(name) ? path_params[name] : nil
203
+ value = @defaults[name] if value.nil?
204
+ if value.nil? || value.to_s.empty?
205
+ hint =
206
+ if name == SCOPE_PARAM
207
+ " — pass it, or set `org_id:` when constructing the client."
208
+ else
209
+ "."
210
+ end
211
+ raise ConfigurationError, "Missing path parameter \"#{name}\" for #{http_method} #{path}#{hint}"
212
+ end
213
+ value.to_s.b.gsub(PATH_SAFE) { |byte| format("%%%02X", byte.ord) }
214
+ end
215
+ end
216
+
217
+ def serialize_query(query)
218
+ return "" if query.nil?
219
+
220
+ pairs = []
221
+ query.each do |name, value|
222
+ next if value.nil?
223
+
224
+ if value.is_a?(Array)
225
+ value.each { |item| pairs << [name.to_s, item.to_s] unless item.nil? }
226
+ else
227
+ pairs << [name.to_s, value.to_s]
228
+ end
229
+ end
230
+ pairs.empty? ? "" : "?#{URI.encode_www_form(pairs)}"
231
+ end
232
+
233
+ def encode_multipart(fields, form_files, boundary)
234
+ files = Array(form_files).map(&:to_s)
235
+ parts = []
236
+ fields.each do |name, value|
237
+ next if value.nil?
238
+
239
+ key = name.to_s
240
+ if files.include?(key)
241
+ upload = Upload.coerce(value)
242
+ parts << +"--#{boundary}\r\n" \
243
+ "Content-Disposition: form-data; name=\"#{escape_part(key)}\"; " \
244
+ "filename=\"#{escape_part(upload.filename)}\"\r\n" \
245
+ "Content-Type: #{upload.content_type}\r\n\r\n"
246
+ parts << upload.bytes
247
+ parts << "\r\n"
248
+ else
249
+ # Anything that isn't a file goes over as a scalar. A Hash or Array
250
+ # here would be ambiguous on the wire, so it is JSON-encoded rather
251
+ # than silently stringified into `{"a"=>1}`.
252
+ scalar = value.is_a?(Hash) || value.is_a?(Array) ? JSON.generate(value) : value.to_s
253
+ parts << +"--#{boundary}\r\n" \
254
+ "Content-Disposition: form-data; name=\"#{escape_part(key)}\"\r\n\r\n"
255
+ parts << scalar
256
+ parts << "\r\n"
257
+ end
258
+ end
259
+ parts << "--#{boundary}--\r\n"
260
+ parts.map(&:b).join
261
+ end
262
+
263
+ # Per WHATWG, a quote inside a multipart field name or filename is
264
+ # percent-escaped rather than backslash-escaped.
265
+ def escape_part(value)
266
+ value.to_s.gsub("\"", "%22").gsub(/[\r\n]/, "")
267
+ end
268
+
269
+ def decode(response, status, accept)
270
+ return (response.body || "").b if accept == :binary
271
+ return nil if accept == :empty || status == 204 || status == 205
272
+
273
+ text = response.body
274
+ return nil if text.nil? || text.empty?
275
+ return text unless (response["content-type"] || "").include?("json")
276
+
277
+ JSON.parse(text)
278
+ end
279
+
280
+ def build_error(response, status, http_method, url)
281
+ text = response.body || ""
282
+ body = text
283
+ unless text.empty?
284
+ begin
285
+ body = JSON.parse(text)
286
+ rescue JSON::ParserError
287
+ # Not JSON — keep the raw text as the body.
288
+ end
289
+ end
290
+ record = body.is_a?(Hash) ? body : {}
291
+ detail = record["error"] || record["message"] || "#{status} Request failed"
292
+ ApiError.new(
293
+ status: status,
294
+ message: "#{http_method} #{url} failed: #{detail}",
295
+ code: record["code"].is_a?(String) ? record["code"] : nil,
296
+ body: body,
297
+ http_method: http_method,
298
+ url: url
299
+ )
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # infrawrench-sdk v0.1.1 | MIT | Copyright (c) 2026 Infrawrench LLC
4
+ # https://github.com/Infrawrench/Infrawrench
5
+ #
6
+ # Generated from the Infrawrench API OpenAPI 3.1 spec (API version 0.1.1).
7
+ #
8
+ # DO NOT EDIT. Regenerate with:
9
+ # pnpm --filter @infrawrench/web generate:sdk
10
+ #
11
+ # Internal routes are absent by construction: the generator consumes the same
12
+ # published spec that /openapi.json serves, which drops every operation
13
+ # marked x-internal.
14
+
15
+ # Kept in its own file so the gemspec can read the version without loading the
16
+ # client — a gemspec that pulls in net/http is a gemspec that can fail to parse.
17
+ module Infrawrench
18
+ VERSION = "0.1.1"
19
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # infrawrench-sdk v0.1.1 | MIT | Copyright (c) 2026 Infrawrench LLC
4
+ # https://github.com/Infrawrench/Infrawrench
5
+ #
6
+ # Generated from the Infrawrench API OpenAPI 3.1 spec (API version 0.1.1).
7
+ #
8
+ # DO NOT EDIT. Regenerate with:
9
+ # pnpm --filter @infrawrench/web generate:sdk
10
+ #
11
+ # Internal routes are absent by construction: the generator consumes the same
12
+ # published spec that /openapi.json serves, which drops every operation
13
+ # marked x-internal.
14
+
15
+ # The gem is called "infrawrench-sdk" but its code lives under infrawrench/, so
16
+ # `require "infrawrench-sdk"` would otherwise fail for anyone who typed the
17
+ # gem name they installed.
18
+ require_relative "infrawrench/sdk"