Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.1.0-alpha.27"
".": "0.1.0-alpha.28"
}
5 changes: 5 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ Layout/MultilineMethodParameterLineBreaks:
Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: no_space

# This option occasionally mangles identifier names
Lint/DeprecatedConstants:
Exclude:
- "**/*.rbi"

# Fairly useful in tests for pattern assertions.
Lint/EmptyInPattern:
Exclude:
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## 0.1.0-alpha.28 (2025-03-18)

Full Changelog: [v0.1.0-alpha.27...v0.1.0-alpha.28](https://github.com/orbcorp/orb-ruby/compare/v0.1.0-alpha.27...v0.1.0-alpha.28)

### ⚠ BREAKING CHANGES

* **model:** base model should recursively store coerced base models ([#165](https://github.com/orbcorp/orb-ruby/issues/165))

### Bug Fixes

* **model:** base model should recursively store coerced base models ([#165](https://github.com/orbcorp/orb-ruby/issues/165)) ([da71413](https://github.com/orbcorp/orb-ruby/commit/da71413184f24adcd89ce5bf302077a29193c14c))


### Chores

* do not label modules as abstract ([#163](https://github.com/orbcorp/orb-ruby/issues/163)) ([ac4e54c](https://github.com/orbcorp/orb-ruby/commit/ac4e54cf7a68dae174ee27b6477fd5d1a6a6b98a))
* **internal:** codegen related update ([#160](https://github.com/orbcorp/orb-ruby/issues/160)) ([2efe526](https://github.com/orbcorp/orb-ruby/commit/2efe526cc8937e1e6c3e334edc1f09ca0b0060d8))
* **internal:** codegen related update ([#164](https://github.com/orbcorp/orb-ruby/issues/164)) ([8b4921d](https://github.com/orbcorp/orb-ruby/commit/8b4921d27329bfbb3d4a27c8105d3dfa963b744d))
* use generics instead of overloading for sorbet type definitions ([#162](https://github.com/orbcorp/orb-ruby/issues/162)) ([9cec19b](https://github.com/orbcorp/orb-ruby/commit/9cec19b1e001799c2b69e68d37405a30d7f85432))

## 0.1.0-alpha.27 (2025-03-14)

Full Changelog: [v0.1.0-alpha.26...v0.1.0-alpha.27](https://github.com/orbcorp/orb-ruby/compare/v0.1.0-alpha.26...v0.1.0-alpha.27)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ GIT
PATH
remote: .
specs:
orb (0.1.0.pre.alpha.27)
orb (0.1.0.pre.alpha.28)
connection_pool

GEM
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The Orb Ruby library provides convenient access to the Orb REST API from any Rub

## Documentation

Documentation for the most recent release of this gem can be found [on RubyDoc](https://gemdocs.org/gems/orb/latest).
Documentation for released of this gem can be found [on RubyDoc](https://gemdocs.org/gems/orb).

The underlying REST API documentation can be found on [docs.withorb.com](https://docs.withorb.com/reference/api-reference).

Expand Down Expand Up @@ -142,6 +142,8 @@ What this means is that while you can use Sorbet to type check your code statica

Due to limitations with the Sorbet type system, where a method otherwise can take an instance of `Orb::BaseModel` class, you will need to use the `**` splat operator to pass the arguments:

Please follow Sorbet's [setup guides](https://sorbet.org/docs/adopting) for best experience.

```ruby
model = CustomerCreateParams.new(email: "example-customer@withorb.com", name: "My Customer")

Expand Down
47 changes: 25 additions & 22 deletions lib/orb/base_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ def follow_redirect(request, status:, response_headers:)

request
end

# @api private
#
# @param status [Integer, Orb::APIConnectionError]
# @param stream [Enumerable, nil]
def reap_connection!(status, stream:)
case status
in (..199) | (300..499)
stream&.each { next }
in Orb::APIConnectionError | (500..)
Orb::Util.close_fused!(stream)
else
end
end
end

# @api private
Expand Down Expand Up @@ -321,28 +335,23 @@ def initialize(
end

begin
response, stream = @requester.execute(input)
status = Integer(response.code)
status, response, stream = @requester.execute(input)
rescue Orb::APIConnectionError => e
status = e
end

# normally we want to drain the response body and reuse the HTTP session by clearing the socket buffers
# unless we hit a server error
srv_fault = (500...).include?(status)

case status
in ..299
[status, response, stream]
in 300..399 if redirect_count >= self.class::MAX_REDIRECTS
message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
self.class.reap_connection!(status, stream: stream)

stream.each { next }
message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
raise Orb::APIConnectionError.new(url: url, message: message)
in 300..399
request = self.class.follow_redirect(request, status: status, response_headers: response)
self.class.reap_connection!(status, stream: stream)

stream.each { next }
request = self.class.follow_redirect(request, status: status, response_headers: response)
send_request(
request,
redirect_count: redirect_count + 1,
Expand All @@ -352,12 +361,10 @@ def initialize(
in Orb::APIConnectionError if retry_count >= max_retries
raise status
in (400..) if retry_count >= max_retries || !self.class.should_retry?(status, headers: response)
decoded = Orb::Util.decode_content(response, stream: stream, suppress_error: true)

if srv_fault
Orb::Util.close_fused!(stream)
else
stream.each { next }
decoded = Kernel.then do
Orb::Util.decode_content(response, stream: stream, suppress_error: true)
ensure
self.class.reap_connection!(status, stream: stream)
end

raise Orb::APIStatusError.for(
Expand All @@ -368,13 +375,9 @@ def initialize(
response: response
)
in (400..) | Orb::APIConnectionError
delay = retry_delay(response, retry_count: retry_count)
self.class.reap_connection!(status, stream: stream)

if srv_fault
Orb::Util.close_fused!(stream)
else
stream&.each { next }
end
delay = retry_delay(response, retry_count: retry_count)
sleep(delay)

send_request(
Expand Down
27 changes: 23 additions & 4 deletions lib/orb/base_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

module Orb
# @api private
#
# @abstract
module Converter
# rubocop:disable Lint/UnusedMethodArgument

Expand Down Expand Up @@ -915,6 +913,13 @@ def known_fields
@known_fields ||= (self < Orb::BaseModel ? superclass.known_fields.dup : {})
end

# @api private
#
# @return [Hash{Symbol=>Symbol}]
def reverse_map
@reverse_map ||= (self < Orb::BaseModel ? superclass.reverse_map.dup : {})
end

# @api private
#
# @return [Hash{Symbol=>Hash{Symbol=>Object}}]
Expand Down Expand Up @@ -958,7 +963,7 @@ def defaults = (@defaults ||= {})
fallback = info[:const]
defaults[name_sym] = fallback if required && !info[:nil?] && info.key?(:const)

key = info.fetch(:api_name, name_sym)
key = info[:api_name]&.tap { reverse_map[_1] = name_sym } || name_sym
setter = "#{name_sym}="

if known_fields.key?(name_sym)
Expand Down Expand Up @@ -1215,7 +1220,21 @@ def deconstruct_keys(keys)
def initialize(data = {})
case Orb::Util.coerce_hash(data)
in Hash => coerced
@data = coerced.transform_keys(&:to_sym)
@data = coerced.to_h do |key, value|
name = key.to_sym
mapped = self.class.reverse_map.fetch(name, name)
type = self.class.fields[mapped]&.fetch(:type)
stored =
case [type, value]
in [Class, Hash] if type <= Orb::BaseModel
type.new(value)
in [Orb::ArrayOf, Array] | [Orb::HashOf, Hash]
type.coerce(value)
else
value
end
[name, stored]
end
else
raise ArgumentError.new("Expected a #{Hash} or #{Orb::BaseModel}, got #{data.inspect}")
end
Expand Down
2 changes: 0 additions & 2 deletions lib/orb/base_page.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# frozen_string_literal: true

module Orb
# @abstract
#
# @example
# ```ruby
# if page.has_next?
Expand Down Expand Up @@ -53,7 +51,7 @@
# @param req [Hash{Symbol=>Object}]
# @param headers [Hash{String=>String}, Net::HTTPHeader]
# @param page_data [Object]
def initialize(client:, req:, headers:, page_data:)

Check warning on line 54 in lib/orb/base_page.rb

View workflow job for this annotation

GitHub Actions / lint

Lint/UnusedMethodArgument: Unused method argument - `headers`.

Check warning on line 54 in lib/orb/base_page.rb

View workflow job for this annotation

GitHub Actions / lint

Lint/UnusedMethodArgument: Unused method argument - `page_data`.
@client = client
@req = req
super()
Expand Down
2 changes: 0 additions & 2 deletions lib/orb/extern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

module Orb
# @api private
#
# @abstract
module Extern
end
end
3 changes: 2 additions & 1 deletion lib/orb/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
# @return [Orb::Page]
def next_page
unless next_page?
raise RuntimeError.new("No more pages available. Please check #next_page? before calling ##{__method__}")
message = "No more pages available. Please check #next_page? before calling ##{__method__}"
raise RuntimeError.new(message)
end

req = Orb::Util.deep_merge(@req, {query: {cursor: pagination_metadata&.next_cursor}})
Expand All @@ -90,7 +91,7 @@

# @return [String]
def inspect
"#<#{self.class}:0x#{object_id.to_s(16)} data=#{data.inspect} pagination_metadata=#{pagination_metadata.inspect}>"

Check warning on line 94 in lib/orb/page.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/LineLength: Line is too long. [120/110]
end

class PaginationMetadata < Orb::BaseModel
Expand Down
19 changes: 12 additions & 7 deletions lib/orb/pooled_net_requester.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def build_request(request, &)

case body
in nil
nil
in String
req["content-length"] ||= body.bytesize.to_s unless req["transfer-encoding"]
req.body_stream = Orb::Util::ReadIOAdapter.new(body, &)
Expand All @@ -79,17 +80,19 @@ def build_request(request, &)
# @api private
#
# @param url [URI::Generic]
# @param deadline [Float]
# @param blk [Proc]
private def with_pool(url, &)
private def with_pool(url, deadline:, &blk)
origin = Orb::Util.uri_origin(url)
timeout = deadline - Orb::Util.monotonic_secs
pool =
@mutex.synchronize do
@pools[origin] ||= ConnectionPool.new(size: @size) do
self.class.connect(url)
end
end

pool.with(&)
pool.with(timeout: timeout, &blk)
end

# @api private
Expand All @@ -106,14 +109,14 @@ def build_request(request, &)
#
# @option request [Float] :deadline
#
# @return [Array(Net::HTTPResponse, Enumerable)]
# @return [Array(Integer, Net::HTTPResponse, Enumerable)]
def execute(request)
url, deadline = request.fetch_values(:url, :deadline)

eof = false
finished = false
enum = Enumerator.new do |y|
with_pool(url) do |conn|
with_pool(url, deadline: deadline) do |conn|
next if finished

req = self.class.build_request(request) do
Expand All @@ -125,7 +128,7 @@ def execute(request)

self.class.calibrate_socket_timeout(conn, deadline)
conn.request(req) do |rsp|
y << [conn, rsp]
y << [conn, req, rsp]
break if finished

rsp.read_body do |bytes|
Expand All @@ -137,9 +140,11 @@ def execute(request)
eof = true
end
end
rescue Timeout::Error
raise Orb::APITimeoutError
end

conn, response = enum.next
conn, _, response = enum.next
body = Orb::Util.fused_enum(enum, external: true) do
finished = true
tap do
Expand All @@ -149,7 +154,7 @@ def execute(request)
end
conn.finish if !eof && conn&.started?
end
[response, (response.body = body)]
[Integer(response.code), response, (response.body = body)]
end

# @api private
Expand Down
2 changes: 0 additions & 2 deletions lib/orb/request_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

module Orb
# @api private
#
# @abstract
module RequestParameters
# @!parse
# # Options to specify HTTP behaviour for this request.
Expand Down
4 changes: 3 additions & 1 deletion lib/orb/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
#
# @param input [Object]
#
# @return [Boolean, Object]
# @return [Boolean]
def primitive?(input)
case input
in true | false | Integer | Float | Symbol | String
Expand Down Expand Up @@ -458,7 +458,7 @@
# @param boundary [String]
# @param key [Symbol, String]
# @param val [Object]
private def encode_multipart_formdata(y, boundary:, key:, val:)

Check warning on line 461 in lib/orb/util.rb

View workflow job for this annotation

GitHub Actions / lint

Naming/MethodParameterName: Method parameter must be at least 3 characters long.
y << "--#{boundary}\r\n"
y << "Content-Disposition: form-data"
unless key.nil?
Expand Down Expand Up @@ -627,6 +627,8 @@
#
# @param enum [Enumerable, nil]
# @param blk [Proc]
#
# @return [Enumerable]
def chain_fused(enum, &blk)
iter = Enumerator.new { blk.call(_1) }
fused_enum(iter) { close_fused!(enum) }
Expand Down
2 changes: 1 addition & 1 deletion lib/orb/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Orb
VERSION = "0.1.0-alpha.27"
VERSION = "0.1.0-alpha.28"
end
2 changes: 1 addition & 1 deletion orb.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
s.email = "team@withorb.com"
s.files = Dir["lib/**/*.rb", "rbi/**/*.rbi", "sig/**/*.rbs", "manifest.yaml"]
s.extra_rdoc_files = ["README.md"]
s.required_ruby_version = ">= 3.0.0"

Check warning on line 13 in orb.gemspec

View workflow job for this annotation

GitHub Actions / lint

Gemspec/RequiredRubyVersion: `required_ruby_version` and `TargetRubyVersion` (3.1, which may be specified in .rubocop.yml) should be equal.
s.add_dependency "connection_pool"
s.homepage = "https://gemdocs.org/gems/orb/latest"
s.homepage = "https://gemdocs.org/gems/orb"
s.metadata["homepage_uri"] = s.homepage
s.metadata["source_code_uri"] = "https://github.com/orbcorp/orb-ruby"
s.metadata["rubygems_mfa_required"] = "false"

Check warning on line 18 in orb.gemspec

View workflow job for this annotation

GitHub Actions / lint

Gemspec/RequireMFA: `metadata['rubygems_mfa_required']` must be set to `'true'`.
end
8 changes: 8 additions & 0 deletions rbi/lib/orb/base_client.rbi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# typed: strong

module Orb
# @api private
class BaseClient
abstract!

Expand Down Expand Up @@ -67,6 +68,13 @@ module Orb
end
def follow_redirect(request, status:, response_headers:)
end

# @api private
sig do
params(status: T.any(Integer, Orb::APIConnectionError), stream: T.nilable(T::Enumerable[String])).void
end
def reap_connection!(status, stream:)
end
end

sig { returns(T.anything) }
Expand Down
Loading
Loading