diff --git a/.gitignore b/.gitignore index 8b1228a8..3d26ceed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ *.gem .idea/ +.ignore .prism.log .ruby-lsp/ .yardoc/ -Brewfile.lock.json bin/tapioca +Brewfile.lock.json doc/ -sorbet/* -!/sorbet/config +sorbet/tapioca/* diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4c5a1a01..10f30916 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.3" + ".": "0.2.0" } \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index e924c263..4af3d69a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -244,6 +244,7 @@ Style/SafeNavigation: Style/SignalException: Exclude: - Rakefile + - "**/*.rake" # We use these sparingly, where we anticipate future branches for the # inner conditional. diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..fd2a0186 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index a4b80c0b..29068b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 0.2.0 (2025-04-17) + +Full Changelog: [v0.1.3...v0.2.0](https://github.com/orbcorp/orb-ruby/compare/v0.1.3...v0.2.0) + +### Features + +* **client:** enable setting base URL from environment variable ([a472f4c](https://github.com/orbcorp/orb-ruby/commit/a472f4cdee0d969b7e240a9519acccd3877f3de6)) + + +### Bug Fixes + +* always send idempotency header when specified as a request option ([f570e93](https://github.com/orbcorp/orb-ruby/commit/f570e93e8fe1a2f6d6df495021ea1152aeaf1e58)) + + +### Chores + +* **internal:** always run post-processing when formatting when syntax_tree ([9df6d63](https://github.com/orbcorp/orb-ruby/commit/9df6d6382e67f6f28f1cedff4ee1c949ffd70237)) +* **internal:** codegen related update ([202fff4](https://github.com/orbcorp/orb-ruby/commit/202fff413ad9e9be16b67a07270dd4ac2c4f14e4)) +* **internal:** codegen related update ([2b99ae2](https://github.com/orbcorp/orb-ruby/commit/2b99ae2ea108ad4e6227bd5ab7e9a7dd064a8ded)) +* **internal:** contribute.md and contributor QoL improvements ([cb204de](https://github.com/orbcorp/orb-ruby/commit/cb204de43549ee6aa9ead2d1e071ec5df0c58cb2)) +* **internal:** loosen internal type restrictions ([9dc6b52](https://github.com/orbcorp/orb-ruby/commit/9dc6b52d1bfa57e3fab328bf8673872522ab7f25)) +* **internal:** minor touch ups on sdk internals ([9297be8](https://github.com/orbcorp/orb-ruby/commit/9297be8eaa459f14cb0b4118066ecd59877686ab)) +* **internal:** protect SSE parsing pipeline from broken UTF-8 characters ([bb2243a](https://github.com/orbcorp/orb-ruby/commit/bb2243a19fbd077687be4b2a4a001e4b7381c54d)) +* **internal:** version bump ([c664f2f](https://github.com/orbcorp/orb-ruby/commit/c664f2fb5019cabca07ce026410d6951f9412d69)) +* refine `#inspect` and `#to_s` for model classes ([86f8280](https://github.com/orbcorp/orb-ruby/commit/86f8280e1126e5bd4013f4c1e5ddd09190044fa3)) + + +### Documentation + +* update documentation links to be more uniform ([a0bfe42](https://github.com/orbcorp/orb-ruby/commit/a0bfe42c34e09819e4948069670cfdf895cb51af)) + ## 0.1.3 (2025-04-11) Full Changelog: [v0.1.2...v0.1.3](https://github.com/orbcorp/orb-ruby/compare/v0.1.2...v0.1.3) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..2058a521 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,132 @@ +## Setting up the environment + +This repository contains a `.ruby-version` file, which should work with either [rbenv](https://github.com/rbenv/rbenv) or [asdf](https://github.com/asdf-vm/asdf) with the [ruby plugin](https://github.com/asdf-vm/asdf-ruby). + +Please follow the instructions for your preferred version manager to install the Ruby version specified in the `.ruby-version` file. + +To set up the repository, run: + +```bash +$ ./scripts/bootstrap +``` + +This will install all the required dependencies. + +## Modifying/Adding code + +Most of the SDK is generated code. Modifications to code will be persisted between generations, but may result in merge conflicts between manual patches and changes from the generator. The generator will never modify the contents `examples/` directory. + +## Adding and running examples + +All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. + +```ruby +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../lib/orb" + +# ... +``` + +```bash +$ chmod +x './examples/.rb' + +# run the example against your api +$ ruby './examples/.rb' +``` + +## Using the repository from source + +If you’d like to use the repository from source, you can either install from git or reference a cloned repository: + +To install via git in your `Gemfile`: + +```ruby +gem "orb-billing", git: "https://www.github.com/orbcorp/orb-ruby" +``` + +Alternatively, reference local copy of the repo: + +```bash +$ git clone -- 'https://www.github.com/orbcorp/orb-ruby' '' +``` + +```ruby +gem "orb-billing", path: "" +``` + +## Running commands + +Running `rake` by itself will show all runnable commands. + +```bash +$ bundle exec rake +``` + +## Running tests + +Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. + +```bash +$ npx prism mock path/to/your/openapi.yml +``` + +```bash +$ bundle exec rake test +``` + +## Linting and formatting + +This repository uses [rubocop](https://github.com/rubocop/rubocop) for linting and formatting of `*.rb` and `*.rbi` files. [syntax_tree](https://github.com/ruby-syntax-tree/syntax_tree) is used for formatting `*.rbs` files. + +There are two separate type checkers supported by this library: [sorbet](https://github.com/sorbet/sorbet) and [steep](https://github.com/soutaro/steep) are used for verifying `*.rbi` and `*.rbs` files respectively. + +To lint and typecheck: + +```bash +$ bundle exec rake lint +``` + +To format and fix all lint issues automatically: + +```bash +$ bundle exec rake format +``` + +## Editor Support + +### Solargraph + +This library includes [Solargraph](https://solargraph.org) support for both auto-completion and go to definition. + +```ruby +gem "solargraph", group: :development +``` + +Note: if you had installed the gem locally using `git: "..."` or `path: "..."`, you must update your [`.solargraph.yml`](https://solargraph.org/guides/configuration) to include the path to where the gem is located: + +```yaml +include: + - '/lib/**/*.rb' +``` + +### Sorbet + +[Sorbet](https://sorbet.org) should mostly work out of the box when editing this library directly. However, there are a some caveats due to the colocation of `*.rb` and `*.rbi` files in the same project. These issues should not otherwise manifest when this library is used as a dependency. + +1. For go to definition usages, sorbet might get confused and may not always navigate to the correct location. + +2. For each generic type in `*.rbi` files, a spurious "Duplicate type member" error is present. + +### Ruby LSP + +The Ruby LSP has [best effort support](https://shopify.github.io/ruby-lsp/#guessed-types) for inferring type information from Ruby code, and as such it may not always be able to provide accurate type information. + +## Documentation Preview + +To preview the documentation, run: + +```bash +$ bundle exec rake docs:preview [PORT=8808] +``` diff --git a/Gemfile.lock b/Gemfile.lock index 8e35716f..1dd82a82 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GIT PATH remote: . specs: - orb-billing (0.1.2) + orb-billing (0.1.3) connection_pool GEM diff --git a/README.md b/README.md index 375bca97..27555d12 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ The Orb Ruby library provides convenient access to the Orb REST API from any Rub ## Documentation -Documentation for released of this gem can be found [on RubyDoc](https://gemdocs.org/gems/orb-billing). +Documentation for releases of this gem can be found [on RubyDoc](https://gemdocs.org/gems/orb-billing). -The underlying REST API documentation can be found on [docs.withorb.com](https://docs.withorb.com/reference/api-reference). +The REST API documentation can be found on [docs.withorb.com](https://docs.withorb.com/reference/api-reference). ## Installation @@ -15,7 +15,7 @@ To use this gem, install via Bundler by adding the following to your application ```ruby -gem "orb-billing", "~> 0.1.3" +gem "orb-billing", "~> 0.2.0" ``` diff --git a/Rakefile b/Rakefile index 4efefa43..433bf6f4 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "pathname" require "securerandom" require "shellwords" @@ -7,10 +8,23 @@ require "minitest/test_task" require "rake/clean" require "rubocop/rake_task" -CLEAN.push(*%w[.idea/ .ruby-lsp/ .yardoc/]) +tapioca = "sorbet/tapioca" +ignore_file = ".ignore" -multitask(default: [:test]) +CLEAN.push(*%w[.idea/ .ruby-lsp/ .yardoc/ doc/], *FileList["*.gem"], ignore_file) +CLOBBER.push(*%w[sorbet/rbi/annotations/ sorbet/rbi/gems/], tapioca) + +multitask(:default) do + sh(*%w[rake --tasks]) +end + +desc("Preview docs; use `PORT=` to change the port") +multitask(:"docs:preview") do + sh(*%w[yard server --bind [::] --reload --quiet --port], ENV.fetch("PORT", "8808")) +end + +desc("Run test suites; use `TEST=path/to/test.rb` to run a specific test file") multitask(:test) do rb = FileList[ENV.fetch("TEST", "./test/**/*_test.rb")] @@ -23,17 +37,20 @@ end rubo_find = %w[find ./lib ./test ./rbi -type f -and ( -name *.rb -or -name *.rbi ) -print0] xargs = %w[xargs --no-run-if-empty --null --max-procs=0 --max-args=300 --] -multitask(:rubocop) do +desc("Lint `*.rb(i)`") +multitask(:"lint:rubocop") do lint = xargs + %w[rubocop --fail-level E] + (ENV.key?("CI") ? %w[--format github] : []) sh("#{rubo_find.shelljoin} | #{lint.shelljoin}") end -multitask(:ruboformat) do +desc("Format `*.rb(i)`") +multitask(:"format:rubocop") do fmt = xargs + %w[rubocop --fail-level F --autocorrect --format simple --] sh("#{rubo_find.shelljoin} | #{fmt.shelljoin}") end -multitask(:syntax_tree) do +desc("Format `*.rbs`") +multitask(:"format:syntax_tree") do find = %w[find ./sig -type f -name *.rbs -print0] inplace = /darwin|bsd/ =~ RUBY_PLATFORM ? %w[-i''] : %w[-i] uuid = SecureRandom.uuid @@ -59,35 +76,64 @@ multitask(:syntax_tree) do # 2. at label `l1`, join previously annotated line with `class | module` information. pst = sed + [subst, "--"] + success = false + # transform class aliases to type aliases, which syntax tree has no trouble with sh("#{find.shelljoin} | #{pre.shelljoin}") # run syntax tree to format `*.rbs` files - sh("#{find.shelljoin} | #{fmt.shelljoin}") + sh("#{find.shelljoin} | #{fmt.shelljoin}") do + success = _1 + end # transform type aliases back to class aliases sh("#{find.shelljoin} | #{pst.shelljoin}") + + # always run post-processing to remove comment marker + fail unless success end -multitask(format: [:ruboformat, :syntax_tree]) +desc("Format everything") +multitask(format: [:"format:rubocop", :"format:syntax_tree"]) -multitask(:steep) do +desc("Typecheck `*.rbs`") +multitask(:"typecheck:steep") do sh(*%w[steep check]) end -multitask(:sorbet) do +desc("Typecheck `*.rbi`") +multitask(:"typecheck:sorbet") do sh(*%w[srb typecheck]) end -file("sorbet/tapioca") do +file(tapioca) do sh(*%w[tapioca init]) end -multitask(typecheck: [:steep, :sorbet]) -multitask(lint: [:rubocop, :typecheck]) +desc("Typecheck everything") +multitask(typecheck: [:"typecheck:steep", :"typecheck:sorbet"]) + +desc("Lint everything") +multitask(lint: [:"lint:rubocop", :typecheck]) + +desc("Build yard docs") +multitask(:"build:docs") do + sh(*%w[yard]) +end -multitask(:build) do - sh(*%w[gem build -- orb.gemspec]) +desc("Build ruby gem") +multitask(:"build:gem") do + # optimizing for grepping through the gem bundle: many tools honour `.ignore` files, including VSCode + # + # both `rbi` and `sig` directories are navigable by their respective tool chains and therefore can be ignored by tools such as `rg` + Pathname(ignore_file).write(<<~GLOB) + rbi/* + sig/* + GLOB + + sh(*%w[gem build -- openai.gemspec]) + rm_rf(ignore_file) end -multitask(release: [:build]) do +desc("Release ruby gem") +multitask(release: [:"build:gem"]) do sh(*%w[gem push], *FileList["orb-*.gem"]) end diff --git a/lib/orb/client.rb b/lib/orb/client.rb index 318b4994..7c9504dc 100644 --- a/lib/orb/client.rb +++ b/lib/orb/client.rb @@ -76,7 +76,8 @@ class Client < Orb::Internal::Transport::BaseClient # # @param api_key [String, nil] Defaults to `ENV["ORB_API_KEY"]` # - # @param base_url [String, nil] Override the default base URL for the API, e.g., `"https://api.example.com/v2/"` + # @param base_url [String, nil] Override the default base URL for the API, e.g., + # `"https://api.example.com/v2/"`. Defaults to `ENV["ORB_BASE_URL"]` # # @param max_retries [Integer] Max number of retries to attempt after a failed retryable request. # @@ -89,7 +90,7 @@ class Client < Orb::Internal::Transport::BaseClient # @param idempotency_header [String] def initialize( api_key: ENV["ORB_API_KEY"], - base_url: nil, + base_url: ENV["ORB_BASE_URL"], max_retries: DEFAULT_MAX_RETRIES, timeout: DEFAULT_TIMEOUT_IN_SECONDS, initial_retry_delay: DEFAULT_INITIAL_RETRY_DELAY, diff --git a/lib/orb/internal.rb b/lib/orb/internal.rb index 0b2b7bbb..b67eff4e 100644 --- a/lib/orb/internal.rb +++ b/lib/orb/internal.rb @@ -3,6 +3,10 @@ module Orb # @api private module Internal - OMIT = Object.new.freeze + OMIT = + Object.new.tap do + _1.define_singleton_method(:inspect) { "#<#{Orb::Internal}::OMIT>" } + end + .freeze end end diff --git a/lib/orb/internal/page.rb b/lib/orb/internal/page.rb index 8d7c49fa..d57a2ad9 100644 --- a/lib/orb/internal/page.rb +++ b/lib/orb/internal/page.rb @@ -22,33 +22,6 @@ class Page # @return [PaginationMetadata] attr_accessor :pagination_metadata - # @api private - # - # @param client [Orb::Internal::Transport::BaseClient] - # @param req [Hash{Symbol=>Object}] - # @param headers [Hash{String=>String}, Net::HTTPHeader] - # @param page_data [Hash{Symbol=>Object}] - def initialize(client:, req:, headers:, page_data:) - super - model = req.fetch(:model) - - case page_data - in {data: Array | nil => data} - @data = data&.map { Orb::Internal::Type::Converter.coerce(model, _1) } - else - end - - case page_data - in {pagination_metadata: Hash | nil => pagination_metadata} - @pagination_metadata = - Orb::Internal::Type::Converter.coerce( - Orb::Internal::Page::PaginationMetadata, - pagination_metadata - ) - else - end - end - # @return [Boolean] def next_page? !pagination_metadata&.next_cursor.nil? @@ -73,19 +46,48 @@ def auto_paging_each(&blk) unless block_given? raise ArgumentError.new("A block must be given to ##{__method__}") end + page = self loop do - page.data&.each { blk.call(_1) } + page.data&.each(&blk) + break unless page.next_page? page = page.next_page end end + # @api private + # + # @param client [Orb::Internal::Transport::BaseClient] + # @param req [Hash{Symbol=>Object}] + # @param headers [Hash{String=>String}, Net::HTTPHeader] + # @param page_data [Hash{Symbol=>Object}] + def initialize(client:, req:, headers:, page_data:) + super + + case page_data + in {data: Array | nil => data} + @data = data&.map { Orb::Internal::Type::Converter.coerce(@model, _1) } + else + end + case page_data + in {pagination_metadata: Hash | nil => pagination_metadata} + @pagination_metadata = + Orb::Internal::Type::Converter.coerce( + Orb::Internal::Page::PaginationMetadata, + pagination_metadata + ) + else + end + end + + # @api private + # # @return [String] def inspect - # rubocop:disable Layout/LineLength - "#<#{self.class}:0x#{object_id.to_s(16)} data=#{data.inspect} pagination_metadata=#{pagination_metadata.inspect}>" - # rubocop:enable Layout/LineLength + model = Orb::Internal::Type::Converter.inspect(@model, depth: 1) + + "#<#{self.class}[#{model}]:0x#{object_id.to_s(16)}>" end class PaginationMetadata < Orb::Internal::Type::BaseModel diff --git a/lib/orb/internal/transport/base_client.rb b/lib/orb/internal/transport/base_client.rb index 83d779fc..400340c5 100644 --- a/lib/orb/internal/transport/base_client.rb +++ b/lib/orb/internal/transport/base_client.rb @@ -93,7 +93,11 @@ def follow_redirect(request, status:, response_headers:) URI.join(url, response_headers["location"]) rescue ArgumentError message = "Server responded with status #{status} but no valid location header." - raise Orb::Errors::APIConnectionError.new(url: url, message: message) + raise Orb::Errors::APIConnectionError.new( + url: url, + response: response_headers, + message: message + ) end request = {**request, url: location} @@ -101,7 +105,11 @@ def follow_redirect(request, status:, response_headers:) case [url.scheme, location.scheme] in ["https", "http"] message = "Tried to redirect to a insecure URL" - raise Orb::Errors::APIConnectionError.new(url: url, message: message) + raise Orb::Errors::APIConnectionError.new( + url: url, + response: response_headers, + message: message + ) else nil end @@ -245,7 +253,7 @@ def initialize( if @idempotency_header && !headers.key?(@idempotency_header) && - !Net::HTTP::IDEMPOTENT_METHODS_.include?(method.to_s.upcase) + (!Net::HTTP::IDEMPOTENT_METHODS_.include?(method.to_s.upcase) || opts.key?(:idempotency_key)) headers[@idempotency_header] = opts.fetch(:idempotency_key) { generate_idempotency_key } end @@ -350,7 +358,7 @@ def initialize( self.class.reap_connection!(status, stream: stream) message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects." - raise Orb::Errors::APIConnectionError.new(url: url, message: message) + raise Orb::Errors::APIConnectionError.new(url: url, response: response, message: message) in 300..399 self.class.reap_connection!(status, stream: stream) @@ -460,6 +468,8 @@ def request(req) end end + # @api private + # # @return [String] def inspect # rubocop:disable Layout/LineLength diff --git a/lib/orb/internal/transport/pooled_net_requester.rb b/lib/orb/internal/transport/pooled_net_requester.rb index 7db786fb..d9e8ee83 100644 --- a/lib/orb/internal/transport/pooled_net_requester.rb +++ b/lib/orb/internal/transport/pooled_net_requester.rb @@ -149,7 +149,7 @@ def execute(request) break if finished rsp.read_body do |bytes| - y << bytes + y << bytes.force_encoding(Encoding::BINARY) break if finished self.class.calibrate_socket_timeout(conn, deadline) diff --git a/lib/orb/internal/type/array_of.rb b/lib/orb/internal/type/array_of.rb index bcbf948e..d7dc22bd 100644 --- a/lib/orb/internal/type/array_of.rb +++ b/lib/orb/internal/type/array_of.rb @@ -13,6 +13,10 @@ module Type class ArrayOf include Orb::Internal::Type::Converter + private_class_method :new + + # @overload [](type_info, spec = {}) + # # @param type_info [Hash{Symbol=>Object}, Proc, Orb::Internal::Type::Converter, Class] # # @param spec [Hash{Symbol=>Object}] . @@ -24,7 +28,7 @@ class ArrayOf # @option spec [Proc] :union # # @option spec [Boolean] :"nil?" - def self.[](type_info, spec = {}) = new(type_info, spec) + def self.[](...) = new(...) # @param other [Object] # @@ -120,7 +124,18 @@ def dump(value, state:) # @option spec [Boolean] :"nil?" def initialize(type_info, spec = {}) @item_type_fn = Orb::Internal::Type::Converter.type_info(type_info || spec) - @nilable = spec[:nil?] + @nilable = spec.fetch(:nil?, false) + end + + # @api private + # + # @param depth [Integer] + # + # @return [String] + def inspect(depth: 0) + items = Orb::Internal::Type::Converter.inspect(item_type, depth: depth.succ) + + "#{self.class}[#{[items, nilable? ? 'nil' : nil].compact.join(' | ')}]" end end end diff --git a/lib/orb/internal/type/base_model.rb b/lib/orb/internal/type/base_model.rb index 41cba58c..eb73b89c 100644 --- a/lib/orb/internal/type/base_model.rb +++ b/lib/orb/internal/type/base_model.rb @@ -63,7 +63,7 @@ def fields setter = "#{name_sym}=" api_name = info.fetch(:api_name, name_sym) - nilable = info[:nil?] + nilable = info.fetch(:nil?, false) const = required && !nilable ? info.fetch(:const, Orb::Internal::OMIT) : Orb::Internal::OMIT [name_sym, setter].each { undef_method(_1) } if known_fields.key?(name_sym) @@ -338,6 +338,27 @@ def deconstruct_keys(keys) .to_h end + class << self + # @param model [Orb::Internal::Type::BaseModel] + # + # @return [Hash{Symbol=>Object}] + def walk(model) + walk = ->(x) do + case x + in Orb::Internal::Type::BaseModel + walk.call(x.to_h) + in Hash + x.transform_values(&walk) + in Array + x.map(&walk) + else + x + end + end + walk.call(model) + end + end + # @param a [Object] # # @return [String] @@ -361,15 +382,38 @@ def initialize(data = {}) end end - # @return [String] - def inspect - rows = self.class.known_fields.keys.map do - "#{_1}=#{@data.key?(_1) ? public_send(_1) : ''}" - rescue Orb::Errors::ConversionError - "#{_1}=#{@data.fetch(_1)}" + class << self + # @api private + # + # @param depth [Integer] + # + # @return [String] + def inspect(depth: 0) + return super() if depth.positive? + + depth = depth.succ + deferred = fields.transform_values do |field| + type, required, nilable = field.fetch_values(:type, :required, :nilable) + inspected = [ + Orb::Internal::Type::Converter.inspect(type, depth: depth), + !required || nilable ? "nil" : nil + ].compact.join(" | ") + -> { inspected }.tap { _1.define_singleton_method(:inspect) { call } } + end + + "#{name}[#{deferred.inspect}]" end - "#<#{self.class.name}:0x#{object_id.to_s(16)} #{rows.join(' ')}>" end + + # @api private + # + # @return [String] + def to_s = self.class.walk(@data).to_s + + # @api private + # + # @return [String] + def inspect = "#<#{self.class}:0x#{object_id.to_s(16)} #{self}>" end end end diff --git a/lib/orb/internal/type/base_page.rb b/lib/orb/internal/type/base_page.rb index ad092fd2..009ecf00 100644 --- a/lib/orb/internal/type/base_page.rb +++ b/lib/orb/internal/type/base_page.rb @@ -36,6 +36,7 @@ def to_enum = super(:auto_paging_each) def initialize(client:, req:, headers:, page_data:) @client = client @req = req + @model = req.fetch(:model) super() end diff --git a/lib/orb/internal/type/boolean.rb b/lib/orb/internal/type/boolean.rb index d1a6b900..c05da5d9 100644 --- a/lib/orb/internal/type/boolean.rb +++ b/lib/orb/internal/type/boolean.rb @@ -11,6 +11,8 @@ module Type class Boolean extend Orb::Internal::Type::Converter + private_class_method :new + # @param other [Object] # # @return [Boolean] diff --git a/lib/orb/internal/type/converter.rb b/lib/orb/internal/type/converter.rb index 04896cad..3675e242 100644 --- a/lib/orb/internal/type/converter.rb +++ b/lib/orb/internal/type/converter.rb @@ -49,6 +49,15 @@ def dump(value, state:) end end + # @api private + # + # @param depth [Integer] + # + # @return [String] + def inspect(depth: 0) + super() + end + # rubocop:enable Lint/UnusedMethodArgument class << self @@ -240,6 +249,21 @@ def dump(target, value, state: {can_retry: true}) Orb::Internal::Type::Unknown.dump(value, state: state) end end + + # @api private + # + # @param target [Object] + # @param depth [Integer] + # + # @return [String] + def inspect(target, depth:) + case target + in Orb::Internal::Type::Converter + target.inspect(depth: depth.succ) + else + target.inspect + end + end end end end diff --git a/lib/orb/internal/type/enum.rb b/lib/orb/internal/type/enum.rb index bd9592d0..e3ddcc6a 100644 --- a/lib/orb/internal/type/enum.rb +++ b/lib/orb/internal/type/enum.rb @@ -58,9 +58,9 @@ def ===(other) = values.include?(other) # # @return [Boolean] def ==(other) - # rubocop:disable Layout/LineLength - other.is_a?(Module) && other.singleton_class <= Orb::Internal::Type::Enum && other.values.to_set == values.to_set - # rubocop:enable Layout/LineLength + # rubocop:disable Style/CaseEquality + Orb::Internal::Type::Enum === other && other.values.to_set == values.to_set + # rubocop:enable Style/CaseEquality end # @api private @@ -103,6 +103,22 @@ def coerce(value, state:) # # # # @return [Symbol, Object] # def dump(value, state:) = super + + # @api private + # + # @param depth [Integer] + # + # @return [String] + def inspect(depth: 0) + if depth.positive? + return is_a?(Module) ? super() : self.class.name + end + + members = values.map { Orb::Internal::Type::Converter.inspect(_1, depth: depth.succ) } + prefix = is_a?(Module) ? name : self.class.name + + "#{prefix}[#{members.join(' | ')}]" + end end end end diff --git a/lib/orb/internal/type/hash_of.rb b/lib/orb/internal/type/hash_of.rb index 44173458..56073fbc 100644 --- a/lib/orb/internal/type/hash_of.rb +++ b/lib/orb/internal/type/hash_of.rb @@ -13,6 +13,10 @@ module Type class HashOf include Orb::Internal::Type::Converter + private_class_method :new + + # @overload [](type_info, spec = {}) + # # @param type_info [Hash{Symbol=>Object}, Proc, Orb::Internal::Type::Converter, Class] # # @param spec [Hash{Symbol=>Object}] . @@ -24,7 +28,7 @@ class HashOf # @option spec [Proc] :union # # @option spec [Boolean] :"nil?" - def self.[](type_info, spec = {}) = new(type_info, spec) + def self.[](...) = new(...) # @param other [Object] # @@ -140,7 +144,18 @@ def dump(value, state:) # @option spec [Boolean] :"nil?" def initialize(type_info, spec = {}) @item_type_fn = Orb::Internal::Type::Converter.type_info(type_info || spec) - @nilable = spec[:nil?] + @nilable = spec.fetch(:nil?, false) + end + + # @api private + # + # @param depth [Integer] + # + # @return [String] + def inspect(depth: 0) + items = Orb::Internal::Type::Converter.inspect(item_type, depth: depth.succ) + + "#{self.class}[#{[items, nilable? ? 'nil' : nil].compact.join(' | ')}]" end end end diff --git a/lib/orb/internal/type/io_like.rb b/lib/orb/internal/type/io_like.rb index fe821f55..7c5c56c7 100644 --- a/lib/orb/internal/type/io_like.rb +++ b/lib/orb/internal/type/io_like.rb @@ -11,6 +11,8 @@ module Type class IOLike extend Orb::Internal::Type::Converter + private_class_method :new + # @param other [Object] # # @return [Boolean] diff --git a/lib/orb/internal/type/union.rb b/lib/orb/internal/type/union.rb index 36474e0c..98cd9b31 100644 --- a/lib/orb/internal/type/union.rb +++ b/lib/orb/internal/type/union.rb @@ -140,9 +140,7 @@ def ===(other) # # @return [Boolean] def ==(other) - # rubocop:disable Layout/LineLength - other.is_a?(Module) && other.singleton_class <= Orb::Internal::Type::Union && other.derefed_variants == derefed_variants - # rubocop:enable Layout/LineLength + Orb::Internal::Type::Union === other && other.derefed_variants == derefed_variants end # @api private @@ -225,6 +223,22 @@ def dump(value, state:) # rubocop:enable Style/CaseEquality # rubocop:enable Style/HashEachMethods + + # @api private + # + # @param depth [Integer] + # + # @return [String] + def inspect(depth: 0) + if depth.positive? + return is_a?(Module) ? super() : self.class.name + end + + members = variants.map { Orb::Internal::Type::Converter.inspect(_1, depth: depth.succ) } + prefix = is_a?(Module) ? name : self.class.name + + "#{prefix}[#{members.join(' | ')}]" + end end end end diff --git a/lib/orb/internal/type/unknown.rb b/lib/orb/internal/type/unknown.rb index 36966379..0110bf0a 100644 --- a/lib/orb/internal/type/unknown.rb +++ b/lib/orb/internal/type/unknown.rb @@ -13,6 +13,8 @@ class Unknown # rubocop:disable Lint/UnusedMethodArgument + private_class_method :new + # @param other [Object] # # @return [Boolean] diff --git a/lib/orb/internal/util.rb b/lib/orb/internal/util.rb index 62195f31..6290e5f4 100644 --- a/lib/orb/internal/util.rb +++ b/lib/orb/internal/util.rb @@ -448,7 +448,7 @@ def initialize(src, &blk) else src end - @buf = String.new.b + @buf = String.new @blk = blk end end @@ -460,7 +460,7 @@ class << self # @return [Enumerable] def writable_enum(&blk) Enumerator.new do |y| - buf = String.new.b + buf = String.new y.define_singleton_method(:write) do self << buf.replace(_1) buf.bytesize @@ -582,6 +582,27 @@ def encode_content(headers, body) # @api private # + # https://www.iana.org/assignments/character-sets/character-sets.xhtml + # + # @param content_type [String] + # @param text [String] + def force_charset!(content_type, text:) + charset = /charset=([^;\s]+)/.match(content_type)&.captures&.first + + return unless charset + + begin + encoding = Encoding.find(charset) + text.force_encoding(encoding) + rescue ArgumentError + nil + end + end + + # @api private + # + # Assumes each chunk in stream has `Encoding::BINARY`. + # # @param headers [Hash{String=>String}, Net::HTTPHeader] # @param stream [Enumerable] # @param suppress_error [Boolean] @@ -589,7 +610,7 @@ def encode_content(headers, body) # @raise [JSON::ParserError] # @return [Object] def decode_content(headers, stream:, suppress_error: false) - case headers["content-type"] + case (content_type = headers["content-type"]) in %r{^application/(?:vnd\.api\+)?json} json = stream.to_a.join begin @@ -606,11 +627,10 @@ def decode_content(headers, stream:, suppress_error: false) in %r{^text/event-stream} lines = decode_lines(stream) decode_sse(lines) - in %r{^text/} - stream.to_a.join else - # TODO: parsing other response types - StringIO.new(stream.to_a.join) + text = stream.to_a.join + force_charset!(content_type, text: text) + StringIO.new(text) end end end @@ -675,12 +695,17 @@ def chain_fused(enum, &blk) class << self # @api private # + # Assumes Strings have been forced into having `Encoding::BINARY`. + # + # This decoder is responsible for reassembling lines split across multiple + # fragments. + # # @param enum [Enumerable] # # @return [Enumerable] def decode_lines(enum) re = /(\r\n|\r|\n)/ - buffer = String.new.b + buffer = String.new cr_seen = nil chain_fused(enum) do |y| @@ -711,6 +736,8 @@ def decode_lines(enum) # # https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream # + # Assumes that `lines` has been decoded with `#decode_lines`. + # # @param lines [Enumerable] # # @return [EnumerableObject}>] @@ -734,7 +761,7 @@ def decode_sse(lines) in "event" current.merge!(event: value) in "data" - (current[:data] ||= String.new.b) << (value << "\n") + (current[:data] ||= String.new) << (value << "\n") in "id" unless value.include?("\0") current.merge!(id: value) in "retry" if /^\d+$/ =~ value diff --git a/lib/orb/version.rb b/lib/orb/version.rb index 902cbc99..4fa2a003 100644 --- a/lib/orb/version.rb +++ b/lib/orb/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Orb - VERSION = "0.1.3" + VERSION = "0.2.0" end diff --git a/orb.gemspec b/orb.gemspec index 83e9b7e7..6e3ca867 100644 --- a/orb.gemspec +++ b/orb.gemspec @@ -8,12 +8,21 @@ Gem::Specification.new do |s| s.summary = "Ruby library to access the Orb API" s.authors = ["Orb"] s.email = "team@withorb.com" - s.files = Dir["lib/**/*.rb", "rbi/**/*.rbi", "sig/**/*.rbs", "manifest.yaml", "CHANGELOG.md", "SECURITY.md"] - s.extra_rdoc_files = ["README.md"] - s.required_ruby_version = ">= 3.0.0" - s.add_dependency "connection_pool" s.homepage = "https://gemdocs.org/gems/orb-billing" s.metadata["homepage_uri"] = s.homepage s.metadata["source_code_uri"] = "https://github.com/orbcorp/orb-ruby" s.metadata["rubygems_mfa_required"] = false.to_s + s.required_ruby_version = ">= 3.0.0" + + s.files = Dir[ + "lib/**/*.rb", + "rbi/**/*.rbi", + "sig/**/*.rbs", + "manifest.yaml", + "SECURITY.md", + "CHANGELOG.md", + ".ignore" + ] + s.extra_rdoc_files = ["README.md"] + s.add_dependency "connection_pool" end diff --git a/rbi/lib/orb/client.rbi b/rbi/lib/orb/client.rbi index 9a65af61..f3b58afe 100644 --- a/rbi/lib/orb/client.rbi +++ b/rbi/lib/orb/client.rbi @@ -78,8 +78,9 @@ module Orb def self.new( # Defaults to `ENV["ORB_API_KEY"]` api_key: ENV["ORB_API_KEY"], - # Override the default base URL for the API, e.g., `"https://api.example.com/v2/"` - base_url: nil, + # Override the default base URL for the API, e.g., + # `"https://api.example.com/v2/"`. Defaults to `ENV["ORB_BASE_URL"]` + base_url: ENV["ORB_BASE_URL"], # Max number of retries to attempt after a failed retryable request. max_retries: DEFAULT_MAX_RETRIES, timeout: DEFAULT_TIMEOUT_IN_SECONDS, diff --git a/rbi/lib/orb/internal.rbi b/rbi/lib/orb/internal.rbi index 3041df91..02b59bf4 100644 --- a/rbi/lib/orb/internal.rbi +++ b/rbi/lib/orb/internal.rbi @@ -7,6 +7,6 @@ module Orb # this alias might be refined in the future. AnyHash = T.type_alias { T::Hash[Symbol, T.anything] } - OMIT = T.let(T.anything, T.anything) + OMIT = T.let(Object.new.freeze, T.anything) end end diff --git a/rbi/lib/orb/internal/page.rbi b/rbi/lib/orb/internal/page.rbi index 8309312f..34463855 100644 --- a/rbi/lib/orb/internal/page.rbi +++ b/rbi/lib/orb/internal/page.rbi @@ -13,6 +13,7 @@ module Orb sig { returns(PaginationMetadata) } attr_accessor :pagination_metadata + # @api private sig { returns(String) } def inspect; end diff --git a/rbi/lib/orb/internal/transport/base_client.rbi b/rbi/lib/orb/internal/transport/base_client.rbi index cbcd1c95..3b8a885b 100644 --- a/rbi/lib/orb/internal/transport/base_client.rbi +++ b/rbi/lib/orb/internal/transport/base_client.rbi @@ -187,6 +187,7 @@ module Orb model: Orb::Internal::Type::Unknown, options: {} ); end + # @api private sig { returns(String) } def inspect; end end diff --git a/rbi/lib/orb/internal/type/array_of.rbi b/rbi/lib/orb/internal/type/array_of.rbi index d9ef3137..1f8fd4a7 100644 --- a/rbi/lib/orb/internal/type/array_of.rbi +++ b/rbi/lib/orb/internal/type/array_of.rbi @@ -10,11 +10,10 @@ module Orb include Orb::Internal::Type::Converter abstract! - final! Elem = type_member(:out) - sig(:final) do + sig do params( type_info: T.any( Orb::Internal::AnyHash, @@ -27,14 +26,14 @@ module Orb end def self.[](type_info, spec = {}); end - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def ===(other); end - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def ==(other); end # @api private - sig(:final) do + sig do override .params(value: T.any( T::Array[T.anything], @@ -46,7 +45,7 @@ module Orb def coerce(value, state:); end # @api private - sig(:final) do + sig do override .params(value: T.any( T::Array[T.anything], @@ -58,15 +57,15 @@ module Orb def dump(value, state:); end # @api private - sig(:final) { returns(Elem) } + sig { returns(Elem) } protected def item_type; end # @api private - sig(:final) { returns(T::Boolean) } + sig { returns(T::Boolean) } protected def nilable?; end # @api private - sig(:final) do + sig do params( type_info: T.any( Orb::Internal::AnyHash, @@ -78,6 +77,10 @@ module Orb .void end def initialize(type_info, spec = {}); end + + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0); end end end end diff --git a/rbi/lib/orb/internal/type/base_model.rbi b/rbi/lib/orb/internal/type/base_model.rbi index f525170c..fadb4e30 100644 --- a/rbi/lib/orb/internal/type/base_model.rbi +++ b/rbi/lib/orb/internal/type/base_model.rbi @@ -175,6 +175,11 @@ module Orb sig { params(keys: T.nilable(T::Array[Symbol])).returns(Orb::Internal::AnyHash) } def deconstruct_keys(keys); end + class << self + sig { params(model: Orb::Internal::Type::BaseModel).returns(Orb::Internal::AnyHash) } + def walk(model); end + end + sig { params(a: T.anything).returns(String) } def to_json(*a); end @@ -185,6 +190,17 @@ module Orb sig { params(data: T.any(T::Hash[Symbol, T.anything], T.self_type)).returns(T.attached_class) } def self.new(data = {}); end + class << self + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0); end + end + + # @api private + sig { returns(String) } + def to_s; end + + # @api private sig { returns(String) } def inspect; end end diff --git a/rbi/lib/orb/internal/type/boolean.rbi b/rbi/lib/orb/internal/type/boolean.rbi index 51da0ac4..cc88e526 100644 --- a/rbi/lib/orb/internal/type/boolean.rbi +++ b/rbi/lib/orb/internal/type/boolean.rbi @@ -10,17 +10,16 @@ module Orb extend Orb::Internal::Type::Converter abstract! - final! - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def self.===(other); end - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def self.==(other); end class << self # @api private - sig(:final) do + sig do override .params(value: T.any( T::Boolean, @@ -32,7 +31,7 @@ module Orb def coerce(value, state:); end # @api private - sig(:final) do + sig do override .params(value: T.any(T::Boolean, T.anything), state: Orb::Internal::Type::Converter::DumpState) .returns(T.any(T::Boolean, T.anything)) diff --git a/rbi/lib/orb/internal/type/converter.rbi b/rbi/lib/orb/internal/type/converter.rbi index e134df41..1e434053 100644 --- a/rbi/lib/orb/internal/type/converter.rbi +++ b/rbi/lib/orb/internal/type/converter.rbi @@ -35,6 +35,10 @@ module Orb end def dump(value, state:); end + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0); end + class << self # @api private sig do @@ -106,6 +110,10 @@ module Orb .returns(T.anything) end def self.dump(target, value, state: {can_retry: true}); end + + # @api private + sig { params(target: T.anything, depth: Integer).returns(String) } + def self.inspect(target, depth:); end end end end diff --git a/rbi/lib/orb/internal/type/enum.rbi b/rbi/lib/orb/internal/type/enum.rbi index b8e5c81d..6eb41366 100644 --- a/rbi/lib/orb/internal/type/enum.rbi +++ b/rbi/lib/orb/internal/type/enum.rbi @@ -57,6 +57,10 @@ module Orb .returns(T.any(Symbol, T.anything)) end def dump(value, state:); end + + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0); end end end end diff --git a/rbi/lib/orb/internal/type/hash_of.rbi b/rbi/lib/orb/internal/type/hash_of.rbi index 6c482004..8072f046 100644 --- a/rbi/lib/orb/internal/type/hash_of.rbi +++ b/rbi/lib/orb/internal/type/hash_of.rbi @@ -10,11 +10,10 @@ module Orb include Orb::Internal::Type::Converter abstract! - final! Elem = type_member(:out) - sig(:final) do + sig do params( type_info: T.any( Orb::Internal::AnyHash, @@ -27,14 +26,14 @@ module Orb end def self.[](type_info, spec = {}); end - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def ===(other); end - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def ==(other); end # @api private - sig(:final) do + sig do override .params( value: T.any(T::Hash[T.anything, T.anything], T.anything), @@ -45,7 +44,7 @@ module Orb def coerce(value, state:); end # @api private - sig(:final) do + sig do override .params( value: T.any(T::Hash[T.anything, T.anything], T.anything), @@ -56,15 +55,15 @@ module Orb def dump(value, state:); end # @api private - sig(:final) { returns(Elem) } + sig { returns(Elem) } protected def item_type; end # @api private - sig(:final) { returns(T::Boolean) } + sig { returns(T::Boolean) } protected def nilable?; end # @api private - sig(:final) do + sig do params( type_info: T.any( Orb::Internal::AnyHash, @@ -76,6 +75,10 @@ module Orb .void end def initialize(type_info, spec = {}); end + + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0); end end end end diff --git a/rbi/lib/orb/internal/type/io_like.rbi b/rbi/lib/orb/internal/type/io_like.rbi index 06ff47b0..47798fbe 100644 --- a/rbi/lib/orb/internal/type/io_like.rbi +++ b/rbi/lib/orb/internal/type/io_like.rbi @@ -10,17 +10,16 @@ module Orb extend Orb::Internal::Type::Converter abstract! - final! - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def self.===(other); end - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def self.==(other); end class << self # @api private - sig(:final) do + sig do override .params(value: T.any( StringIO, @@ -33,7 +32,7 @@ module Orb def coerce(value, state:); end # @api private - sig(:final) do + sig do override .params( value: T.any(Pathname, StringIO, IO, String, T.anything), diff --git a/rbi/lib/orb/internal/type/union.rbi b/rbi/lib/orb/internal/type/union.rbi index 18ce6879..2675dc8b 100644 --- a/rbi/lib/orb/internal/type/union.rbi +++ b/rbi/lib/orb/internal/type/union.rbi @@ -62,6 +62,10 @@ module Orb ).returns(T.anything) end def dump(value, state:); end + + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0); end end end end diff --git a/rbi/lib/orb/internal/type/unknown.rbi b/rbi/lib/orb/internal/type/unknown.rbi index 2bd89240..93bd4287 100644 --- a/rbi/lib/orb/internal/type/unknown.rbi +++ b/rbi/lib/orb/internal/type/unknown.rbi @@ -10,17 +10,16 @@ module Orb extend Orb::Internal::Type::Converter abstract! - final! - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def self.===(other); end - sig(:final) { params(other: T.anything).returns(T::Boolean) } + sig { params(other: T.anything).returns(T::Boolean) } def self.==(other); end class << self # @api private - sig(:final) do + sig do override.params( value: T.anything, state: Orb::Internal::Type::Converter::CoerceState @@ -29,7 +28,7 @@ module Orb def coerce(value, state:); end # @api private - sig(:final) do + sig do override.params( value: T.anything, state: Orb::Internal::Type::Converter::DumpState diff --git a/rbi/lib/orb/internal/util.rbi b/rbi/lib/orb/internal/util.rbi index 02bad0ea..a8284783 100644 --- a/rbi/lib/orb/internal/util.rbi +++ b/rbi/lib/orb/internal/util.rbi @@ -215,6 +215,14 @@ module Orb def encode_content(headers, body); end # @api private + # + # https://www.iana.org/assignments/character-sets/character-sets.xhtml + sig { params(content_type: String, text: String).void } + def force_charset!(content_type, text:); end + + # @api private + # + # Assumes each chunk in stream has `Encoding::BINARY`. sig do params( headers: T.any(T::Hash[String, String], Net::HTTPHeader), @@ -263,12 +271,19 @@ module Orb class << self # @api private + # + # Assumes Strings have been forced into having `Encoding::BINARY`. + # + # This decoder is responsible for reassembling lines split across multiple + # fragments. sig { params(enum: T::Enumerable[String]).returns(T::Enumerable[String]) } def decode_lines(enum); end # @api private # # https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream + # + # Assumes that `lines` has been decoded with `#decode_lines`. sig do params(lines: T::Enumerable[String]).returns(T::Enumerable[Orb::Internal::Util::ServerSentEvent]) end diff --git a/scripts/bootstrap b/scripts/bootstrap index 88566757..cc31aa85 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -2,7 +2,7 @@ set -e -cd "$(dirname "$0")/.." +cd -- "$(dirname -- "$0")/.." if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ]; then brew bundle check >/dev/null 2>&1 || { @@ -13,4 +13,4 @@ fi echo "==> Installing Ruby dependencies…" -bundle install +exec -- bundle install "$@" diff --git a/scripts/format b/scripts/format index 67b400de..177d1e63 100755 --- a/scripts/format +++ b/scripts/format @@ -5,4 +5,5 @@ set -e cd -- "$(dirname -- "$0")/.." echo "==> Running formatters" + exec -- bundle exec rake format "$@" diff --git a/scripts/lint b/scripts/lint index 39581dc1..08b0dbeb 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,4 +4,6 @@ set -e cd -- "$(dirname -- "$0")/.." +echo "==> Running linters" + exec -- bundle exec rake lint "$@" diff --git a/scripts/test b/scripts/test index 2e1fe093..8e5d35cd 100755 --- a/scripts/test +++ b/scripts/test @@ -2,7 +2,7 @@ set -e -cd "$(dirname "$0")/.." +cd -- "$(dirname -- "$0")/.." RED='\033[0;31m' GREEN='\033[0;32m' diff --git a/sig/orb/internal.rbs b/sig/orb/internal.rbs index e6e6fd6f..4ebb487b 100644 --- a/sig/orb/internal.rbs +++ b/sig/orb/internal.rbs @@ -1,5 +1,5 @@ module Orb module Internal - OMIT: top + OMIT: Object end end diff --git a/sig/orb/internal/type/array_of.rbs b/sig/orb/internal/type/array_of.rbs index f37088f6..20dac6aa 100644 --- a/sig/orb/internal/type/array_of.rbs +++ b/sig/orb/internal/type/array_of.rbs @@ -35,6 +35,8 @@ module Orb | Orb::Internal::Type::Converter::input type_info, ?::Hash[Symbol, top] spec ) -> void + + def inspect: (?depth: Integer) -> String end end end diff --git a/sig/orb/internal/type/base_model.rbs b/sig/orb/internal/type/base_model.rbs index bae69564..709d39bb 100644 --- a/sig/orb/internal/type/base_model.rbs +++ b/sig/orb/internal/type/base_model.rbs @@ -69,12 +69,20 @@ module Orb def deconstruct_keys: (::Array[Symbol]? keys) -> ::Hash[Symbol, top] + def self.walk: ( + Orb::Internal::Type::BaseModel model + ) -> ::Hash[Symbol, top] + def to_json: (*top a) -> String def to_yaml: (*top a) -> String def initialize: (?::Hash[Symbol, top] | self data) -> void + def self.inspect: (?depth: Integer) -> String + + def to_s: -> String + def inspect: -> String end end diff --git a/sig/orb/internal/type/converter.rbs b/sig/orb/internal/type/converter.rbs index e3fb8663..018efc59 100644 --- a/sig/orb/internal/type/converter.rbs +++ b/sig/orb/internal/type/converter.rbs @@ -23,6 +23,8 @@ module Orb state: Orb::Internal::Type::Converter::dump_state ) -> top + def inspect: (?depth: Integer) -> String + def self.type_info: ( { const: (nil | bool | Integer | Float | Symbol)?, @@ -44,6 +46,8 @@ module Orb top value, ?state: Orb::Internal::Type::Converter::dump_state ) -> top + + def self.inspect: (top target, depth: Integer) -> String end end end diff --git a/sig/orb/internal/type/enum.rbs b/sig/orb/internal/type/enum.rbs index 61541787..f87e8c1d 100644 --- a/sig/orb/internal/type/enum.rbs +++ b/sig/orb/internal/type/enum.rbs @@ -21,6 +21,8 @@ module Orb Symbol | top value, state: Orb::Internal::Type::Converter::dump_state ) -> (Symbol | top) + + def inspect: (?depth: Integer) -> String end end end diff --git a/sig/orb/internal/type/hash_of.rbs b/sig/orb/internal/type/hash_of.rbs index 54691d6e..014ffc68 100644 --- a/sig/orb/internal/type/hash_of.rbs +++ b/sig/orb/internal/type/hash_of.rbs @@ -35,6 +35,8 @@ module Orb | Orb::Internal::Type::Converter::input type_info, ?::Hash[Symbol, top] spec ) -> void + + def inspect: (?depth: Integer) -> String end end end diff --git a/sig/orb/internal/type/union.rbs b/sig/orb/internal/type/union.rbs index e7c8de0c..e907f7a9 100644 --- a/sig/orb/internal/type/union.rbs +++ b/sig/orb/internal/type/union.rbs @@ -39,6 +39,8 @@ module Orb top value, state: Orb::Internal::Type::Converter::dump_state ) -> top + + def inspect: (?depth: Integer) -> String end end end diff --git a/sig/orb/internal/util.rbs b/sig/orb/internal/util.rbs index e0d7f2c4..1488744e 100644 --- a/sig/orb/internal/util.rbs +++ b/sig/orb/internal/util.rbs @@ -120,6 +120,8 @@ module Orb top body ) -> top + def self?.force_charset!: (String content_type, text: String) -> void + def self?.decode_content: ( ::Hash[String, String] headers, stream: Enumerable[String], diff --git a/sorbet/rbi/.gitignore b/sorbet/rbi/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/sorbet/rbi/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/orb/internal/type/base_model_test.rb b/test/orb/internal/type/base_model_test.rb index 138d5e6b..d33e2616 100644 --- a/test/orb/internal/type/base_model_test.rb +++ b/test/orb/internal/type/base_model_test.rb @@ -154,6 +154,12 @@ def test_dump_retry end class Orb::Test::EnumModelTest < Minitest::Test + class E0 + include Orb::Internal::Type::Enum + + def initialize(*values) = (@values = values) + end + module E1 extend Orb::Internal::Type::Enum @@ -183,6 +189,10 @@ module E4 def test_coerce cases = { + [E0.new, "one"] => [{no: 1}, "one"], + [E0.new(:one), "one"] => [{yes: 1}, :one], + [E0.new(:two), "one"] => [{maybe: 1}, "one"], + # rubocop:disable Lint/BooleanSymbol [E1, true] => [{yes: 1}, true], [E1, false] => [{no: 1}, false], @@ -432,8 +442,10 @@ def test_accessors end class Orb::Test::UnionTest < Minitest::Test - module U0 - extend Orb::Internal::Type::Union + class U0 + include Orb::Internal::Type::Union + + def initialize(*variants) = variants.each { variant(_1) } end module U1 @@ -519,6 +531,11 @@ def test_coerce cases = { [U0, :""] => [{no: 1}, 0, :""], + [U0.new(Integer, Float), "one"] => [{no: 1}, 2, "one"], + [U0.new(Integer, Float), 1.0] => [{yes: 1}, 2, 1.0], + [U0.new({const: :a}), "a"] => [{yes: 1}, 1, :a], + [U0.new({const: :a}), "2"] => [{maybe: 1}, 1, "2"], + [U1, "a"] => [{yes: 1}, 1, :a], [U1, "2"] => [{maybe: 1}, 2, "2"], [U1, :b] => [{maybe: 1}, 2, :b], @@ -556,6 +573,12 @@ def test_coerce end class Orb::Test::BaseModelQoLTest < Minitest::Test + class E0 + include Orb::Internal::Type::Enum + + def initialize(*values) = (@values = values) + end + module E1 extend Orb::Internal::Type::Enum @@ -575,6 +598,26 @@ module E3 B = 3 end + class U0 + include Orb::Internal::Type::Union + + def initialize(*variants) = variants.each { variant(_1) } + end + + module U1 + extend Orb::Internal::Type::Union + + variant String + variant Integer + end + + module U2 + extend Orb::Internal::Type::Union + + variant String + variant Integer + end + class M1 < Orb::Internal::Type::BaseModel required :a, Integer end @@ -592,8 +635,15 @@ def test_equality [Orb::Internal::Type::Unknown, Orb::Internal::Type::Unknown] => true, [Orb::Internal::Type::Boolean, Orb::Internal::Type::Boolean] => true, [Orb::Internal::Type::Unknown, Orb::Internal::Type::Boolean] => false, + [E0.new(:a, :b), E0.new(:a, :b)] => true, + [E0.new(:a, :b), E0.new(:b, :a)] => true, + [E0.new(:a, :b), E0.new(:b, :c)] => false, [E1, E2] => true, [E1, E3] => false, + [U0.new(String, Integer), U0.new(String, Integer)] => true, + [U0.new(String, Integer), U0.new(Integer, String)] => false, + [U0.new(String, Float), U0.new(String, Integer)] => false, + [U1, U2] => true, [M1, M2] => false, [M1, M3] => true } diff --git a/test/orb/internal/util_test.rb b/test/orb/internal/util_test.rb index c5144373..5c8a2a37 100644 --- a/test/orb/internal/util_test.rb +++ b/test/orb/internal/util_test.rb @@ -368,6 +368,24 @@ def test_close_fused_sse_chain end end +class Orb::Test::UtilContentDecodingTest < Minitest::Test + def test_charset + cases = { + "application/json" => Encoding::BINARY, + "application/json; charset=utf-8" => Encoding::UTF_8, + "charset=uTf-8 application/json; " => Encoding::UTF_8, + "charset=UTF-8; application/json; " => Encoding::UTF_8, + "charset=ISO-8859-1 ;application/json; " => Encoding::ISO_8859_1, + "charset=EUC-KR ;application/json; " => Encoding::EUC_KR + } + text = String.new.force_encoding(Encoding::BINARY) + cases.each do |content_type, encoding| + Orb::Internal::Util.force_charset!(content_type, text: text) + assert_equal(encoding, text.encoding) + end + end +end + class Orb::Test::UtilSseTest < Minitest::Test def test_decode_lines cases = { @@ -381,7 +399,9 @@ def test_decode_lines %W[\na b\n\n] => %W[\n ab\n \n], %W[\na b] => %W[\n ab], %W[\u1F62E\u200D\u1F4A8] => %W[\u1F62E\u200D\u1F4A8], - %W[\u1F62E \u200D \u1F4A8] => %W[\u1F62E\u200D\u1F4A8] + %W[\u1F62E \u200D \u1F4A8] => %W[\u1F62E\u200D\u1F4A8], + ["\xf0\x9f".b, "\xa5\xba".b] => ["\xf0\x9f\xa5\xba".b], + ["\xf0".b, "\x9f".b, "\xa5".b, "\xba".b] => ["\xf0\x9f\xa5\xba".b] } eols = %W[\n \r \r\n] cases.each do |enum, expected|