api2convert 10.2.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +99 -0
  3. data/LICENSE +21 -0
  4. data/README.md +164 -0
  5. data/docs/CHANGELOG.md +9 -0
  6. data/docs/SDK_CONTRACT.md +99 -0
  7. data/examples/convert.rb +20 -0
  8. data/examples/webhook.rb +32 -0
  9. data/lib/api2convert/client.rb +139 -0
  10. data/lib/api2convert/config.rb +66 -0
  11. data/lib/api2convert/errors.rb +121 -0
  12. data/lib/api2convert/http/net_http_sender.rb +148 -0
  13. data/lib/api2convert/http/request.rb +47 -0
  14. data/lib/api2convert/http/response.rb +23 -0
  15. data/lib/api2convert/http/transport.rb +290 -0
  16. data/lib/api2convert/input_type.rb +16 -0
  17. data/lib/api2convert/job_status.rb +29 -0
  18. data/lib/api2convert/model/conversion.rb +30 -0
  19. data/lib/api2convert/model/input_file.rb +37 -0
  20. data/lib/api2convert/model/job.rb +66 -0
  21. data/lib/api2convert/model/job_message.rb +30 -0
  22. data/lib/api2convert/model/output_file.rb +40 -0
  23. data/lib/api2convert/model/preset.rb +32 -0
  24. data/lib/api2convert/model/status.rb +24 -0
  25. data/lib/api2convert/resource/contracts.rb +16 -0
  26. data/lib/api2convert/resource/conversions.rb +33 -0
  27. data/lib/api2convert/resource/jobs.rb +112 -0
  28. data/lib/api2convert/resource/presets.rb +43 -0
  29. data/lib/api2convert/resource/stats.rb +35 -0
  30. data/lib/api2convert/result.rb +185 -0
  31. data/lib/api2convert/support/data.rb +89 -0
  32. data/lib/api2convert/support/secret.rb +27 -0
  33. data/lib/api2convert/upload/file_uploader.rb +87 -0
  34. data/lib/api2convert/upload/multipart_stream.rb +74 -0
  35. data/lib/api2convert/version.rb +8 -0
  36. data/lib/api2convert/webhook/event.rb +23 -0
  37. data/lib/api2convert/webhook/verifier.rb +74 -0
  38. data/lib/api2convert.rb +62 -0
  39. data/openapi/api2convert.openapi.json +3325 -0
  40. metadata +88 -0
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Api2Convert
6
+ module Result
7
+ # A downloadable output file.
8
+ #
9
+ # Returned by `client.download(output)` and used internally by
10
+ # {ConversionResult}. A password supplied at conversion time (or to
11
+ # `client.download(output, password)`) is remembered and sent automatically on
12
+ # every download; an explicit password argument to {#save}/{#contents}
13
+ # overrides the remembered one for that call.
14
+ class FileDownload
15
+ def initialize(transport, output, download_password = nil)
16
+ @transport = transport
17
+ @output = output
18
+ @download_password = download_password
19
+ end
20
+
21
+ # The self-contained download URL (no auth required).
22
+ def url
23
+ @output.uri
24
+ end
25
+
26
+ # Stream the file to disk. +path_or_dir+ is a file path, or a directory (the
27
+ # API filename is used). The body is streamed straight to the file
28
+ # chunk-by-chunk (never buffered whole in memory), so an arbitrarily large
29
+ # output cannot exhaust memory. A mid-stream failure removes the partial file
30
+ # so a truncated download never masquerades as a complete result. Returns the
31
+ # path written to.
32
+ def save(path_or_dir, download_password = nil)
33
+ target = resolve_target(path_or_dir.to_s)
34
+ parent = File.dirname(target)
35
+ parent = "." if parent.empty?
36
+ begin
37
+ FileUtils.mkdir_p(parent)
38
+ rescue SystemCallError
39
+ raise Api2Convert::Error, "Could not create directory: #{parent}"
40
+ end
41
+
42
+ stream_to_file(target, resolve_password(download_password))
43
+ target
44
+ end
45
+
46
+ # Download the file and return its contents. This buffers the whole body in
47
+ # memory by design — use {#save} for a large file to stream it to disk.
48
+ def contents(download_password = nil)
49
+ password = resolve_password(download_password)
50
+ # A passwordless download follows storage redirects; a password-protected
51
+ # one must not (the X-Oc-Download-Password header could leak on a redirect).
52
+ @transport.download(@output.uri, headers(password), follow_redirects: password.nil?)
53
+ end
54
+
55
+ # Redacted representation — the remembered download password is masked.
56
+ def inspect
57
+ "#<#{self.class.name} url=#{@output.uri.inspect} " \
58
+ "download_password=#{Support::Secret.mask(@download_password)}>"
59
+ end
60
+
61
+ def to_s
62
+ inspect
63
+ end
64
+
65
+ private
66
+
67
+ def stream_to_file(target, password)
68
+ success = false
69
+ begin
70
+ File.open(target, "wb") do |file|
71
+ @transport.download(
72
+ @output.uri, headers(password), follow_redirects: password.nil?, sink: file
73
+ )
74
+ success = true
75
+ end
76
+ rescue SystemCallError
77
+ raise Api2Convert::Error, "Could not open file for writing: #{target}"
78
+ ensure
79
+ # Any failure (open error, network break, a refused-redirect NetworkError,
80
+ # a mid-stream break) leaves a partial or empty file — remove it so a
81
+ # truncated download can never masquerade as a complete result.
82
+ FileUtils.rm_f(target) unless success
83
+ end
84
+ end
85
+
86
+ def resolve_password(download_password)
87
+ download_password.nil? ? @download_password : download_password
88
+ end
89
+
90
+ def headers(password)
91
+ password.nil? ? {} : { "X-Oc-Download-Password" => password }
92
+ end
93
+
94
+ def resolve_target(path_or_dir)
95
+ looks_like_dir = File.directory?(path_or_dir) ||
96
+ path_or_dir.end_with?("/") ||
97
+ path_or_dir.end_with?(File::SEPARATOR)
98
+ if looks_like_dir
99
+ name = safe_name(@output.filename) || safe_name(@output.id) || "output"
100
+ return File.join(path_or_dir.sub(%r{[/\\]+\z}, ""), name)
101
+ end
102
+ path_or_dir
103
+ end
104
+
105
+ # Reduce an API-supplied name to a bare filename safe to append to a dir.
106
+ # `output.filename` / `output.id` come straight from the API JSON, so a value
107
+ # like `../../etc/cron.d/evil` (or one with separators or a NUL byte) must
108
+ # never escape the caller's chosen directory. Returns nil when nothing usable
109
+ # remains, so the caller can fall back.
110
+ def safe_name(name)
111
+ return nil if name.nil?
112
+
113
+ base = File.basename(name.delete("\x00").tr("\\", "/")).strip
114
+ # Reject a name that reduces to nothing usable. Note Ruby's File.basename("/")
115
+ # is "/" (unlike POSIX basename -> ""), so a pure-separator name must be
116
+ # caught here too, matching the sibling SDKs' fallback behavior.
117
+ return nil if base == "." || base == ".." || base.delete("/").empty?
118
+
119
+ base
120
+ end
121
+ end
122
+
123
+ # The result of a completed conversion.
124
+ #
125
+ # The common case is one output: `result.save("out.pdf")`. Jobs that produce
126
+ # several files expose them via {#outputs} and {#download}.
127
+ class ConversionResult
128
+ # @return [Model::Job] the completed job.
129
+ attr_reader :job
130
+
131
+ def initialize(job, transport, index = 0, download_password = nil)
132
+ @job = job
133
+ @transport = transport
134
+ @index = index
135
+ @download_password = download_password
136
+ end
137
+
138
+ # The selected output file (the first one by default).
139
+ def output
140
+ # Any index not present — including a negative one — raises rather than
141
+ # wrapping around.
142
+ if @index.negative? || @index >= @job.output.length
143
+ raise Api2Convert::Error, "The job produced no output files."
144
+ end
145
+
146
+ @job.output[@index]
147
+ end
148
+
149
+ # All output files produced by the job.
150
+ def outputs
151
+ @job.output
152
+ end
153
+
154
+ # The download URL of the selected output (self-contained, no auth).
155
+ def url
156
+ output.uri
157
+ end
158
+
159
+ # Download the selected output to disk. Returns the path written to.
160
+ def save(path_or_dir, download_password = nil)
161
+ download.save(path_or_dir, download_password)
162
+ end
163
+
164
+ # Download the selected output and return its contents.
165
+ def contents(download_password = nil)
166
+ download.contents(download_password)
167
+ end
168
+
169
+ # A {FileDownload} for a specific output (defaults to the selected one).
170
+ def download(output_file = nil)
171
+ FileDownload.new(@transport, output_file.nil? ? output : output_file, @download_password)
172
+ end
173
+
174
+ # Redacted representation — the remembered download password is masked.
175
+ def inspect
176
+ "#<#{self.class.name} job=#{@job.id.inspect} outputs=#{@job.output.length} " \
177
+ "download_password=#{Support::Secret.mask(@download_password)}>"
178
+ end
179
+
180
+ def to_s
181
+ inspect
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api2Convert
4
+ module Support
5
+ # Typed, null-safe accessors over decoded JSON.
6
+ #
7
+ # Mirrors the PHP SDK's `Support\Data` helper: model hydration stays free of
8
+ # scattered casts and, crucially, **never raises** on a surprising payload — a
9
+ # missing or wrong-typed field falls back to a sensible default. Internal
10
+ # helper, not part of the public API.
11
+ module Data
12
+ module_function
13
+
14
+ # Return +value+ when it is a real String, else +default+. Does not
15
+ # stringify ints/floats/bools — only genuine strings pass through.
16
+ def as_str(value, default = "")
17
+ value.is_a?(String) ? value : default
18
+ end
19
+
20
+ def nullable_str(value)
21
+ value.is_a?(String) ? value : nil
22
+ end
23
+
24
+ # Coerce numeric values to Integer (truncating toward zero), else nil.
25
+ #
26
+ # +true+/+false+ are rejected (they must not become 1/0). Numeric strings
27
+ # and floats are truncated ("3.9" -> 3), matching PHP's (int) cast.
28
+ def nullable_int(value)
29
+ return nil if [true, false].include?(value)
30
+ return value if value.is_a?(Integer)
31
+
32
+ if value.is_a?(Float)
33
+ return nil unless value.finite?
34
+
35
+ return value.to_i
36
+ end
37
+ if value.is_a?(String)
38
+ begin
39
+ return Float(value).to_i
40
+ rescue ArgumentError, TypeError
41
+ return nil
42
+ end
43
+ end
44
+ nil
45
+ end
46
+
47
+ def as_bool(value, default = false)
48
+ [true, false].include?(value) ? value : default
49
+ end
50
+
51
+ # Return +value+ when it is a Hash (a JSON object), else an empty Hash.
52
+ def as_object(value)
53
+ value.is_a?(Hash) ? value : {}
54
+ end
55
+
56
+ # Return a list of values: an Array passes through; a Hash is reduced to its
57
+ # values (mirrors PHP `array_values`); anything else yields [].
58
+ def as_list(value)
59
+ return value if value.is_a?(Array)
60
+ return value.values if value.is_a?(Hash)
61
+
62
+ []
63
+ end
64
+
65
+ # Build a model from each Hash element of +value+; skip non-Hash elements.
66
+ def map_objects(value)
67
+ as_list(value).each_with_object([]) do |item, acc|
68
+ acc << yield(item) if item.is_a?(Hash)
69
+ end
70
+ end
71
+
72
+ def str_list(value)
73
+ as_list(value).grep(String)
74
+ end
75
+
76
+ # Percent-encode a value for use as a single URL path segment.
77
+ #
78
+ # A job/preset id or a stats date/filter is interpolated into the request
79
+ # path; a value carrying `/`, `?`, `#`, a space or any other reserved
80
+ # character (e.g. `all/../contracts`) must be encoded so it can never break
81
+ # out of its segment or forge a different path. Only the RFC 3986 unreserved
82
+ # set survives; everything else becomes `%XX` over the UTF-8 bytes (mirrors
83
+ # Node's `encodeURIComponent` / Go's `url.PathEscape`).
84
+ def encode_segment(value)
85
+ value.to_s.b.gsub(/[^A-Za-z0-9\-_.~]/n) { |byte| format("%%%02X", byte.ord) }
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api2Convert
4
+ module Support
5
+ # Redaction helpers for `#inspect` / `#to_s`.
6
+ #
7
+ # Without these, `p client`, a `config.inspect` in a log line, or an object
8
+ # dumped in a backtrace would print `@api_key` / `@download_password` in
9
+ # cleartext. Every object that holds a secret overrides `#inspect` to route the
10
+ # secret through {mask}. Internal helper, not part of the public API.
11
+ module Secret
12
+ module_function
13
+
14
+ # Mask a secret for display. A `nil` renders as `nil`; a sufficiently long
15
+ # value keeps only its last 4 characters (so it stays recognizable in a log)
16
+ # with the rest hidden; a short value collapses to `[FILTERED]`.
17
+ def mask(value)
18
+ return "nil" if value.nil?
19
+
20
+ str = value.to_s
21
+ return "[FILTERED]" if str.length < 8
22
+
23
+ "[FILTERED:...#{str[-4..-1]}]"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "securerandom"
5
+
6
+ module Api2Convert
7
+ module Upload
8
+ # Uploads a local file to a job's per-job upload server.
9
+ #
10
+ # This step is intentionally hand-written — it is NOT described by the OpenAPI
11
+ # spec. It posts a `multipart/form-data` body (field `file`) to
12
+ # `{job.server}/upload-file/{job.id}` and authenticates with the per-job
13
+ # `X-Oc-Token` header — never the account API key. The body is streamed, so
14
+ # large files are not read into memory. Internal.
15
+ class FileUploader
16
+ def initialize(transport)
17
+ @transport = transport
18
+ end
19
+
20
+ # @param job [Model::Job] a staged job (created with `process: false`).
21
+ # @param file [String, Pathname, IO] a path, or an IO/StringIO of bytes.
22
+ # @return [Model::InputFile]
23
+ def upload(job, file, filename = nil)
24
+ if job.server.nil? || job.server.empty? || job.token.nil?
25
+ raise Api2Convert::Error,
26
+ "Cannot upload: the job has no upload server/token. " \
27
+ "Create the job with process=false and upload before starting it."
28
+ end
29
+
30
+ io, resolved_name, opened = resolve(file, filename)
31
+ seekable = io.respond_to?(:rewind)
32
+ url = "#{job.server.sub(%r{/+\z}, "")}/upload-file/#{job.id}"
33
+ token = job.token
34
+ boundary = "----Api2Convert#{SecureRandom.hex(16)}"
35
+
36
+ build = lambda do
37
+ io.rewind if seekable
38
+ body_stream, length = MultipartStream.build(boundary, "file", resolved_name, io)
39
+ @transport.build_request(
40
+ "POST", url,
41
+ headers: {
42
+ "X-Oc-Token" => token,
43
+ "Content-Type" => "multipart/form-data; boundary=#{boundary}"
44
+ },
45
+ body_stream: body_stream,
46
+ content_length: length
47
+ )
48
+ end
49
+
50
+ begin
51
+ response = @transport.send_request(build, replayable: seekable)
52
+ Model::InputFile.from_hash(@transport.interpret(response))
53
+ ensure
54
+ opened&.close
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # Returns `[io, filename, opened]` where +opened+ is the handle to close
61
+ # afterwards (nil when the caller owns the IO). A path/Pathname is opened in
62
+ # binary mode; any other IO without a known byte size is buffered so the
63
+ # multipart Content-Length can be computed.
64
+ def resolve(file, filename)
65
+ if file.is_a?(String) || file.respond_to?(:to_path)
66
+ raw_path = file.respond_to?(:to_path) ? file.to_path : file
67
+ path = File.realpath(raw_path)
68
+ raise Api2Convert::Error, "Input file not found: #{file}" unless File.file?(path)
69
+
70
+ # Opened without a block on purpose: the handle is streamed by the
71
+ # transport and closed in upload's ensure clause.
72
+ handle = File.open(path, "rb") # rubocop:disable Style/FileOpen
73
+ # Mirror the null-coalesce: only nil falls back to the default, so an
74
+ # explicit "" filename is preserved as given.
75
+ name = filename.nil? ? File.basename(path) : filename
76
+ return [handle, name, handle]
77
+ end
78
+
79
+ io = file
80
+ io = StringIO.new(io.read) unless io.respond_to?(:size) && io.respond_to?(:rewind)
81
+ [io, filename.nil? ? "file" : filename, nil]
82
+ rescue Errno::ENOENT
83
+ raise Api2Convert::Error, "Input file not found: #{file}"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Api2Convert
6
+ module Upload
7
+ # A read-only IO that concatenates a multipart preamble, the file body IO, and
8
+ # the trailing boundary — so the body is streamed to the socket rather than
9
+ # loaded into memory. `Net::HTTP#body_stream=` reads it via `#read(len)`.
10
+ class MultipartStream
11
+ # Build the streamed body for a single `file` field. Returns
12
+ # `[stream, content_length]`; the caller sets `Content-Length` from the
13
+ # length so `Net::HTTP` does not fall back to chunked encoding.
14
+ #
15
+ # +io+ must be positioned at the start and expose a byte {#size} (a File or
16
+ # a StringIO both do — the uploader coerces anything else into a StringIO).
17
+ def self.build(boundary, field, filename, io)
18
+ disposition = %(form-data; name="#{escape(field)}"; filename="#{escape(filename)}")
19
+ preamble = [
20
+ "--#{boundary}",
21
+ "Content-Disposition: #{disposition}",
22
+ "Content-Type: application/octet-stream",
23
+ "",
24
+ ""
25
+ ].join("\r\n").b
26
+ epilogue = "\r\n--#{boundary}--\r\n".b
27
+
28
+ body_size = io.size - (io.respond_to?(:pos) ? io.pos : 0)
29
+ length = preamble.bytesize + body_size + epilogue.bytesize
30
+ [new(preamble, io, epilogue), length]
31
+ end
32
+
33
+ # Strip quotes and CR/LF from a header parameter so it can't break out of
34
+ # the multipart header (header-injection defense).
35
+ def self.escape(value)
36
+ value.to_s.gsub('"', "%22").gsub(/[\r\n]/, "")
37
+ end
38
+
39
+ def initialize(preamble, io, epilogue)
40
+ @parts = [StringIO.new(preamble), io, StringIO.new(epilogue)]
41
+ @index = 0
42
+ end
43
+
44
+ # Read up to +length+ bytes across the parts. With no length, reads all that
45
+ # remains. Returns nil at end of stream (the contract `Net::HTTP` expects).
46
+ def read(length = nil, outbuf = nil)
47
+ buffer = outbuf || "".b
48
+ buffer.clear
49
+
50
+ if length.nil?
51
+ while @index < @parts.length
52
+ chunk = @parts[@index].read
53
+ buffer << chunk unless chunk.nil?
54
+ @index += 1
55
+ end
56
+ return buffer
57
+ end
58
+
59
+ while buffer.bytesize < length && @index < @parts.length
60
+ chunk = @parts[@index].read(length - buffer.bytesize)
61
+ if chunk.nil?
62
+ @index += 1
63
+ else
64
+ buffer << chunk
65
+ end
66
+ end
67
+
68
+ return nil if buffer.empty? && @index >= @parts.length
69
+
70
+ buffer
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api2Convert
4
+ # The SDK version. Kept in lock-step with the sibling SDKs (PHP, Python, Java,
5
+ # Node.js): all official ports implement the same `docs/SDK_CONTRACT.md` and
6
+ # version together.
7
+ VERSION = "10.2.0"
8
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api2Convert
4
+ module Webhook
5
+ # A verified webhook callback. The API posts the job whose status changed.
6
+ class Event
7
+ # @return [Model::Job] the job whose status changed.
8
+ attr_reader :job
9
+ # @return [Hash] the full decoded callback body.
10
+ attr_reader :payload
11
+
12
+ def initialize(job, payload)
13
+ @job = job
14
+ @payload = payload
15
+ freeze
16
+ end
17
+
18
+ def self.from_hash(payload)
19
+ new(Model::Job.from_hash(payload), payload)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+
6
+ module Api2Convert
7
+ module Webhook
8
+ # Webhook callback verification and parsing.
9
+ #
10
+ # Pass the **raw** request body (the exact string received) so signature
11
+ # verification is byte-exact. Verification uses HMAC-SHA256 and matches the
12
+ # server's signed-webhooks scheme; until signed webhooks are enabled on your
13
+ # account no signature is sent — use {#parse} then, or call {#construct_event}
14
+ # with an empty secret to skip verification.
15
+ class Verifier
16
+ # Verify the signature (when a secret is given) and return the typed event.
17
+ #
18
+ # +payload+ must be the raw request body. +signature+ is the value of the
19
+ # signature header (`X-Oc-Signature`). Pass an empty +secret+ to skip
20
+ # verification. Raises {SignatureVerificationError} when the signature is
21
+ # missing or does not match.
22
+ def construct_event(payload, signature, secret)
23
+ unless secret.nil? || secret == ""
24
+ if signature.nil? || signature == ""
25
+ raise SignatureVerificationError, "Missing webhook signature header."
26
+ end
27
+
28
+ expected = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, to_bytes(payload))
29
+ unless secure_compare(expected, signature)
30
+ raise SignatureVerificationError, "Webhook signature verification failed."
31
+ end
32
+ end
33
+
34
+ parse(payload)
35
+ end
36
+
37
+ # Parse a callback body into a typed event WITHOUT verifying a signature.
38
+ # Only use this when signed webhooks are not yet enabled for your account.
39
+ def parse(payload)
40
+ begin
41
+ decoded = JSON.parse(to_bytes(payload))
42
+ rescue JSON::ParserError => e
43
+ raise SignatureVerificationError, "Webhook payload is not valid JSON: #{e.message}"
44
+ end
45
+ raise SignatureVerificationError, "Webhook payload is not a JSON object." unless decoded.is_a?(Hash)
46
+
47
+ Event.from_hash(decoded)
48
+ end
49
+
50
+ private
51
+
52
+ def to_bytes(payload)
53
+ payload.is_a?(String) ? payload : payload.to_s
54
+ end
55
+
56
+ # Constant-time comparison, guarding the length check first. Uses OpenSSL's
57
+ # fixed-length compare when available (Ruby 2.6+); otherwise a portable
58
+ # branch-free fallback so old Rubies stay constant-time too.
59
+ def secure_compare(expected, actual)
60
+ a = expected.to_s.b
61
+ b = actual.to_s.b
62
+ return false unless a.bytesize == b.bytesize
63
+
64
+ if OpenSSL.respond_to?(:fixed_length_secure_compare)
65
+ OpenSSL.fixed_length_secure_compare(a, b)
66
+ else
67
+ result = 0
68
+ a.bytesize.times { |i| result |= a.getbyte(i) ^ b.getbyte(i) }
69
+ result.zero?
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Official Ruby SDK for the API2Convert file-conversion API.
4
+ #
5
+ # Convert, compress and transform images, documents, audio, video, ebooks,
6
+ # archives and CAD — and run operations like OCR, merge, thumbnail and website
7
+ # capture — in one line of code:
8
+ #
9
+ # require "api2convert"
10
+ #
11
+ # client = Api2Convert::Client.new("YOUR_API_KEY")
12
+ # client.convert("invoice.docx", "pdf").save("invoice.pdf")
13
+ #
14
+ # It is one of five official ports (PHP, Python, Java, Node.js, Ruby) that all
15
+ # implement the same language-agnostic contract in `docs/SDK_CONTRACT.md`.
16
+ module Api2Convert
17
+ end
18
+
19
+ require_relative "api2convert/version"
20
+ require_relative "api2convert/errors"
21
+ require_relative "api2convert/config"
22
+ require_relative "api2convert/support/data"
23
+ require_relative "api2convert/support/secret"
24
+ require_relative "api2convert/job_status"
25
+ require_relative "api2convert/input_type"
26
+
27
+ require_relative "api2convert/model/status"
28
+ require_relative "api2convert/model/conversion"
29
+ require_relative "api2convert/model/input_file"
30
+ require_relative "api2convert/model/output_file"
31
+ require_relative "api2convert/model/job_message"
32
+ require_relative "api2convert/model/preset"
33
+ require_relative "api2convert/model/job"
34
+
35
+ require_relative "api2convert/http/request"
36
+ require_relative "api2convert/http/response"
37
+ require_relative "api2convert/http/net_http_sender"
38
+ require_relative "api2convert/http/transport"
39
+
40
+ require_relative "api2convert/upload/multipart_stream"
41
+ require_relative "api2convert/upload/file_uploader"
42
+
43
+ require_relative "api2convert/resource/jobs"
44
+ require_relative "api2convert/resource/conversions"
45
+ require_relative "api2convert/resource/presets"
46
+ require_relative "api2convert/resource/stats"
47
+ require_relative "api2convert/resource/contracts"
48
+
49
+ require_relative "api2convert/webhook/event"
50
+ require_relative "api2convert/webhook/verifier"
51
+
52
+ require_relative "api2convert/result"
53
+ require_relative "api2convert/client"
54
+
55
+ module Api2Convert
56
+ # Webhook verifier — usable without a configured client.
57
+ #
58
+ # event = Api2Convert.webhooks.construct_event(raw_body, signature, secret)
59
+ def self.webhooks
60
+ Webhook::Verifier.new
61
+ end
62
+ end