diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2ade785 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI +on: + push: + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +jobs: + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/amocrm-ruby' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + + steps: + - uses: actions/checkout@v6 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: false + - run: |- + bundle install + + - name: Run lints + run: ./scripts/lint + test: + timeout-minutes: 10 + name: test + runs-on: ${{ github.repository == 'stainless-sdks/amocrm-ruby' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v6 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: false + - run: |- + bundle install + + - name: Run tests + run: ./scripts/test diff --git a/.github/workflows/publish-gem.yml b/.github/workflows/publish-gem.yml new file mode 100644 index 0000000..e51f89c --- /dev/null +++ b/.github/workflows/publish-gem.yml @@ -0,0 +1,31 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to rubygems.org in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/Hexlet/amocrm-ruby/actions/workflows/publish-gem.yml +name: Publish Gem +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: false + - run: |- + bundle install + + - name: Publish to RubyGems.org + run: | + bash ./bin/publish-gem + env: + # `RUBYGEMS_HOST` is only required for private gem repositories, not https://rubygems.org + RUBYGEMS_HOST: ${{ secrets.AMOCRM_RUBYGEMS_HOST || secrets.RUBYGEMS_HOST }} + GEM_HOST_API_KEY: ${{ secrets.AMOCRM_GEM_HOST_API_KEY || secrets.GEM_HOST_API_KEY }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..85bfcab --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,22 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'Hexlet/amocrm-ruby' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v6 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + RUBYGEMS_HOST: ${{ secrets.AMOCRM_RUBYGEMS_HOST || secrets.RUBYGEMS_HOST }} + GEM_HOST_API_KEY: ${{ secrets.AMOCRM_GEM_HOST_API_KEY || secrets.GEM_HOST_API_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d26cee --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.gem +.idea/ +.ignore +.prism.log +.ruby-lsp/ +.yardoc/ +bin/tapioca +Brewfile.lock.json +doc/ +sorbet/tapioca/* diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..1332969 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1" +} \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..26517b8 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,296 @@ +# yaml-language-server: $schema=https://www.rubyschema.org/rubocop.json +--- +# Explicitly disable pending cops for now. This is the default behaviour but +# this avoids a large warning every time we run it. +# Stop RuboCop nagging about rubocop-rake. +# Ensure that RuboCop validates according to the lowest version of Ruby that we support. +AllCops: + Exclude: + - "bin/*" + NewCops: enable + SuggestExtensions: false + TargetRubyVersion: 3.2 + +# Whether MFA is required or not should be left to the token configuration. +Gemspec/RequireMFA: + Enabled: false + +# Don't require this extra line break, it can be excessive. +Layout/EmptyLineAfterGuardClause: + Enabled: false + +# Don't leave complex assignment values hanging off to the right. +Layout/EndAlignment: + EnforcedStyleAlignWith: variable + +Layout/FirstArrayElementLineBreak: + Enabled: true + +Layout/FirstHashElementLineBreak: + Enabled: true + +Layout/FirstMethodArgumentLineBreak: + Enabled: true + +Layout/FirstMethodParameterLineBreak: + Enabled: true + +# Set a reasonable line length; rely on other cops to correct long lines. +Layout/LineLength: + AllowedPatterns: + - "^\\s*#.*$" + - ^require(_relative)? + - "Amocrm::Internal::Type::BaseModel$" + - "^\\s*[A-Z0-9_]+ = :" + - "Amocrm::(Models|Resources|Test)::" + Max: 110 + +Layout/MultilineArrayLineBreaks: + Enabled: true + +# Start the assignment on the same line variable is mentioned. +Layout/MultilineAssignmentLayout: + EnforcedStyle: same_line + +Layout/MultilineHashKeyLineBreaks: + Enabled: true + +Layout/MultilineMethodArgumentLineBreaks: + Enabled: true + +Layout/MultilineMethodParameterLineBreaks: + Enabled: true + +# Prefer compact hash literals. +Layout/SpaceInsideHashLiteralBraces: + EnforcedStyle: no_space + Exclude: + - "**/*.rbi" + +Lint/BooleanSymbol: + Enabled: false + +# This option occasionally mangles identifier names +Lint/DeprecatedConstants: + Exclude: + - "**/*.rbi" + +# We use pattern assertion in tests to ensure correctness. +Lint/DuplicateMatchPattern: + Exclude: + - "test/**/*" + +# Fairly useful in tests for pattern assertions. +Lint/EmptyInPattern: + Exclude: + - "test/**/*" + +Lint/MissingCopEnableDirective: + Exclude: + - "examples/**/*.rb" + +Lint/MissingSuper: + Exclude: + - "**/*.rbi" + +Lint/SymbolConversion: + Exclude: + - "**/*.rbi" + +# Disabled for safety reasons, this option changes code semantics. +Lint/UnusedMethodArgument: + AutoCorrect: false + +# This option is prone to causing accidental bugs. +Lint/UselessAssignment: + AutoCorrect: false + Exclude: + - "examples/**/*.rb" + +Metrics/AbcSize: + Enabled: false + +Metrics/BlockLength: + AllowedPatterns: + - assert_pattern + - type_alias + - define_sorbet_constant! + Exclude: + - "**/*.rbi" + +Metrics/ClassLength: + Enabled: false + +Metrics/CollectionLiteralLength: + Exclude: + - "test/**/*" + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Metrics/ParameterLists: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Naming/AccessorMethodName: + Enabled: false + +# Need to preserve block identifier for documentation. +Naming/BlockForwarding: + Enabled: false + +# Underscores are generally useful for disambiguation. +Naming/ClassAndModuleCamelCase: + Enabled: false + +Naming/MethodParameterName: + Enabled: false + +Naming/PredicatePrefix: + Exclude: + - "**/*.rbi" + +Naming/VariableNumber: + Enabled: false + +# Nothing wrong with inline private methods. +Style/AccessModifierDeclarations: + Enabled: false + +Style/AccessorGrouping: + Exclude: + - "**/*.rbi" + +# Behaviour of alias_method is more predictable. +Style/Alias: + EnforcedStyle: prefer_alias_method + +# And/or have confusing precedence, avoid them. +Style/AndOr: + EnforcedStyle: always + +Style/ArgumentsForwarding: + Enabled: false + +Style/BisectedAttrAccessor: + Exclude: + - "**/*.rbi" + +# We prefer nested modules in lib/, but are currently using compact style for tests. +Style/ClassAndModuleChildren: + Exclude: + - "test/**/*" + +Style/CommentAnnotation: + Enabled: false + +# We should go back and add these docs, but ignore for now. +Style/Documentation: + Enabled: false + +# Allow explicit empty elses, for clarity. +Style/EmptyElse: + Enabled: false + +Style/EmptyMethod: + Exclude: + - "**/*.rbi" + +# We commonly use ENV['KEY'], it's OK. +Style/FetchEnvVar: + Enabled: false + +# Just to be safe, ensure nobody is mutating our internal strings. +Style/FrozenStringLiteralComment: + EnforcedStyle: always + Exclude: + - "**/*.rbi" + +# Nothing wrong with clear if statements. +Style/IfUnlessModifier: + Enabled: false + +# Rubocop is pretty bad about mangling single line lambdas. +Style/Lambda: + Enabled: false + +# Prefer consistency in method calling syntax. +Style/MethodCallWithArgsParentheses: + AllowedMethods: + - raise + Enabled: true + Exclude: + - "**/*.gemspec" + +Style/MultilineBlockChain: + Enabled: false + +# Perfectly fine. +Style/MultipleComparison: + Enabled: false + +Style/MutableConstant: + Exclude: + - "**/*.rbi" + +# Not all parameters should be named. +Style/NumberedParameters: + Enabled: false + +Style/NumberedParametersLimit: + Max: 2 + +# Reasonable to use brackets for errors with long messages. +Style/RaiseArgs: + Enabled: false + +# Be explicit about `RuntimeError`s. +Style/RedundantException: + Enabled: false + +Style/RedundantInitialize: + Exclude: + - "**/*.rbi" + +Style/RedundantParentheses: + Exclude: + - "**/*.rbi" + +# Prefer slashes for regex literals. +Style/RegexpLiteral: + EnforcedStyle: slashes + +# Allow explicit ifs, especially for imperative use. +Style/SafeNavigation: + Enabled: false + +Style/SignalException: + Exclude: + - Rakefile + - "**/*.rake" + +# We use these sparingly, where we anticipate future branches for the +# inner conditional. +Style/SoleNestedConditional: + Enabled: false + +# Prefer double quotes so that interpolation can be easily added. +Style/StringLiterals: + EnforcedStyle: double_quotes + +# Prefer explicit symbols for clarity; you can search for `:the_symbol`. +Style/SymbolArray: + EnforcedStyle: brackets + +# This option makes examples harder to read for ruby novices. +Style/SymbolProc: + Exclude: + - "examples/**/*.rb" diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..944880f --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2.0 diff --git a/.solargraph.yml b/.solargraph.yml new file mode 100644 index 0000000..b019824 --- /dev/null +++ b/.solargraph.yml @@ -0,0 +1,11 @@ +--- +max_files: 0 +include: + - '*.gemspec' + - 'Rakefile' + - 'examples/**/*.rb' + - 'lib/**/*.rb' + - 'test/amocrm/resource_namespaces.rb' + - 'test/amocrm/test_helper.rb' +exclude: + - 'rbi/**/*' diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 0000000..77521d0 --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hexlet%2Famocrm-259151150f55da97dac4aa0df44f6299d2b0c4f610c346934b752f53fd23f598.yml +openapi_spec_hash: 6e1baef5165a728622de8d2dd0903f36 +config_hash: e01761a134b02efe7127582e0ad9cc0d diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..84c12f2 --- /dev/null +++ b/.yardopts @@ -0,0 +1,6 @@ +--type-name-tag generic:Generic +--default-return void +--markup markdown +--markup-provider redcarpet +--exclude /rbi +--exclude /sig diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..557825d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ +## 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 of `lib/amocrm/helpers/` and `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/amocrm" + +# ... +``` + +```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 "amocrm", git: "https://www.github.com/Hexlet/amocrm-ruby" +``` + +Alternatively, reference local copy of the repo: + +```bash +$ git clone -- 'https://www.github.com/Hexlet/amocrm-ruby' '' +``` + +```ruby +gem "amocrm", 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` files; And [syntax_tree](https://github.com/ruby-syntax-tree/syntax_tree) is used for formatting of both `*.rbi` and `*.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 + +### Ruby LSP + +[Ruby LSP](https://github.com/Shopify/ruby-lsp) has quite good support for go to definition, but not auto-completion. + +This can be installed along side Solargraph. + +### Solargraph + +[Solargraph](https://solargraph.org) has quite good support for auto-completion, but not go to definition. + +This can be installed along side Ruby LSP. + +### 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. + +## Documentation Preview + +To preview the documentation, run: + +```bash +$ bundle exec rake docs:preview [PORT=8808] +``` diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..1be178c --- /dev/null +++ b/Gemfile @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +group :development do + gem "rake" + gem "rbs" + gem "rubocop" + gem "sorbet" + gem "steep" + gem "syntax_tree" + gem "syntax_tree-rbs", github: "ruby-syntax-tree/syntax_tree-rbs", branch: "main" + gem "tapioca" +end + +group :development, :test do + gem "async" + gem "minitest" + gem "minitest-focus" + gem "minitest-hooks" + gem "minitest-proveit" + gem "minitest-rg" + gem "webmock" +end + +group :development, :docs do + gem "redcarpet" + gem "webrick" + gem "yard" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..74c12b8 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,228 @@ +GIT + remote: https://github.com/ruby-syntax-tree/syntax_tree-rbs.git + revision: f94bc3060682ffbd126e4d5086ffedc89073d626 + branch: main + specs: + syntax_tree-rbs (1.0.0) + prettier_print + rbs + syntax_tree (>= 2.0.1) + +PATH + remote: . + specs: + amocrm (0.0.1) + cgi + connection_pool + +GEM + remote: https://rubygems.org/ + specs: + activesupport (8.1.1) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.3) + async (2.34.0) + console (~> 1.29) + fiber-annotation + io-event (~> 1.11) + metrics (~> 0.12) + traces (~> 0.18) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (3.3.1) + cgi (0.5.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + console (1.34.2) + fiber-annotation + fiber-local (~> 1.1) + json + crack (1.0.1) + bigdecimal + rexml + csv (3.3.5) + drb (2.2.3) + erubi (1.13.1) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) + fiber-annotation (0.2.0) + fiber-local (1.1.0) + fiber-storage + fiber-storage (1.0.1) + fileutils (1.8.0) + hashdiff (1.2.1) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + io-event (1.11.2) + json (2.15.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) + metrics (0.15.0) + minitest (5.26.0) + minitest-focus (1.4.0) + minitest (>= 4, < 6) + minitest-hooks (1.5.2) + minitest (> 5.3) + minitest-proveit (1.0.0) + minitest (> 5, < 7) + minitest-rg (5.3.0) + minitest (~> 5.0) + mutex_m (0.3.0) + netrc (0.11.0) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + prettier_print (1.2.1) + prism (1.6.0) + public_suffix (6.0.2) + racc (1.8.1) + rainbow (3.1.1) + rake (13.3.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rbi (0.3.7) + prism (~> 1.0) + rbs (>= 3.4.4) + rbs (3.9.5) + logger + redcarpet (3.6.1) + regexp_parser (2.11.3) + rexml (3.4.4) + rubocop (1.81.7) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + ruby-progressbar (1.13.0) + securerandom (0.4.1) + sorbet (0.6.12690) + sorbet-static (= 0.6.12690) + sorbet-runtime (0.6.12690) + sorbet-static (0.6.12690-aarch64-linux) + sorbet-static (0.6.12690-universal-darwin) + sorbet-static (0.6.12690-x86_64-linux) + sorbet-static-and-runtime (0.6.12690) + sorbet (= 0.6.12690) + sorbet-runtime (= 0.6.12690) + spoom (1.6.3) + erubi (>= 1.10.0) + prism (>= 0.28.0) + rbi (>= 0.3.3) + rexml (>= 3.2.6) + sorbet-static-and-runtime (>= 0.5.10187) + thor (>= 0.19.2) + steep (1.10.0) + activesupport (>= 5.1) + concurrent-ruby (>= 1.1.10) + csv (>= 3.0.9) + fileutils (>= 1.1.0) + json (>= 2.1.0) + language_server-protocol (>= 3.17.0.4, < 4.0) + listen (~> 3.0) + logger (>= 1.3.0) + mutex_m (>= 0.3.0) + parser (>= 3.1) + rainbow (>= 2.2.2, < 4.0) + rbs (~> 3.9) + securerandom (>= 0.1) + strscan (>= 1.0.0) + terminal-table (>= 2, < 5) + uri (>= 0.12.0) + strscan (3.1.5) + syntax_tree (6.3.0) + prettier_print (>= 1.2.0) + tapioca (0.16.11) + benchmark + bundler (>= 2.2.25) + netrc (>= 0.11.0) + parallel (>= 1.21.0) + rbi (~> 0.2) + sorbet-static-and-runtime (>= 0.5.11087) + spoom (>= 1.2.0) + thor (>= 1.2.0) + yard-sorbet + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + thor (1.4.0) + traces (0.18.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + uri (1.1.0) + webmock (3.26.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.9.1) + yard (0.9.37) + yard-sorbet (0.9.0) + sorbet-runtime + yard + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm64-darwin + universal-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + amocrm! + async + minitest + minitest-focus + minitest-hooks + minitest-proveit + minitest-rg + rake + rbs + redcarpet + rubocop + sorbet + steep + syntax_tree + syntax_tree-rbs! + tapioca + webmock + webrick + yard + +BUNDLED WITH + 2.4.1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..736fba9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Amocrm + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 29fd395..d3b9c9d 100644 --- a/README.md +++ b/README.md @@ -1 +1,233 @@ -# amocrm-ruby \ No newline at end of file +# Amocrm Ruby API library + +The Amocrm Ruby library provides convenient access to the Amocrm REST API from any Ruby 3.2.0+ application. It ships with comprehensive types & docstrings in Yard, RBS, and RBI – [see below](https://github.com/Hexlet/amocrm-ruby#Sorbet) for usage with Sorbet. The standard library's `net/http` is used as the HTTP transport, with connection pooling via the `connection_pool` gem. + +It is generated with [Stainless](https://www.stainless.com/). + +## Documentation + +Documentation for releases of this gem can be found [on RubyDoc](https://gemdocs.org/gems/amocrm). + +## Installation + +To use this gem, install via Bundler by adding the following to your application's `Gemfile`: + + + +```ruby +gem "amocrm", "~> 0.0.1" +``` + + + +## Usage + +```ruby +require "bundler/setup" +require "amocrm" + +amocrm = Amocrm::Client.new(api_key: "My API Key") + +response = amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] +) + +puts(response) +``` + +### Handling errors + +When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `Amocrm::Errors::APIError` will be thrown: + +```ruby +begin + unsorted = amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] + ) +rescue Amocrm::Errors::APIConnectionError => e + puts("The server could not be reached") + puts(e.cause) # an underlying Exception, likely raised within `net/http` +rescue Amocrm::Errors::RateLimitError => e + puts("A 429 status code was received; we should back off a bit.") +rescue Amocrm::Errors::APIStatusError => e + puts("Another non-200-range status code was received") + puts(e.status) +end +``` + +Error codes are as follows: + +| Cause | Error Type | +| ---------------- | -------------------------- | +| HTTP 400 | `BadRequestError` | +| HTTP 401 | `AuthenticationError` | +| HTTP 403 | `PermissionDeniedError` | +| HTTP 404 | `NotFoundError` | +| HTTP 409 | `ConflictError` | +| HTTP 422 | `UnprocessableEntityError` | +| HTTP 429 | `RateLimitError` | +| HTTP >= 500 | `InternalServerError` | +| Other HTTP error | `APIStatusError` | +| Timeout | `APITimeoutError` | +| Network error | `APIConnectionError` | + +### Retries + +Certain errors will be automatically retried 2 times by default, with a short exponential backoff. + +Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, >=500 Internal errors, and timeouts will all be retried by default. + +You can use the `max_retries` option to configure or disable this: + +```ruby +# Configure the default for all requests: +amocrm = Amocrm::Client.new( + max_retries: 0, # default is 2 + api_key: "My API Key" +) + +# Or, configure per-request: +amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {max_retries: 5} +) +``` + +### Timeouts + +By default, requests will time out after 60 seconds. You can use the timeout option to configure or disable this: + +```ruby +# Configure the default for all requests: +amocrm = Amocrm::Client.new( + timeout: nil, # default is 60 + api_key: "My API Key" +) + +# Or, configure per-request: +amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {timeout: 5} +) +``` + +On timeout, `Amocrm::Errors::APITimeoutError` is raised. + +Note that requests that time out are retried by default. + +## Advanced concepts + +### BaseModel + +All parameter and response objects inherit from `Amocrm::Internal::Type::BaseModel`, which provides several conveniences, including: + +1. All fields, including unknown ones, are accessible with `obj[:prop]` syntax, and can be destructured with `obj => {prop: prop}` or pattern-matching syntax. + +2. Structural equivalence for equality; if two API calls return the same values, comparing the responses with == will return true. + +3. Both instances and the classes themselves can be pretty-printed. + +4. Helpers such as `#to_h`, `#deep_to_h`, `#to_json`, and `#to_yaml`. + +### Making custom or undocumented requests + +#### Undocumented properties + +You can send undocumented parameters to any endpoint, and read undocumented response properties, like so: + +Note: the `extra_` parameters of the same name overrides the documented parameters. + +```ruby +response = + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: { + extra_query: {my_query_parameter: value}, + extra_body: {my_body_parameter: value}, + extra_headers: {"my-header": value} + } + ) + +puts(response[:my_undocumented_property]) +``` + +#### Undocumented request params + +If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` under the `request_options:` parameter when making a request, as seen in the examples above. + +#### Undocumented endpoints + +To make requests to undocumented endpoints while retaining the benefit of auth, retries, and so on, you can make requests using `client.request`, like so: + +```ruby +response = client.request( + method: :post, + path: '/undocumented/endpoint', + query: {"dog": "woof"}, + headers: {"useful-header": "interesting-value"}, + body: {"hello": "world"} +) +``` + +### Concurrency & connection pooling + +The `Amocrm::Client` instances are threadsafe, but are only are fork-safe when there are no in-flight HTTP requests. + +Each instance of `Amocrm::Client` has its own HTTP connection pool with a default size of 99. As such, we recommend instantiating the client once per application in most settings. + +When all available connections from the pool are checked out, requests wait for a new connection to become available, with queue time counting towards the request timeout. + +Unless otherwise specified, other classes in the SDK do not have locks protecting their underlying data structure. + +## Sorbet + +This library provides comprehensive [RBI](https://sorbet.org/docs/rbi) definitions, and has no dependency on sorbet-runtime. + +You can provide typesafe request parameters like so: + +```ruby +amocrm.v4.leads.unsorted.create_forms( + body: [ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body.new( + metadata: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata.new, + source_name: "source_name", + source_uid: "source_uid" + ) + ] +) +``` + +Or, equivalently: + +```ruby +# Hashes work, but are not typesafe: +amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] +) + +# You can also splat a full Params class: +params = Amocrm::V4::Leads::UnsortedCreateFormsParams.new( + body: [ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body.new( + metadata: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata.new, + source_name: "source_name", + source_uid: "source_uid" + ) + ] +) +amocrm.v4.leads.unsorted.create_forms(**params) +``` + +## Versioning + +This package follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions. As the library is in initial development and has a major version of `0`, APIs may change at any time. + +This package considers improvements to the (non-runtime) `*.rbi` and `*.rbs` type definitions to be non-breaking changes. + +## Requirements + +Ruby 3.2.0 or higher. + +## Contributing + +See [the contributing documentation](https://github.com/Hexlet/amocrm-ruby/tree/main/CONTRIBUTING.md). diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..8526099 --- /dev/null +++ b/Rakefile @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +require "pathname" +require "securerandom" +require "shellwords" + +require "minitest/test_task" +require "rake/clean" +require "rubocop/rake_task" + +tapioca = "sorbet/tapioca" +examples = "examples" +ignore_file = ".ignore" + +FILES_ENV = "FORMAT_FILE" + +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 --reload --quiet --bind [::] --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")] + .map { "require_relative(#{_1.dump});" } + .join + + ruby(*%w[-w -e], rb, verbose: false) { fail unless _1 } +end + +xargs = %w[xargs --no-run-if-empty --null --max-procs=0 --max-args=300 --] +ruby_opt = {"RUBYOPT" => [ENV["RUBYOPT"], "--encoding=UTF-8"].compact.join(" ")} + +filtered = ->(ext, dirs) do + if ENV.key?(FILES_ENV) + %w[sed -E -n -e] << "/\\.#{ext}$/p" << "--" << ENV.fetch(FILES_ENV) + else + (%w[find] + dirs + %w[-type f -and -name]) << "*.#{ext}" << "-print0" + end +end + +desc("Lint `*.rb(i)`") +multitask(:"lint:rubocop") do + find = %w[find ./lib ./test ./rbi ./examples -type f -and ( -name *.rb -or -name *.rbi ) -print0] + + rubocop = %w[rubocop] + rubocop += %w[--format github] if ENV.key?("CI") + + # some lines cannot be shortened + rubocop += %w[--except Lint/RedundantCopDisableDirective,Layout/LineLength] + + lint = xargs + rubocop + sh("#{find.shelljoin} | #{lint.shelljoin}") +end + +norm_lines = %w[tr -- \n \0].shelljoin + +desc("Format `*.rb`") +multitask(:"format:rb") do + # while `syntax_tree` is much faster than `rubocop`, `rubocop` is the only formatter with full syntax support + files = filtered["rb", %w[./lib ./test ./examples]] + fmt = xargs + %w[rubocop --fail-level F --autocorrect --format simple --] + sh("#{files.shelljoin} | #{norm_lines} | #{fmt.shelljoin}") +end + +desc("Format `*.rbi`") +multitask(:"format:rbi") do + files = filtered["rbi", %w[./rbi]] + fmt = xargs + %w[stree write --] + sh(ruby_opt, "#{files.shelljoin} | #{norm_lines} | #{fmt.shelljoin}") +end + +desc("Format `*.rbs`") +multitask(:"format:rbs") do + files = filtered["rbs", %w[./sig]] + inplace = /darwin|bsd/ =~ RUBY_PLATFORM ? ["-i", ""] : %w[-i] + uuid = SecureRandom.uuid + + # `syntax_tree` has trouble with `rbs`'s class & module aliases + + sed_bin = /darwin/ =~ RUBY_PLATFORM ? "/usr/bin/sed" : "sed" + sed = xargs + [sed_bin, "-E", *inplace, "-e"] + # annotate unprocessable aliases with a unique comment + pre = sed + ["s/(class|module) ([^ ]+) = (.+$)/# \\1 #{uuid}\\n\\2: \\3/", "--"] + fmt = xargs + %w[stree write --plugin=rbs --] + # remove the unique comment and unprocessable aliases to type aliases + subst = <<~SED + s/# (class|module) #{uuid}/\\1/ + t l1 + b + + : l1 + N + s/\\n *([^:]+): (.+)$/ \\1 = \\2/ + SED + # for each line: + # 1. try transform the unique comment into `class | module`, if successful, branch to label `l1`. + # 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("#{files.shelljoin} | #{norm_lines} | #{pre.shelljoin}") + # run syntax tree to format `*.rbs` files + sh(ruby_opt, "#{files.shelljoin} | #{norm_lines} | #{fmt.shelljoin}") do + success = _1 + end + # transform type aliases back to class aliases + sh("#{files.shelljoin} | #{norm_lines} | #{pst.shelljoin}") + + # always run post-processing to remove comment marker + fail unless success +end + +desc("Format everything") +multitask(format: [:"format:rb", :"format:rbi", :"format:rbs"]) + +desc("Typecheck `*.rbs`") +multitask(:"typecheck:steep") do + sh(*%w[steep check]) +end + +directory(examples) + +desc("Typecheck `*.rbi`") +multitask("typecheck:sorbet": examples) do + sh(*%w[srb typecheck --dir], examples) +end + +directory(tapioca) do + sh(*%w[tapioca init]) +end + +desc("Typecheck everything") +multitask(typecheck: [:"typecheck:steep", :"typecheck:sorbet"]) + +desc("Lint and typecheck") +multitask(lint: [:"lint:rubocop", :typecheck]) + +desc("Build yard docs") +multitask(:"build:docs") do + sh(*%w[yard]) +end + +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 -- amocrm.gemspec]) + rm_rf(ignore_file) +end + +desc("Release ruby gem") +multitask(release: [:"build:gem"]) do + sh(*%w[gem push], *FileList["*.gem"]) +end diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0b5da91 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainless.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Amocrm, please follow the respective company's security reporting guidelines. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/Steepfile b/Steepfile new file mode 100644 index 0000000..528b48c --- /dev/null +++ b/Steepfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "yaml" + +target(:lib) do + configure_code_diagnostics(Steep::Diagnostic::Ruby.strict) + + signature("sig") + + YAML.safe_load_file("./manifest.yaml", symbolize_names: true) => {dependencies:} + # currently these libraries lack the `*.rbs` annotations required by `steep` + stdlibs = dependencies - %w[English etc net/http rbconfig set stringio] + + stdlibs.each { library(_1) } +end diff --git a/amocrm.gemspec b/amocrm.gemspec new file mode 100644 index 0000000..75512e9 --- /dev/null +++ b/amocrm.gemspec @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "lib/amocrm/version" + +Gem::Specification.new do |s| + s.name = "amocrm" + s.version = Amocrm::VERSION + s.summary = "Ruby library to access the Amocrm API" + s.authors = ["Amocrm"] + s.email = "" + s.homepage = "https://gemdocs.org/gems/amocrm" + s.metadata["homepage_uri"] = s.homepage + s.metadata["source_code_uri"] = "https://github.com/Hexlet/amocrm-ruby" + s.metadata["rubygems_mfa_required"] = false.to_s + s.required_ruby_version = ">= 3.2.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 "cgi" + s.add_dependency "connection_pool" +end diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..c05436e --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${GEM_HOST_API_KEY}" ]; then + errors+=("The GEM_HOST_API_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/bin/publish-gem b/bin/publish-gem new file mode 100644 index 0000000..8444af2 --- /dev/null +++ b/bin/publish-gem @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +cd -- "$(dirname -- "$0")/.." + +bundle +find . -maxdepth 1 -type f -name "*.gem" -delete +rake release \ No newline at end of file diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 0000000..d8c73e9 --- /dev/null +++ b/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/lib/amocrm.rb b/lib/amocrm.rb new file mode 100644 index 0000000..a22b629 --- /dev/null +++ b/lib/amocrm.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# Standard libraries. +# rubocop:disable Lint/RedundantRequireStatement +require "English" +require "base64" +require "cgi" +require "date" +require "erb" +require "etc" +require "json" +require "net/http" +require "openssl" +require "pathname" +require "rbconfig" +require "securerandom" +require "set" +require "stringio" +require "time" +require "uri" +# rubocop:enable Lint/RedundantRequireStatement + +# We already ship the preferred sorbet manifests in the package itself. +# `tapioca` currently does not offer us a way to opt out of unnecessary compilation. +if Object.const_defined?(:Tapioca) && + caller.chain([$PROGRAM_NAME]).chain(ARGV).any?(/tapioca/) && + ARGV.none?(/dsl/) + return +end + +# Gems. +require "connection_pool" + +# Package files. +require_relative "amocrm/version" +require_relative "amocrm/internal/util" +require_relative "amocrm/internal/type/converter" +require_relative "amocrm/internal/type/unknown" +require_relative "amocrm/internal/type/boolean" +require_relative "amocrm/internal/type/file_input" +require_relative "amocrm/internal/type/enum" +require_relative "amocrm/internal/type/union" +require_relative "amocrm/internal/type/array_of" +require_relative "amocrm/internal/type/hash_of" +require_relative "amocrm/internal/type/base_model" +require_relative "amocrm/internal/type/base_page" +require_relative "amocrm/internal/type/request_parameters" +require_relative "amocrm/internal" +require_relative "amocrm/request_options" +require_relative "amocrm/file_part" +require_relative "amocrm/errors" +require_relative "amocrm/internal/transport/base_client" +require_relative "amocrm/internal/transport/pooled_net_requester" +require_relative "amocrm/client" +require_relative "amocrm/models/v4/leads/unsorted_accept_params" +require_relative "amocrm/models/v4/leads/unsorted_accept_response" +require_relative "amocrm/models/v4/leads/unsorted_create_forms_params" +require_relative "amocrm/models/v4/leads/unsorted_create_forms_response" +require_relative "amocrm/models/v4/leads/unsorted_decline_params" +require_relative "amocrm/models/v4/leads/unsorted_decline_response" +require_relative "amocrm/models" +require_relative "amocrm/resources/v4" +require_relative "amocrm/resources/v4/leads" +require_relative "amocrm/resources/v4/leads/unsorted" diff --git a/lib/amocrm/client.rb b/lib/amocrm/client.rb new file mode 100644 index 0000000..4bea66d --- /dev/null +++ b/lib/amocrm/client.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Amocrm + class Client < Amocrm::Internal::Transport::BaseClient + # Default max number of retries to attempt after a failed retryable request. + DEFAULT_MAX_RETRIES = 2 + + # Default per-request timeout. + DEFAULT_TIMEOUT_IN_SECONDS = 60.0 + + # Default initial retry delay in seconds. + # Overall delay is calculated using exponential backoff + jitter. + DEFAULT_INITIAL_RETRY_DELAY = 0.5 + + # Default max retry delay in seconds. + DEFAULT_MAX_RETRY_DELAY = 8.0 + + # @return [String] + attr_reader :api_key + + # @return [Amocrm::Resources::V4] + attr_reader :v4 + + # Creates and returns a new client for interacting with the API. + # + # @param api_key [String, nil] Defaults to `ENV["AMOCRM_API_KEY"]` + # + # @param base_url [String, nil] Override the default base URL for the API, e.g., + # `"https://api.example.com/v2/"`. Defaults to `ENV["AMOCRM_BASE_URL"]` + # + # @param max_retries [Integer] Max number of retries to attempt after a failed retryable request. + # + # @param timeout [Float] + # + # @param initial_retry_delay [Float] + # + # @param max_retry_delay [Float] + def initialize( + api_key: ENV["AMOCRM_API_KEY"], + base_url: ENV["AMOCRM_BASE_URL"], + max_retries: self.class::DEFAULT_MAX_RETRIES, + timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS, + initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY, + max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY + ) + base_url ||= "https://{subdomain}.amocrm.ru" + + if api_key.nil? + raise ArgumentError.new("api_key is required, and can be set via environ: \"AMOCRM_API_KEY\"") + end + + @api_key = api_key.to_s + + super( + base_url: base_url, + timeout: timeout, + max_retries: max_retries, + initial_retry_delay: initial_retry_delay, + max_retry_delay: max_retry_delay + ) + + @v4 = Amocrm::Resources::V4.new(client: self) + end + end +end diff --git a/lib/amocrm/errors.rb b/lib/amocrm/errors.rb new file mode 100644 index 0000000..330be7d --- /dev/null +++ b/lib/amocrm/errors.rb @@ -0,0 +1,228 @@ +# frozen_string_literal: true + +module Amocrm + module Errors + class Error < StandardError + # @!attribute cause + # + # @return [StandardError, nil] + end + + class ConversionError < Amocrm::Errors::Error + # @return [StandardError, nil] + def cause = @cause.nil? ? super : @cause + + # @api private + # + # @param on [Class] + # @param method [Symbol] + # @param target [Object] + # @param value [Object] + # @param cause [StandardError, nil] + def initialize(on:, method:, target:, value:, cause: nil) + cls = on.name.split("::").last + + message = [ + "Failed to parse #{cls}.#{method} from #{value.class} to #{target.inspect}.", + "To get the unparsed API response, use #{cls}[#{method.inspect}].", + cause && "Cause: #{cause.message}" + ].filter(&:itself).join(" ") + + @cause = cause + super(message) + end + end + + class APIError < Amocrm::Errors::Error + # @return [URI::Generic] + attr_accessor :url + + # @return [Integer, nil] + attr_accessor :status + + # @return [Hash{String=>String}, nil] + attr_accessor :headers + + # @return [Object, nil] + attr_accessor :body + + # @api private + # + # @param url [URI::Generic] + # @param status [Integer, nil] + # @param headers [Hash{String=>String}, nil] + # @param body [Object, nil] + # @param request [nil] + # @param response [nil] + # @param message [String, nil] + def initialize(url:, status: nil, headers: nil, body: nil, request: nil, response: nil, message: nil) + @url = url + @status = status + @headers = headers + @body = body + @request = request + @response = response + super(message) + end + end + + class APIConnectionError < Amocrm::Errors::APIError + # @!attribute status + # + # @return [nil] + + # @!attribute body + # + # @return [nil] + + # @api private + # + # @param url [URI::Generic] + # @param status [nil] + # @param headers [Hash{String=>String}, nil] + # @param body [nil] + # @param request [nil] + # @param response [nil] + # @param message [String, nil] + def initialize( + url:, + status: nil, + headers: nil, + body: nil, + request: nil, + response: nil, + message: "Connection error." + ) + super + end + end + + class APITimeoutError < Amocrm::Errors::APIConnectionError + # @api private + # + # @param url [URI::Generic] + # @param status [nil] + # @param headers [Hash{String=>String}, nil] + # @param body [nil] + # @param request [nil] + # @param response [nil] + # @param message [String, nil] + def initialize( + url:, + status: nil, + headers: nil, + body: nil, + request: nil, + response: nil, + message: "Request timed out." + ) + super + end + end + + class APIStatusError < Amocrm::Errors::APIError + # @api private + # + # @param url [URI::Generic] + # @param status [Integer] + # @param headers [Hash{String=>String}, nil] + # @param body [Object, nil] + # @param request [nil] + # @param response [nil] + # @param message [String, nil] + # + # @return [self] + def self.for(url:, status:, headers:, body:, request:, response:, message: nil) + kwargs = + { + url: url, + status: status, + headers: headers, + body: body, + request: request, + response: response, + message: message + } + + case status + in 400 + Amocrm::Errors::BadRequestError.new(**kwargs) + in 401 + Amocrm::Errors::AuthenticationError.new(**kwargs) + in 403 + Amocrm::Errors::PermissionDeniedError.new(**kwargs) + in 404 + Amocrm::Errors::NotFoundError.new(**kwargs) + in 409 + Amocrm::Errors::ConflictError.new(**kwargs) + in 422 + Amocrm::Errors::UnprocessableEntityError.new(**kwargs) + in 429 + Amocrm::Errors::RateLimitError.new(**kwargs) + in (500..) + Amocrm::Errors::InternalServerError.new(**kwargs) + else + Amocrm::Errors::APIStatusError.new(**kwargs) + end + end + + # @!parse + # # @return [Integer] + # attr_accessor :status + + # @api private + # + # @param url [URI::Generic] + # @param status [Integer] + # @param headers [Hash{String=>String}, nil] + # @param body [Object, nil] + # @param request [nil] + # @param response [nil] + # @param message [String, nil] + def initialize(url:, status:, headers:, body:, request:, response:, message: nil) + message ||= {url: url.to_s, status: status, body: body} + super( + url: url, + status: status, + headers: headers, + body: body, + request: request, + response: response, + message: message&.to_s + ) + end + end + + class BadRequestError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 400 + end + + class AuthenticationError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 401 + end + + class PermissionDeniedError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 403 + end + + class NotFoundError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 404 + end + + class ConflictError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 409 + end + + class UnprocessableEntityError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 422 + end + + class RateLimitError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 429 + end + + class InternalServerError < Amocrm::Errors::APIStatusError + HTTP_STATUS = (500..) + end + end +end diff --git a/lib/amocrm/file_part.rb b/lib/amocrm/file_part.rb new file mode 100644 index 0000000..909db8f --- /dev/null +++ b/lib/amocrm/file_part.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Amocrm + class FilePart + # @return [Pathname, StringIO, IO, String] + attr_reader :content + + # @return [String, nil] + attr_reader :content_type + + # @return [String, nil] + attr_reader :filename + + # @api private + # + # @return [String] + private def read + case content + in Pathname + content.read(binmode: true) + in StringIO + content.string + in IO + content.read + in String + content + end + end + + # @param a [Object] + # + # @return [String] + def to_json(*a) = read.to_json(*a) + + # @param a [Object] + # + # @return [String] + def to_yaml(*a) = read.to_yaml(*a) + + # @param content [Pathname, StringIO, IO, String] + # @param filename [Pathname, String, nil] + # @param content_type [String, nil] + def initialize(content, filename: nil, content_type: nil) + @content_type = content_type + @filename = + case [filename, (@content = content)] + in [String | Pathname, _] + ::File.basename(filename) + in [nil, Pathname] + content.basename.to_path + in [nil, IO] + content.to_path + else + filename + end + end + end +end diff --git a/lib/amocrm/internal.rb b/lib/amocrm/internal.rb new file mode 100644 index 0000000..7da322a --- /dev/null +++ b/lib/amocrm/internal.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + OMIT = + Object.new.tap do + _1.define_singleton_method(:inspect) { "#<#{Amocrm::Internal}::OMIT>" } + end + .freeze + + define_sorbet_constant!(:AnyHash) do + T.type_alias { T::Hash[Symbol, T.anything] } + end + define_sorbet_constant!(:FileInput) do + T.type_alias { T.any(Pathname, StringIO, IO, String, Amocrm::FilePart) } + end + end +end diff --git a/lib/amocrm/internal/transport/base_client.rb b/lib/amocrm/internal/transport/base_client.rb new file mode 100644 index 0000000..a02253a --- /dev/null +++ b/lib/amocrm/internal/transport/base_client.rb @@ -0,0 +1,567 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Transport + # @api private + # + # @abstract + class BaseClient + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + # from whatwg fetch spec + MAX_REDIRECTS = 20 + + # rubocop:disable Style/MutableConstant + PLATFORM_HEADERS = + { + "x-stainless-arch" => Amocrm::Internal::Util.arch, + "x-stainless-lang" => "ruby", + "x-stainless-os" => Amocrm::Internal::Util.os, + "x-stainless-package-version" => Amocrm::VERSION, + "x-stainless-runtime" => ::RUBY_ENGINE, + "x-stainless-runtime-version" => ::RUBY_ENGINE_VERSION + } + # rubocop:enable Style/MutableConstant + + class << self + # @api private + # + # @param req [Hash{Symbol=>Object}] + # + # @raise [ArgumentError] + def validate!(req) + keys = [:method, :path, :query, :headers, :body, :unwrap, :page, :stream, :model, :options] + case req + in Hash + req.each_key do |k| + unless keys.include?(k) + raise ArgumentError.new("Request `req` keys must be one of #{keys}, got #{k.inspect}") + end + end + else + raise ArgumentError.new("Request `req` must be a Hash or RequestOptions, got #{req.inspect}") + end + end + + # @api private + # + # @param status [Integer] + # @param headers [Hash{String=>String}] + # + # @return [Boolean] + def should_retry?(status, headers:) + coerced = Amocrm::Internal::Util.coerce_boolean(headers["x-should-retry"]) + case [coerced, status] + in [true | false, _] + coerced + in [_, 408 | 409 | 429 | (500..)] + # retry on: + # 408: timeouts + # 409: locks + # 429: rate limits + # 500+: unknown errors + true + else + false + end + end + + # @api private + # + # @param request [Hash{Symbol=>Object}] . + # + # @option request [Symbol] :method + # + # @option request [URI::Generic] :url + # + # @option request [Hash{String=>String}] :headers + # + # @option request [Object] :body + # + # @option request [Integer] :max_retries + # + # @option request [Float] :timeout + # + # @param status [Integer] + # + # @param response_headers [Hash{String=>String}] + # + # @return [Hash{Symbol=>Object}] + def follow_redirect(request, status:, response_headers:) + method, url, headers = request.fetch_values(:method, :url, :headers) + location = + Kernel.then do + URI.join(url, response_headers["location"]) + rescue ArgumentError + message = "Server responded with status #{status} but no valid location header." + raise Amocrm::Errors::APIConnectionError.new( + url: url, + response: response_headers, + message: message + ) + end + + request = {**request, url: location} + + case [url.scheme, location.scheme] + in ["https", "http"] + message = "Tried to redirect to a insecure URL" + raise Amocrm::Errors::APIConnectionError.new( + url: url, + response: response_headers, + message: message + ) + else + nil + end + + # from whatwg fetch spec + case [status, method] + in [301 | 302, :post] | [303, _] + drop = %w[content-encoding content-language content-length content-location content-type] + request = { + **request, + method: method == :head ? :head : :get, + headers: headers.except(*drop), + body: nil + } + else + end + + # from undici + if Amocrm::Internal::Util.uri_origin(url) != Amocrm::Internal::Util.uri_origin(location) + drop = %w[authorization cookie host proxy-authorization] + request = {**request, headers: request.fetch(:headers).except(*drop)} + end + + request + end + + # @api private + # + # @param status [Integer, Amocrm::Errors::APIConnectionError] + # @param stream [Enumerable, nil] + def reap_connection!(status, stream:) + case status + in (..199) | (300..499) + stream&.each { next } + in Amocrm::Errors::APIConnectionError | (500..) + Amocrm::Internal::Util.close_fused!(stream) + else + end + end + end + + # @return [URI::Generic] + attr_reader :base_url + + # @return [Float] + attr_reader :timeout + + # @return [Integer] + attr_reader :max_retries + + # @return [Float] + attr_reader :initial_retry_delay + + # @return [Float] + attr_reader :max_retry_delay + + # @return [Hash{String=>String}] + attr_reader :headers + + # @return [String, nil] + attr_reader :idempotency_header + + # @api private + # @return [Amocrm::Internal::Transport::PooledNetRequester] + attr_reader :requester + + # @api private + # + # @param base_url [String] + # @param timeout [Float] + # @param max_retries [Integer] + # @param initial_retry_delay [Float] + # @param max_retry_delay [Float] + # @param headers [Hash{String=>String, Integer, Array, nil}] + # @param idempotency_header [String, nil] + def initialize( + base_url:, + timeout: 0.0, + max_retries: 0, + initial_retry_delay: 0.0, + max_retry_delay: 0.0, + headers: {}, + idempotency_header: nil + ) + @requester = Amocrm::Internal::Transport::PooledNetRequester.new + @headers = Amocrm::Internal::Util.normalized_headers( + self.class::PLATFORM_HEADERS, + { + "accept" => "application/json", + "content-type" => "application/json", + "user-agent" => user_agent + }, + headers + ) + @base_url_components = Amocrm::Internal::Util.parse_uri(base_url) + @base_url = Amocrm::Internal::Util.unparse_uri(@base_url_components) + @idempotency_header = idempotency_header&.to_s&.downcase + @timeout = timeout + @max_retries = max_retries + @initial_retry_delay = initial_retry_delay + @max_retry_delay = max_retry_delay + end + + # @api private + # + # @return [String] + private def user_agent = "#{self.class.name}/Ruby #{Amocrm::VERSION}" + + # @api private + # + # @return [String] + private def generate_idempotency_key = "stainless-ruby-retry-#{SecureRandom.uuid}" + + # @api private + # + # @param req [Hash{Symbol=>Object}] . + # + # @option req [Symbol] :method + # + # @option req [String, Array] :path + # + # @option req [Hash{String=>Array, String, nil}, nil] :query + # + # @option req [Hash{String=>String, Integer, Array, nil}, nil] :headers + # + # @option req [Object, nil] :body + # + # @option req [Symbol, Integer, Array, Proc, nil] :unwrap + # + # @option req [Class, nil] :page + # + # @option req [Class, nil] :stream + # + # @option req [Amocrm::Internal::Type::Converter, Class, nil] :model + # + # @param opts [Hash{Symbol=>Object}] . + # + # @option opts [String, nil] :idempotency_key + # + # @option opts [Hash{String=>Array, String, nil}, nil] :extra_query + # + # @option opts [Hash{String=>String, nil}, nil] :extra_headers + # + # @option opts [Object, nil] :extra_body + # + # @option opts [Integer, nil] :max_retries + # + # @option opts [Float, nil] :timeout + # + # @return [Hash{Symbol=>Object}] + private def build_request(req, opts) + method, uninterpolated_path = req.fetch_values(:method, :path) + + path = Amocrm::Internal::Util.interpolate_path(uninterpolated_path) + + query = Amocrm::Internal::Util.deep_merge(req[:query].to_h, opts[:extra_query].to_h) + + headers = Amocrm::Internal::Util.normalized_headers( + @headers, + req[:headers].to_h, + opts[:extra_headers].to_h + ) + + if @idempotency_header && + !headers.key?(@idempotency_header) && + (!Net::HTTP::IDEMPOTENT_METHODS_.include?(method.to_s.upcase) || opts.key?(:idempotency_key)) + headers[@idempotency_header] = opts.fetch(:idempotency_key) { generate_idempotency_key } + end + + unless headers.key?("x-stainless-retry-count") + headers["x-stainless-retry-count"] = "0" + end + + timeout = opts.fetch(:timeout, @timeout).to_f.clamp(0..) + unless headers.key?("x-stainless-timeout") || timeout.zero? + headers["x-stainless-timeout"] = timeout.to_s + end + + headers.reject! { |_, v| v.to_s.empty? } + + body = + case method + in :get | :head | :options | :trace + nil + else + Amocrm::Internal::Util.deep_merge(*[req[:body], opts[:extra_body]].compact) + end + + url = Amocrm::Internal::Util.join_parsed_uri( + @base_url_components, + {**req, path: path, query: query} + ) + headers, encoded = Amocrm::Internal::Util.encode_content(headers, body) + { + method: method, + url: url, + headers: headers, + body: encoded, + max_retries: opts.fetch(:max_retries, @max_retries), + timeout: timeout + } + end + + # @api private + # + # @param headers [Hash{String=>String}] + # @param retry_count [Integer] + # + # @return [Float] + private def retry_delay(headers, retry_count:) + # Non-standard extension + span = Float(headers["retry-after-ms"], exception: false)&.then { _1 / 1000 } + return span if span + + retry_header = headers["retry-after"] + return span if (span = Float(retry_header, exception: false)) + + span = retry_header&.then do + Time.httpdate(_1) - Time.now + rescue ArgumentError + nil + end + return span if span + + scale = retry_count**2 + jitter = 1 - (0.25 * rand) + (@initial_retry_delay * scale * jitter).clamp(0, @max_retry_delay) + end + + # @api private + # + # @param request [Hash{Symbol=>Object}] . + # + # @option request [Symbol] :method + # + # @option request [URI::Generic] :url + # + # @option request [Hash{String=>String}] :headers + # + # @option request [Object] :body + # + # @option request [Integer] :max_retries + # + # @option request [Float] :timeout + # + # @param redirect_count [Integer] + # + # @param retry_count [Integer] + # + # @param send_retry_header [Boolean] + # + # @raise [Amocrm::Errors::APIError] + # @return [Array(Integer, Net::HTTPResponse, Enumerable)] + def send_request(request, redirect_count:, retry_count:, send_retry_header:) + url, headers, max_retries, timeout = request.fetch_values(:url, :headers, :max_retries, :timeout) + input = {**request.except(:timeout), deadline: Amocrm::Internal::Util.monotonic_secs + timeout} + + if send_retry_header + headers["x-stainless-retry-count"] = retry_count.to_s + end + + begin + status, response, stream = @requester.execute(input) + rescue Amocrm::Errors::APIConnectionError => e + status = e + end + headers = Amocrm::Internal::Util.normalized_headers(response&.each_header&.to_h) + + case status + in ..299 + [status, response, stream] + in 300..399 if redirect_count >= self.class::MAX_REDIRECTS + self.class.reap_connection!(status, stream: stream) + + message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects." + raise Amocrm::Errors::APIConnectionError.new(url: url, response: response, message: message) + in 300..399 + self.class.reap_connection!(status, stream: stream) + + request = self.class.follow_redirect(request, status: status, response_headers: headers) + send_request( + request, + redirect_count: redirect_count + 1, + retry_count: retry_count, + send_retry_header: send_retry_header + ) + in Amocrm::Errors::APIConnectionError if retry_count >= max_retries + raise status + in (400..) if retry_count >= max_retries || !self.class.should_retry?(status, headers: headers) + decoded = Kernel.then do + Amocrm::Internal::Util.decode_content(headers, stream: stream, suppress_error: true) + ensure + self.class.reap_connection!(status, stream: stream) + end + + raise Amocrm::Errors::APIStatusError.for( + url: url, + status: status, + headers: headers, + body: decoded, + request: nil, + response: response + ) + in (400..) | Amocrm::Errors::APIConnectionError + self.class.reap_connection!(status, stream: stream) + + delay = retry_delay(response || {}, retry_count: retry_count) + sleep(delay) + + send_request( + request, + redirect_count: redirect_count, + retry_count: retry_count + 1, + send_retry_header: send_retry_header + ) + end + end + + # Execute the request specified by `req`. This is the method that all resource + # methods call into. + # + # @overload request(method, path, query: {}, headers: {}, body: nil, unwrap: nil, page: nil, stream: nil, model: Amocrm::Internal::Type::Unknown, options: {}) + # + # @param method [Symbol] + # + # @param path [String, Array] + # + # @param query [Hash{String=>Array, String, nil}, nil] + # + # @param headers [Hash{String=>String, Integer, Array, nil}, nil] + # + # @param body [Object, nil] + # + # @param unwrap [Symbol, Integer, Array, Proc, nil] + # + # @param page [Class, nil] + # + # @param stream [Class, nil] + # + # @param model [Amocrm::Internal::Type::Converter, Class, nil] + # + # @param options [Amocrm::RequestOptions, Hash{Symbol=>Object}, nil] . + # + # @option options [String, nil] :idempotency_key + # + # @option options [Hash{String=>Array, String, nil}, nil] :extra_query + # + # @option options [Hash{String=>String, nil}, nil] :extra_headers + # + # @option options [Object, nil] :extra_body + # + # @option options [Integer, nil] :max_retries + # + # @option options [Float, nil] :timeout + # + # @raise [Amocrm::Errors::APIError] + # @return [Object] + def request(req) + self.class.validate!(req) + model = req.fetch(:model) { Amocrm::Internal::Type::Unknown } + opts = req[:options].to_h + unwrap = req[:unwrap] + Amocrm::RequestOptions.validate!(opts) + request = build_request(req.except(:options), opts) + url = request.fetch(:url) + + # Don't send the current retry count in the headers if the caller modified the header defaults. + send_retry_header = request.fetch(:headers)["x-stainless-retry-count"] == "0" + status, response, stream = send_request( + request, + redirect_count: 0, + retry_count: 0, + send_retry_header: send_retry_header + ) + + headers = Amocrm::Internal::Util.normalized_headers(response.each_header.to_h) + decoded = Amocrm::Internal::Util.decode_content(headers, stream: stream) + case req + in {stream: Class => st} + st.new( + model: model, + url: url, + status: status, + headers: headers, + response: response, + unwrap: unwrap, + stream: decoded + ) + in {page: Class => page} + page.new(client: self, req: req, headers: headers, page_data: decoded) + else + unwrapped = Amocrm::Internal::Util.dig(decoded, unwrap) + Amocrm::Internal::Type::Converter.coerce(model, unwrapped) + end + end + + # @api private + # + # @return [String] + def inspect + # rubocop:disable Layout/LineLength + "#<#{self.class.name}:0x#{object_id.to_s(16)} base_url=#{@base_url} max_retries=#{@max_retries} timeout=#{@timeout}>" + # rubocop:enable Layout/LineLength + end + + define_sorbet_constant!(:RequestComponents) do + T.type_alias do + { + method: Symbol, + path: T.any(String, T::Array[String]), + query: T.nilable(T::Hash[String, T.nilable(T.any(T::Array[String], String))]), + headers: T.nilable( + T::Hash[String, + T.nilable( + T.any( + String, + Integer, + T::Array[T.nilable(T.any(String, Integer))] + ) + )] + ), + body: T.nilable(T.anything), + unwrap: T.nilable( + T.any( + Symbol, + Integer, + T::Array[T.any(Symbol, Integer)], + T.proc.params(arg0: T.anything).returns(T.anything) + ) + ), + page: T.nilable(T::Class[Amocrm::Internal::Type::BasePage[Amocrm::Internal::Type::BaseModel]]), + stream: T.nilable(T::Class[T.anything]), + model: T.nilable(Amocrm::Internal::Type::Converter::Input), + options: T.nilable(Amocrm::RequestOptions::OrHash) + } + end + end + define_sorbet_constant!(:RequestInput) do + T.type_alias do + { + method: Symbol, + url: URI::Generic, + headers: T::Hash[String, String], + body: T.anything, + max_retries: Integer, + timeout: Float + } + end + end + end + end + end +end diff --git a/lib/amocrm/internal/transport/pooled_net_requester.rb b/lib/amocrm/internal/transport/pooled_net_requester.rb new file mode 100644 index 0000000..2017698 --- /dev/null +++ b/lib/amocrm/internal/transport/pooled_net_requester.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Transport + # @api private + class PooledNetRequester + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + # from the golang stdlib + # https://github.com/golang/go/blob/c8eced8580028328fde7c03cbfcb720ce15b2358/src/net/http/transport.go#L49 + KEEP_ALIVE_TIMEOUT = 30 + + DEFAULT_MAX_CONNECTIONS = [Etc.nprocessors, 99].max + + class << self + # @api private + # + # @param cert_store [OpenSSL::X509::Store] + # @param url [URI::Generic] + # + # @return [Net::HTTP] + def connect(cert_store:, url:) + port = + case [url.port, url.scheme] + in [Integer, _] + url.port + in [nil, "http" | "ws"] + Net::HTTP.http_default_port + in [nil, "https" | "wss"] + Net::HTTP.https_default_port + end + + Net::HTTP.new(url.host, port).tap do + _1.use_ssl = %w[https wss].include?(url.scheme) + _1.max_retries = 0 + + (_1.cert_store = cert_store) if _1.use_ssl? + end + end + + # @api private + # + # @param conn [Net::HTTP] + # @param deadline [Float] + def calibrate_socket_timeout(conn, deadline) + timeout = deadline - Amocrm::Internal::Util.monotonic_secs + conn.open_timeout = conn.read_timeout = conn.write_timeout = conn.continue_timeout = timeout + end + + # @api private + # + # @param request [Hash{Symbol=>Object}] . + # + # @option request [Symbol] :method + # + # @option request [URI::Generic] :url + # + # @option request [Hash{String=>String}] :headers + # + # @param blk [Proc] + # + # @yieldparam [String] + # @return [Array(Net::HTTPGenericRequest, Proc)] + def build_request(request, &blk) + method, url, headers, body = request.fetch_values(:method, :url, :headers, :body) + req = Net::HTTPGenericRequest.new( + method.to_s.upcase, + !body.nil?, + method != :head, + URI(url.to_s) # ensure we construct a URI class of the right scheme + ) + + headers.each { req[_1] = _2 } + + case body + in nil + nil + in String + req["content-length"] ||= body.bytesize.to_s unless req["transfer-encoding"] + req.body_stream = Amocrm::Internal::Util::ReadIOAdapter.new(body, &blk) + in StringIO + req["content-length"] ||= body.size.to_s unless req["transfer-encoding"] + req.body_stream = Amocrm::Internal::Util::ReadIOAdapter.new(body, &blk) + in Pathname | IO | Enumerator + req["transfer-encoding"] ||= "chunked" unless req["content-length"] + req.body_stream = Amocrm::Internal::Util::ReadIOAdapter.new(body, &blk) + end + + [req, req.body_stream&.method(:close)] + end + end + + # @api private + # + # @param url [URI::Generic] + # @param deadline [Float] + # @param blk [Proc] + # + # @raise [Timeout::Error] + # @yieldparam [Net::HTTP] + private def with_pool(url, deadline:, &blk) + origin = Amocrm::Internal::Util.uri_origin(url) + timeout = deadline - Amocrm::Internal::Util.monotonic_secs + pool = + @mutex.synchronize do + @pools[origin] ||= ConnectionPool.new(size: @size) do + self.class.connect(cert_store: @cert_store, url: url) + end + end + + pool.with(timeout: timeout, &blk) + end + + # @api private + # + # @param request [Hash{Symbol=>Object}] . + # + # @option request [Symbol] :method + # + # @option request [URI::Generic] :url + # + # @option request [Hash{String=>String}] :headers + # + # @option request [Object] :body + # + # @option request [Float] :deadline + # + # @return [Array(Integer, Net::HTTPResponse, Enumerable)] + def execute(request) + url, deadline = request.fetch_values(:url, :deadline) + + req = nil + finished = false + + # rubocop:disable Metrics/BlockLength + enum = Enumerator.new do |y| + next if finished + + with_pool(url, deadline: deadline) do |conn| + eof = false + closing = nil + ::Thread.handle_interrupt(Object => :never) do + ::Thread.handle_interrupt(Object => :immediate) do + req, closing = self.class.build_request(request) do + self.class.calibrate_socket_timeout(conn, deadline) + end + + self.class.calibrate_socket_timeout(conn, deadline) + unless conn.started? + conn.keep_alive_timeout = self.class::KEEP_ALIVE_TIMEOUT + conn.start + end + + self.class.calibrate_socket_timeout(conn, deadline) + ::Kernel.catch(:jump) do + conn.request(req) do |rsp| + y << [req, rsp] + ::Kernel.throw(:jump) if finished + + rsp.read_body do |bytes| + y << bytes.force_encoding(Encoding::BINARY) + ::Kernel.throw(:jump) if finished + + self.class.calibrate_socket_timeout(conn, deadline) + end + eof = true + end + end + end + ensure + begin + conn.finish if !eof && conn&.started? + ensure + closing&.call + end + end + end + rescue Timeout::Error + raise Amocrm::Errors::APITimeoutError.new(url: url, request: req) + rescue StandardError + raise Amocrm::Errors::APIConnectionError.new(url: url, request: req) + end + # rubocop:enable Metrics/BlockLength + + _, response = enum.next + body = Amocrm::Internal::Util.fused_enum(enum, external: true) do + finished = true + loop { enum.next } + end + [Integer(response.code), response, body] + end + + # @api private + # + # @param size [Integer] + def initialize(size: self.class::DEFAULT_MAX_CONNECTIONS) + @mutex = Mutex.new + @size = size + @cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths) + @pools = {} + end + + define_sorbet_constant!(:Request) do + T.type_alias { {method: Symbol, url: URI::Generic, headers: T::Hash[String, String], body: T.anything, deadline: Float} } + end + end + end + end +end diff --git a/lib/amocrm/internal/type/array_of.rb b/lib/amocrm/internal/type/array_of.rb new file mode 100644 index 0000000..7c6ad6f --- /dev/null +++ b/lib/amocrm/internal/type/array_of.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + # + # @abstract + # + # @generic Elem + # + # Array of items of a given type. + class ArrayOf + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + private_class_method :new + + # @overload [](type_info, spec = {}) + # + # @param type_info [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] + # + # @param spec [Hash{Symbol=>Object}] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + # + # @return [self] + def self.[](...) = new(...) + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ===(other) = other.is_a?(Array) && other.all?(item_type) + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ==(other) + # rubocop:disable Layout/LineLength + other.is_a?(Amocrm::Internal::Type::ArrayOf) && other.nilable? == nilable? && other.item_type == item_type + # rubocop:enable Layout/LineLength + end + + # @api public + # + # @return [Integer] + def hash = [self.class, item_type].hash + + # @api private + # + # @param value [Array, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [Array, Object] + def coerce(value, state:) + exactness = state.fetch(:exactness) + + unless value.is_a?(Array) + exactness[:no] += 1 + state[:error] = TypeError.new("#{value.class} can't be coerced into #{Array}") + return value + end + + target = item_type + exactness[:yes] += 1 + value + .map do |item| + case [nilable?, item] + in [true, nil] + exactness[:yes] += 1 + nil + else + Amocrm::Internal::Type::Converter.coerce(target, item, state: state) + end + end + end + + # @api private + # + # @param value [Array, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Array, Object] + def dump(value, state:) + target = item_type + if value.is_a?(Array) + value.map do + Amocrm::Internal::Type::Converter.dump(target, _1, state: state) + end + else + super + end + end + + # @api private + # + # @return [Object] + def to_sorbet_type + T::Array[Amocrm::Internal::Util::SorbetRuntimeSupport.to_sorbet_type(item_type)] + end + + # @api private + # + # @return [generic] + protected def item_type = @item_type_fn.call + + # @api private + # + # @return [Boolean] + protected def nilable? = @nilable + + # @api private + # + # @param type_info [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] + # + # @param spec [Hash{Symbol=>Object}] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + def initialize(type_info, spec = {}) + @item_type_fn = Amocrm::Internal::Type::Converter.type_info(type_info || spec) + @meta = Amocrm::Internal::Type::Converter.meta_info(type_info, spec) + @nilable = spec.fetch(:nil?, false) + end + + # @api private + # + # @param depth [Integer] + # + # @return [String] + def inspect(depth: 0) + items = Amocrm::Internal::Type::Converter.inspect(item_type, depth: depth.succ) + + "#{self.class}[#{[items, nilable? ? 'nil' : nil].compact.join(' | ')}]" + end + end + end + end +end diff --git a/lib/amocrm/internal/type/base_model.rb b/lib/amocrm/internal/type/base_model.rb new file mode 100644 index 0000000..243f198 --- /dev/null +++ b/lib/amocrm/internal/type/base_model.rb @@ -0,0 +1,523 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @abstract + class BaseModel + extend Amocrm::Internal::Type::Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + class << self + # @api private + # + # Assumes superclass fields are totally defined before fields are accessed / + # defined on subclasses. + # + # @param child [Class] + def inherited(child) + super + child.known_fields.replace(known_fields.dup) + end + + # @api private + # + # @return [Hash{Symbol=>Hash{Symbol=>Object}}] + def known_fields = @known_fields ||= {} + + # @api private + # + # @return [Hash{Symbol=>Hash{Symbol=>Object}}] + def fields + known_fields.transform_values do |field| + {**field.except(:type_fn), type: field.fetch(:type_fn).call} + end + end + + # @api private + # + # @param name_sym [Symbol] + # + # @param required [Boolean] + # + # @param type_info [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] + # + # @param spec [Hash{Symbol=>Object}] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + private def add_field(name_sym, required:, type_info:, spec:) + meta = Amocrm::Internal::Type::Converter.meta_info(type_info, spec) + type_fn, info = + case type_info + in Proc | Amocrm::Internal::Type::Converter | Class + [Amocrm::Internal::Type::Converter.type_info({**spec, union: type_info}), spec] + in Hash + [Amocrm::Internal::Type::Converter.type_info(type_info), type_info] + end + + setter = :"#{name_sym}=" + api_name = info.fetch(:api_name, name_sym) + nilable = info.fetch(:nil?, false) + const = required && !nilable ? info.fetch(:const, Amocrm::Internal::OMIT) : Amocrm::Internal::OMIT + + [name_sym, setter].each { undef_method(_1) } if known_fields.key?(name_sym) + + known_fields[name_sym] = + { + mode: @mode, + api_name: api_name, + required: required, + nilable: nilable, + const: const, + type_fn: type_fn, + meta: meta + } + + define_method(setter) do |value| + target = type_fn.call + state = Amocrm::Internal::Type::Converter.new_coerce_state(translate_names: false) + coerced = Amocrm::Internal::Type::Converter.coerce(target, value, state: state) + status = @coerced.store(name_sym, state.fetch(:error) || true) + stored = + case [target, status] + in [Amocrm::Internal::Type::Converter | Symbol, true] + coerced + else + value + end + @data.store(name_sym, stored) + end + + # rubocop:disable Style/CaseEquality + # rubocop:disable Metrics/BlockLength + define_method(name_sym) do + target = type_fn.call + + case @coerced[name_sym] + in true | false if Amocrm::Internal::Type::Converter === target + @data.fetch(name_sym) + in ::StandardError => e + raise Amocrm::Errors::ConversionError.new( + on: self.class, + method: __method__, + target: target, + value: @data.fetch(name_sym), + cause: e + ) + else + Kernel.then do + value = @data.fetch(name_sym) { const == Amocrm::Internal::OMIT ? nil : const } + state = Amocrm::Internal::Type::Converter.new_coerce_state(translate_names: false) + if (nilable || !required) && value.nil? + nil + else + Amocrm::Internal::Type::Converter.coerce( + target, value, state: state + ) + end + rescue StandardError => e + raise Amocrm::Errors::ConversionError.new( + on: self.class, + method: __method__, + target: target, + value: value, + cause: e + ) + end + end + end + # rubocop:enable Metrics/BlockLength + # rubocop:enable Style/CaseEquality + end + + # @api private + # + # @param name_sym [Symbol] + # + # @param type_info [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] + # + # @param spec [Hash{Symbol=>Object}] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + def required(name_sym, type_info, spec = {}) + add_field(name_sym, required: true, type_info: type_info, spec: spec) + end + + # @api private + # + # @param name_sym [Symbol] + # + # @param type_info [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] + # + # @param spec [Hash{Symbol=>Object}] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + def optional(name_sym, type_info, spec = {}) + add_field(name_sym, required: false, type_info: type_info, spec: spec) + end + + # @api private + # + # `request_only` attributes not excluded from `.#coerce` when receiving responses + # even if well behaved servers should not send them + # + # @param blk [Proc] + private def request_only(&blk) + @mode = :dump + blk.call + ensure + @mode = nil + end + + # @api private + # + # `response_only` attributes are omitted from `.#dump` when making requests + # + # @param blk [Proc] + private def response_only(&blk) + @mode = :coerce + blk.call + ensure + @mode = nil + end + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ==(other) + other.is_a?(Class) && other <= Amocrm::Internal::Type::BaseModel && other.fields == fields + end + + # @api public + # + # @return [Integer] + def hash = fields.hash + end + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ==(other) = self.class == other.class && @data == other.to_h + + # @api public + # + # @return [Integer] + def hash = [self.class, @data].hash + + class << self + # @api private + # + # @param value [Amocrm::Internal::Type::BaseModel, Hash{Object=>Object}, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [self, Object] + def coerce(value, state:) + exactness = state.fetch(:exactness) + + if value.is_a?(self) + exactness[:yes] += 1 + return value + end + + unless (val = Amocrm::Internal::Util.coerce_hash(value)).is_a?(Hash) + exactness[:no] += 1 + state[:error] = TypeError.new("#{value.class} can't be coerced into #{Hash}") + return value + end + exactness[:yes] += 1 + + keys = val.keys.to_set + instance = new + data = instance.to_h + status = instance.instance_variable_get(:@coerced) + + # rubocop:disable Metrics/BlockLength + fields.each do |name, field| + mode, required, target = field.fetch_values(:mode, :required, :type) + api_name, nilable, const = field.fetch_values(:api_name, :nilable, :const) + src_name = state.fetch(:translate_names) ? api_name : name + + unless val.key?(src_name) + if required && mode != :dump && const == Amocrm::Internal::OMIT + exactness[nilable ? :maybe : :no] += 1 + else + exactness[:yes] += 1 + end + next + end + + item = val.fetch(src_name) + keys.delete(src_name) + + state[:error] = nil + converted = + if item.nil? && (nilable || !required) + exactness[nilable ? :yes : :maybe] += 1 + nil + else + coerced = Amocrm::Internal::Type::Converter.coerce(target, item, state: state) + case target + in Amocrm::Internal::Type::Converter | Symbol + coerced + else + item + end + end + + status.store(name, state.fetch(:error) || true) + data.store(name, converted) + end + # rubocop:enable Metrics/BlockLength + + keys.each { data.store(_1, val.fetch(_1)) } + instance + end + + # @api private + # + # @param value [self, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Hash{Object=>Object}, Object] + def dump(value, state:) + unless (coerced = Amocrm::Internal::Util.coerce_hash(value)).is_a?(Hash) + return super + end + + acc = {} + + coerced.each do |key, val| + name = key.is_a?(String) ? key.to_sym : key + case (field = known_fields[name]) + in nil + acc.store(name, super(val, state: state)) + else + api_name, mode, type_fn = field.fetch_values(:api_name, :mode, :type_fn) + case mode + in :coerce + next + else + target = type_fn.call + acc.store(api_name, Amocrm::Internal::Type::Converter.dump(target, val, state: state)) + end + end + end + + known_fields.each_value do |field| + api_name, mode, const = field.fetch_values(:api_name, :mode, :const) + next if mode == :coerce || acc.key?(api_name) || const == Amocrm::Internal::OMIT + acc.store(api_name, const) + end + + acc + end + + # @api private + # + # @return [Object] + def to_sorbet_type + self + end + end + + class << self + # @api private + # + # @param model [Amocrm::Internal::Type::BaseModel] + # @param convert [Boolean] + # + # @return [Hash{Symbol=>Object}] + def recursively_to_h(model, convert:) + rec = ->(x) do + case x + in Amocrm::Internal::Type::BaseModel + if convert + fields = x.class.known_fields + x.to_h.to_h do |key, val| + [key, rec.call(fields.key?(key) ? x.public_send(key) : val)] + rescue Amocrm::Errors::ConversionError + [key, rec.call(val)] + end + else + rec.call(x.to_h) + end + in Hash + x.transform_values(&rec) + in Array + x.map(&rec) + else + x + end + end + rec.call(model) + end + end + + # @api public + # + # Returns the raw value associated with the given key, if found. Otherwise, nil is + # returned. + # + # It is valid to lookup keys that are not in the API spec, for example to access + # undocumented features. This method does not parse response data into + # higher-level types. Lookup by anything other than a Symbol is an ArgumentError. + # + # @param key [Symbol] + # + # @return [Object, nil] + def [](key) + unless key.instance_of?(Symbol) + raise ArgumentError.new("Expected symbol key for lookup, got #{key.inspect}") + end + + @data[key] + end + + # @api public + # + # Returns a Hash of the data underlying this object. O(1) + # + # Keys are Symbols and values are the raw values from the response. The return + # value indicates which values were ever set on the object. i.e. there will be a + # key in this hash if they ever were, even if the set value was nil. + # + # This method is not recursive. The returned value is shared by the object, so it + # should not be mutated. + # + # @return [Hash{Symbol=>Object}] + def to_h = @data + + alias_method :to_hash, :to_h + + # @api public + # + # In addition to the behaviour of `#to_h`, this method will recursively call + # `#to_h` on nested models. + # + # @return [Hash{Symbol=>Object}] + def deep_to_h = self.class.recursively_to_h(@data, convert: false) + + # @param keys [Array, nil] + # + # @return [Hash{Symbol=>Object}] + def deconstruct_keys(keys) + (keys || self.class.known_fields.keys) + .filter_map do |k| + unless self.class.known_fields.key?(k) + next + end + + [k, public_send(k)] + end + .to_h + end + + # @api public + # + # @param a [Object] + # + # @return [String] + def to_json(*a) = Amocrm::Internal::Type::Converter.dump(self.class, self).to_json(*a) + + # @api public + # + # @param a [Object] + # + # @return [String] + def to_yaml(*a) = Amocrm::Internal::Type::Converter.dump(self.class, self).to_yaml(*a) + + # Create a new instance of a model. + # + # @param data [Hash{Symbol=>Object}, self] + def initialize(data = {}) + @data = {} + @coerced = {} + Amocrm::Internal::Util.coerce_hash!(data).each do + if self.class.known_fields.key?(_1) + public_send(:"#{_1}=", _2) + else + @data.store(_1, _2) + @coerced.store(_1, false) + end + end + end + + 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 = [ + Amocrm::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 + end + + # @api public + # + # @return [String] + def to_s = deep_to_h.to_s + + # @api private + # + # @return [String] + def inspect + converted = self.class.recursively_to_h(self, convert: true) + "#<#{self.class}:0x#{object_id.to_s(16)} #{converted}>" + end + + define_sorbet_constant!(:KnownField) do + T.type_alias { {mode: T.nilable(Symbol), required: T::Boolean, nilable: T::Boolean} } + end + end + end + end +end diff --git a/lib/amocrm/internal/type/base_page.rb b/lib/amocrm/internal/type/base_page.rb new file mode 100644 index 0000000..a4750a5 --- /dev/null +++ b/lib/amocrm/internal/type/base_page.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + # + # @generic Elem + # + # This module provides a base implementation for paginated responses in the SDK. + module BasePage + # rubocop:disable Lint/UnusedMethodArgument + + # @api public + # + # @return [Boolean] + def next_page? = (raise NotImplementedError) + + # @api public + # + # @raise [Amocrm::Errors::APIError] + # @return [self] + def next_page = (raise NotImplementedError) + + # @api public + # + # @param blk [Proc] + # + # @yieldparam [generic] + # @return [void] + def auto_paging_each(&blk) = (raise NotImplementedError) + + # @return [Enumerable>] + def to_enum = super(:auto_paging_each) + + alias_method :enum_for, :to_enum + + # @api private + # + # @param client [Amocrm::Internal::Transport::BaseClient] + # @param req [Hash{Symbol=>Object}] + # @param headers [Hash{String=>String}] + # @param page_data [Object] + def initialize(client:, req:, headers:, page_data:) + @client = client + @req = req + @model = req.fetch(:model) + super() + end + + # rubocop:enable Lint/UnusedMethodArgument + end + end + end +end diff --git a/lib/amocrm/internal/type/boolean.rb b/lib/amocrm/internal/type/boolean.rb new file mode 100644 index 0000000..a4d5767 --- /dev/null +++ b/lib/amocrm/internal/type/boolean.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + # + # @abstract + # + # Ruby has no Boolean class; this is something for models to refer to. + class Boolean + extend Amocrm::Internal::Type::Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + private_class_method :new + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def self.===(other) = other == true || other == false + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def self.==(other) = other.is_a?(Class) && other <= Amocrm::Internal::Type::Boolean + + class << self + # @api private + # + # Coerce value to Boolean if possible, otherwise return the original value. + # + # @param value [Boolean, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [Boolean, Object] + def coerce(value, state:) + state.fetch(:exactness)[value == true || value == false ? :yes : :no] += 1 + value + end + + # @!method dump(value, state:) + # @api private + # + # @param value [Boolean, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Boolean, Object] + + # @api private + # + # @return [Object] + def to_sorbet_type + T::Boolean + end + end + end + end + end +end diff --git a/lib/amocrm/internal/type/converter.rb b/lib/amocrm/internal/type/converter.rb new file mode 100644 index 0000000..39aa817 --- /dev/null +++ b/lib/amocrm/internal/type/converter.rb @@ -0,0 +1,327 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + module Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + # rubocop:disable Lint/UnusedMethodArgument + + # @api private + # + # @param value [Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [Object] + def coerce(value, state:) = (raise NotImplementedError) + + # @api private + # + # @param value [Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Object] + def dump(value, state:) + case value + in Array + value.map { Amocrm::Internal::Type::Unknown.dump(_1, state: state) } + in Hash + value.transform_values { Amocrm::Internal::Type::Unknown.dump(_1, state: state) } + in Amocrm::Internal::Type::BaseModel + value.class.dump(value, state: state) + in StringIO + value.string + in Pathname | IO + state[:can_retry] = false if value.is_a?(IO) + Amocrm::FilePart.new(value) + in Amocrm::FilePart + state[:can_retry] = false if value.content.is_a?(IO) + value + else + value + end + end + + # @api private + # + # @param depth [Integer] + # + # @return [String] + def inspect(depth: 0) + super() + end + + # rubocop:enable Lint/UnusedMethodArgument + + class << self + # @api private + # + # @param spec [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + # + # @return [Proc] + def type_info(spec) + case spec + in Proc + spec + in Hash + type_info(spec.slice(:const, :enum, :union).first&.last) + in true | false + -> { Amocrm::Internal::Type::Boolean } + in Amocrm::Internal::Type::Converter | Class | Symbol + -> { spec } + in NilClass | Integer | Float + -> { spec.class } + end + end + + # @api private + # + # @param type_info [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] . + # + # @option type_info [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option type_info [Proc] :enum + # + # @option type_info [Proc] :union + # + # @option type_info [Boolean] :"nil?" + # + # @param spec [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + # + # @return [Hash{Symbol=>Object}] + def meta_info(type_info, spec) + [spec, type_info].grep(Hash).first.to_h.except(:const, :enum, :union, :nil?) + end + + # @api private + # + # @param translate_names [Boolean] + # + # @return [Hash{Symbol=>Object}] + def new_coerce_state(translate_names: true) + { + translate_names: translate_names, + strictness: true, + exactness: {yes: 0, no: 0, maybe: 0}, + error: nil, + branched: 0 + } + end + + # @api private + # + # Based on `target`, transform `value` into `target`, to the extent possible: + # + # 1. if the given `value` conforms to `target` already, return the given `value` + # 2. if it's possible and safe to convert the given `value` to `target`, then the + # converted value + # 3. otherwise, the given `value` unaltered + # + # The coercion process is subject to improvement between minor release versions. + # See https://docs.pydantic.dev/latest/concepts/unions/#smart-mode + # + # @param target [Amocrm::Internal::Type::Converter, Class] + # + # @param value [Object] + # + # @param state [Hash{Symbol=>Object}] The `strictness` is one of `true`, `false`. This informs the coercion strategy + # when we have to decide between multiple possible conversion targets: + # + # - `true`: the conversion must be exact, with minimum coercion. + # - `false`: the conversion can be approximate, with some coercion. + # + # The `exactness` is `Hash` with keys being one of `yes`, `no`, or `maybe`. For + # any given conversion attempt, the exactness will be updated based on how closely + # the value recursively matches the target type: + # + # - `yes`: the value can be converted to the target type with minimum coercion. + # - `maybe`: the value can be converted to the target type with some reasonable + # coercion. + # - `no`: the value cannot be converted to the target type. + # + # See implementation below for more details. + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [Object] + def coerce(target, value, state: Amocrm::Internal::Type::Converter.new_coerce_state) + # rubocop:disable Metrics/BlockNesting + exactness = state.fetch(:exactness) + + case target + in Amocrm::Internal::Type::Converter + return target.coerce(value, state: state) + in Class + if value.is_a?(target) + exactness[:yes] += 1 + return value + end + + case target + in -> { _1 <= NilClass } + exactness[value.nil? ? :yes : :maybe] += 1 + return nil + in -> { _1 <= Integer } + case value + in Integer + exactness[:yes] += 1 + return value + else + Kernel.then do + return Integer(value).tap { exactness[:maybe] += 1 } + rescue ArgumentError, TypeError => e + state[:error] = e + end + end + in -> { _1 <= Float } + if value.is_a?(Numeric) + exactness[:yes] += 1 + return Float(value) + else + Kernel.then do + return Float(value).tap { exactness[:maybe] += 1 } + rescue ArgumentError, TypeError => e + state[:error] = e + end + end + in -> { _1 <= String } + case value + in String | Symbol | Numeric + exactness[value.is_a?(Numeric) ? :maybe : :yes] += 1 + return value.to_s + in StringIO + exactness[:yes] += 1 + return value.string + else + state[:error] = TypeError.new("#{value.class} can't be coerced into #{String}") + end + in -> { _1 <= Date || _1 <= Time } + Kernel.then do + return target.parse(value).tap { exactness[:yes] += 1 } + rescue ArgumentError, TypeError => e + state[:error] = e + end + in -> { _1 <= StringIO } if value.is_a?(String) + exactness[:yes] += 1 + return StringIO.new(value.b) + else + end + in Symbol + case value + in Symbol | String + if value.to_sym == target + exactness[:yes] += 1 + return target + else + exactness[:maybe] += 1 + return value + end + else + message = "cannot convert non-matching #{value.class} into #{target.inspect}" + state[:error] = ArgumentError.new(message) + end + else + end + + exactness[:no] += 1 + value + # rubocop:enable Metrics/BlockNesting + end + + # @api private + # + # @param target [Amocrm::Internal::Type::Converter, Class] + # + # @param value [Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Object] + def dump(target, value, state: {can_retry: true}) + case target + in Amocrm::Internal::Type::Converter + target.dump(value, state: state) + else + Amocrm::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 Amocrm::Internal::Type::Converter + target.inspect(depth: depth.succ) + else + target.inspect + end + end + end + + define_sorbet_constant!(:Input) do + T.type_alias { T.any(Amocrm::Internal::Type::Converter, T::Class[T.anything]) } + end + define_sorbet_constant!(:CoerceState) do + T.type_alias do + { + translate_names: T::Boolean, + strictness: T::Boolean, + exactness: {yes: Integer, no: Integer, maybe: Integer}, + error: T::Class[StandardError], + branched: Integer + } + end + end + define_sorbet_constant!(:DumpState) do + T.type_alias { {can_retry: T::Boolean} } + end + end + end + end +end diff --git a/lib/amocrm/internal/type/enum.rb b/lib/amocrm/internal/type/enum.rb new file mode 100644 index 0000000..175e27f --- /dev/null +++ b/lib/amocrm/internal/type/enum.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + # + # A value from among a specified list of options. OpenAPI enum values map to Ruby + # values in the SDK as follows: + # + # 1. boolean => true | false + # 2. integer => Integer + # 3. float => Float + # 4. string => Symbol + # + # We can therefore convert string values to Symbols, but can't convert other + # values safely. + module Enum + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + # All of the valid Symbol values for this enum. + # + # @return [Array] + def values = constants.map { const_get(_1) } + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ===(other) = values.include?(other) + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ==(other) + # rubocop:disable Style/CaseEquality + Amocrm::Internal::Type::Enum === other && other.values.to_set == values.to_set + # rubocop:enable Style/CaseEquality + end + + # @api public + # + # @return [Integer] + def hash = values.to_set.hash + + # @api private + # + # Unlike with primitives, `Enum` additionally validates that the value is a member + # of the enum. + # + # @param value [String, Symbol, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [Symbol, Object] + def coerce(value, state:) + exactness = state.fetch(:exactness) + val = value.is_a?(String) ? value.to_sym : value + + if values.include?(val) + exactness[:yes] += 1 + val + elsif values.first&.class == val.class + exactness[:maybe] += 1 + value + else + exactness[:no] += 1 + state[:error] = TypeError.new("#{value.class} can't be coerced into #{self}") + value + end + end + + # @!method dump(value, state:) + # @api private + # + # @param value [Symbol, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Symbol, Object] + + # @api private + # + # @return [Object] + def to_sorbet_type + types = values.map { Amocrm::Internal::Util::SorbetRuntimeSupport.to_sorbet_type(_1) }.uniq + case types + in [] + T.noreturn + in [type] + type + else + T.any(*types) + end + end + + # @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 { Amocrm::Internal::Type::Converter.inspect(_1, depth: depth.succ) } + prefix = is_a?(Module) ? name : self.class.name + + "#{prefix}[#{members.join(' | ')}]" + end + end + end + end +end diff --git a/lib/amocrm/internal/type/file_input.rb b/lib/amocrm/internal/type/file_input.rb new file mode 100644 index 0000000..32a21bc --- /dev/null +++ b/lib/amocrm/internal/type/file_input.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + # + # @abstract + # + # Either `Pathname` or `StringIO`, or `IO`, or + # `Amocrm::Internal::Type::FileInput`. + # + # Note: when `IO` is used, all retries are disabled, since many IO` streams are + # not rewindable. + class FileInput + extend Amocrm::Internal::Type::Converter + + private_class_method :new + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def self.===(other) + case other + in Pathname | StringIO | IO | String | Amocrm::FilePart + true + else + false + end + end + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def self.==(other) = other.is_a?(Class) && other <= Amocrm::Internal::Type::FileInput + + class << self + # @api private + # + # @param value [StringIO, String, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [StringIO, Object] + def coerce(value, state:) + exactness = state.fetch(:exactness) + case value + in String + exactness[:yes] += 1 + StringIO.new(value) + in StringIO + exactness[:yes] += 1 + value + else + state[:error] = TypeError.new("#{value.class} can't be coerced into #{StringIO}") + exactness[:no] += 1 + value + end + end + + # @api private + # + # @param value [Pathname, StringIO, IO, String, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Pathname, StringIO, IO, String, Object] + def dump(value, state:) + case value + in StringIO | String + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 + # while not required, a filename is recommended, and in practice many servers do expect this + Amocrm::FilePart.new(value, filename: "upload") + in IO + state[:can_retry] = false + value.to_path.nil? ? Amocrm::FilePart.new(value, filename: "upload") : value + in Amocrm::FilePart if value.content.is_a?(IO) + state[:can_retry] = false + value + else + value + end + end + + # @api private + # + # @return [Object] + def to_sorbet_type + T.any(Pathname, StringIO, IO, String, Amocrm::FilePart) + end + end + end + end + end +end diff --git a/lib/amocrm/internal/type/hash_of.rb b/lib/amocrm/internal/type/hash_of.rb new file mode 100644 index 0000000..9771c82 --- /dev/null +++ b/lib/amocrm/internal/type/hash_of.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + # + # @abstract + # + # @generic Elem + # + # Hash of items of a given type. + class HashOf + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + private_class_method :new + + # @overload [](type_info, spec = {}) + # + # @param type_info [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] + # + # @param spec [Hash{Symbol=>Object}] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + # + # @return [self] + def self.[](...) = new(...) + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ===(other) + type = item_type + case other + in Hash + other.all? do |key, val| + case [key, val] + in [Symbol | String, ^type] + true + else + false + end + end + else + false + end + end + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ==(other) + # rubocop:disable Layout/LineLength + other.is_a?(Amocrm::Internal::Type::HashOf) && other.nilable? == nilable? && other.item_type == item_type + # rubocop:enable Layout/LineLength + end + + # @api public + # + # @return [Integer] + def hash = [self.class, item_type].hash + + # @api private + # + # @param value [Hash{Object=>Object}, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [Hash{Symbol=>Object}, Object] + def coerce(value, state:) + exactness = state.fetch(:exactness) + + unless value.is_a?(Hash) + exactness[:no] += 1 + state[:error] = TypeError.new("#{value.class} can't be coerced into #{Hash}") + return value + end + + target = item_type + exactness[:yes] += 1 + value + .to_h do |key, val| + k = key.is_a?(String) ? key.to_sym : key + v = + case [nilable?, val] + in [true, nil] + exactness[:yes] += 1 + nil + else + Amocrm::Internal::Type::Converter.coerce(target, val, state: state) + end + + exactness[:no] += 1 unless k.is_a?(Symbol) + [k, v] + end + end + + # @api private + # + # @param value [Hash{Object=>Object}, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Hash{Symbol=>Object}, Object] + def dump(value, state:) + target = item_type + if value.is_a?(Hash) + value.transform_values do + Amocrm::Internal::Type::Converter.dump(target, _1, state: state) + end + else + super + end + end + + # @api private + # + # @return [Object] + def to_sorbet_type + T::Hash[Amocrm::Internal::Util::SorbetRuntimeSupport.to_sorbet_type(item_type)] + end + + # @api private + # + # @return [generic] + protected def item_type = @item_type_fn.call + + # @api private + # + # @return [Boolean] + protected def nilable? = @nilable + + # @api private + # + # @param type_info [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] + # + # @param spec [Hash{Symbol=>Object}] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + def initialize(type_info, spec = {}) + @item_type_fn = Amocrm::Internal::Type::Converter.type_info(type_info || spec) + @meta = Amocrm::Internal::Type::Converter.meta_info(type_info, spec) + @nilable = spec.fetch(:nil?, false) + end + + # @api private + # + # @param depth [Integer] + # + # @return [String] + def inspect(depth: 0) + items = Amocrm::Internal::Type::Converter.inspect(item_type, depth: depth.succ) + + "#{self.class}[#{[items, nilable? ? 'nil' : nil].compact.join(' | ')}]" + end + end + end + end +end diff --git a/lib/amocrm/internal/type/request_parameters.rb b/lib/amocrm/internal/type/request_parameters.rb new file mode 100644 index 0000000..728b92d --- /dev/null +++ b/lib/amocrm/internal/type/request_parameters.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + module RequestParameters + # @!attribute request_options + # Options to specify HTTP behaviour for this request. + # + # @return [Amocrm::RequestOptions, Hash{Symbol=>Object}] + + # @param mod [Module] + def self.included(mod) + raise ArgumentError.new(mod) unless mod <= Amocrm::Internal::Type::BaseModel + + mod.optional(:request_options, Amocrm::RequestOptions) + end + + # @api private + module Converter + # @api private + # + # @param params [Object] + # + # @return [Array(Object, Hash{Symbol=>Object})] + def dump_request(params) + state = {can_retry: true} + case (dumped = dump(params, state: state)) + in Hash + options = Amocrm::Internal::Util.coerce_hash!(dumped[:request_options]).to_h + request_options = state.fetch(:can_retry) ? options : {**options, max_retries: 0} + [dumped.except(:request_options), request_options] + else + [dumped, nil] + end + end + end + end + end + end +end diff --git a/lib/amocrm/internal/type/union.rb b/lib/amocrm/internal/type/union.rb new file mode 100644 index 0000000..fd82c83 --- /dev/null +++ b/lib/amocrm/internal/type/union.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + # + # @example + # # `unsorted_accept_response` is a `Amocrm::Models::V4::Leads::UnsortedAcceptResponse` + # case unsorted_accept_response + # when Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse + # puts(unsorted_accept_response._embedded) + # when Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Problem + # puts(unsorted_accept_response.detail) + # else + # puts(unsorted_accept_response) + # end + module Union + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + # @api private + # + # All of the specified variant info for this union. + # + # @return [ArrayObject})>] + private def known_variants = (@known_variants ||= []) + + # @api private + # + # @return [ArrayObject})>] + protected def derefed_variants + known_variants.map { |key, variant_fn, meta| [key, variant_fn.call, meta] } + end + + # All of the specified variants for this union. + # + # @return [Array] + def variants = derefed_variants.map { _2 } + + # @api private + # + # @param property [Symbol] + private def discriminator(property) + case property + in Symbol + @discriminator = property + end + end + + # @api private + # + # @param key [Symbol, Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] + # + # @param spec [Hash{Symbol=>Object}, Proc, Amocrm::Internal::Type::Converter, Class] . + # + # @option spec [NilClass, TrueClass, FalseClass, Integer, Float, Symbol] :const + # + # @option spec [Proc] :enum + # + # @option spec [Proc] :union + # + # @option spec [Boolean] :"nil?" + private def variant(key, spec = nil) + meta = Amocrm::Internal::Type::Converter.meta_info(nil, spec) + variant_info = + case key + in Symbol + [key, Amocrm::Internal::Type::Converter.type_info(spec), meta] + in Proc | Amocrm::Internal::Type::Converter | Class | Hash + [nil, Amocrm::Internal::Type::Converter.type_info(key), meta] + end + + known_variants << variant_info + end + + # @api private + # + # @param value [Object] + # + # @return [Amocrm::Internal::Type::Converter, Class, nil] + private def resolve_variant(value) + case [@discriminator, value] + in [_, Amocrm::Internal::Type::BaseModel] + value.class + in [Symbol, Hash] + key = value.fetch(@discriminator) do + value.fetch(@discriminator.to_s, Amocrm::Internal::OMIT) + end + + return nil if key == Amocrm::Internal::OMIT + + key = key.to_sym if key.is_a?(String) + _, found = known_variants.find { |k,| k == key } + found&.call + else + nil + end + end + + # rubocop:disable Style/HashEachMethods + # rubocop:disable Style/CaseEquality + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ===(other) + known_variants.any? do |_, variant_fn| + variant_fn.call === other + end + end + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def ==(other) + Amocrm::Internal::Type::Union === other && other.derefed_variants == derefed_variants + end + + # @api public + # + # @return [Integer] + def hash = variants.hash + + # @api private + # + # Tries to efficiently coerce the given value to one of the known variants. + # + # If the value cannot match any of the known variants, the coercion is considered + # non-viable and returns the original value. + # + # @param value [Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [Object] + def coerce(value, state:) + if (target = resolve_variant(value)) + return Amocrm::Internal::Type::Converter.coerce(target, value, state: state) + end + + strictness = state.fetch(:strictness) + exactness = state.fetch(:exactness) + + alternatives = [] + known_variants.each do |_, variant_fn| + target = variant_fn.call + exact = state[:exactness] = {yes: 0, no: 0, maybe: 0} + state[:branched] += 1 + + coerced = Amocrm::Internal::Type::Converter.coerce(target, value, state: state) + yes, no, maybe = exact.values + if (no + maybe).zero? || (!strictness && yes.positive?) + exact.each { exactness[_1] += _2 } + state[:exactness] = exactness + return coerced + elsif maybe.positive? + alternatives << [[-yes, -maybe, no], exact, coerced] + end + end + + case alternatives.sort_by!(&:first) + in [] + exactness[:no] += 1 + state[:error] = ArgumentError.new("no matching variant for #{value.inspect}") + value + in [[_, exact, coerced], *] + exact.each { exactness[_1] += _2 } + coerced + end + .tap { state[:exactness] = exactness } + ensure + state[:strictness] = strictness + end + + # @api private + # + # @param value [Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Object] + def dump(value, state:) + if (target = resolve_variant(value)) + return Amocrm::Internal::Type::Converter.dump(target, value, state: state) + end + + known_variants.each do + target = _2.call + return Amocrm::Internal::Type::Converter.dump(target, value, state: state) if target === value + end + + super + end + + # @api private + # + # @return [Object] + def to_sorbet_type + types = variants.map { Amocrm::Internal::Util::SorbetRuntimeSupport.to_sorbet_type(_1) }.uniq + case types + in [] + T.noreturn + in [type] + type + else + T.any(*types) + end + end + + # 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 { Amocrm::Internal::Type::Converter.inspect(_1, depth: depth.succ) } + prefix = is_a?(Module) ? name : self.class.name + + "#{prefix}[#{members.join(' | ')}]" + end + end + end + end +end diff --git a/lib/amocrm/internal/type/unknown.rb b/lib/amocrm/internal/type/unknown.rb new file mode 100644 index 0000000..ddd70ce --- /dev/null +++ b/lib/amocrm/internal/type/unknown.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + module Type + # @api private + # + # @abstract + # + # When we don't know what to expect for the value. + class Unknown + extend Amocrm::Internal::Type::Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + # rubocop:disable Lint/UnusedMethodArgument + + private_class_method :new + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def self.===(other) = true + + # @api public + # + # @param other [Object] + # + # @return [Boolean] + def self.==(other) = other.is_a?(Class) && other <= Amocrm::Internal::Type::Unknown + + class << self + # @api private + # + # No coercion needed for Unknown type. + # + # @param value [Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Class] :error + # + # @option state [Integer] :branched + # + # @return [Object] + def coerce(value, state:) + state.fetch(:exactness)[:yes] += 1 + value + end + + # @!method dump(value, state:) + # @api private + # + # @param value [Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # + # @return [Object] + + # @api private + # + # @return [Object] + def to_sorbet_type + T.anything + end + end + + # rubocop:enable Lint/UnusedMethodArgument + end + end + end +end diff --git a/lib/amocrm/internal/util.rb b/lib/amocrm/internal/util.rb new file mode 100644 index 0000000..3756406 --- /dev/null +++ b/lib/amocrm/internal/util.rb @@ -0,0 +1,920 @@ +# frozen_string_literal: true + +module Amocrm + module Internal + # @api private + module Util + # @api private + # + # @return [Float] + def self.monotonic_secs = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + # @api private + # + # @param ns [Module, Class] + # + # @return [Enumerable] + def self.walk_namespaces(ns) + ns.constants(false).lazy.flat_map do + case (c = ns.const_get(_1, false)) + in Module | Class + walk_namespaces(c) + else + [] + end + end + .chain([ns]) + end + + class << self + # @api private + # + # @return [String] + def arch + case (arch = RbConfig::CONFIG["arch"])&.downcase + in nil + "unknown" + in /aarch64|arm64/ + "arm64" + in /x86_64/ + "x64" + in /arm/ + "arm" + else + "other:#{arch}" + end + end + + # @api private + # + # @return [String] + def os + case (host = RbConfig::CONFIG["host_os"])&.downcase + in nil + "Unknown" + in /linux/ + "Linux" + in /darwin/ + "MacOS" + in /freebsd/ + "FreeBSD" + in /openbsd/ + "OpenBSD" + in /mswin|mingw|cygwin|ucrt/ + "Windows" + else + "Other:#{host}" + end + end + end + + class << self + # @api private + # + # @param input [Object] + # + # @return [Boolean] + def primitive?(input) + case input + in true | false | Numeric | Symbol | String + true + else + false + end + end + + # @api private + # + # @param input [String, Boolean] + # + # @return [Boolean, Object] + def coerce_boolean(input) + case input.is_a?(String) ? input.downcase : input + in "true" + true + in "false" + false + else + input + end + end + + # @api private + # + # @param input [String, Boolean] + # + # @raise [ArgumentError] + # @return [Boolean, nil] + def coerce_boolean!(input) + case coerce_boolean(input) + in true | false | nil => coerced + coerced + else + raise ArgumentError.new("Unable to coerce #{input.inspect} into boolean value") + end + end + + # @api private + # + # @param input [String, Integer] + # + # @return [Integer, Object] + def coerce_integer(input) + Integer(input, exception: false) || input + end + + # @api private + # + # @param input [String, Integer, Float] + # + # @return [Float, Object] + def coerce_float(input) + Float(input, exception: false) || input + end + + # @api private + # + # @param input [Object] + # + # @return [Hash{Object=>Object}, Object] + def coerce_hash(input) + case input + in NilClass | Array | Set | Enumerator | StringIO | IO + input + else + input.respond_to?(:to_h) ? input.to_h : input + end + end + + # @api private + # + # @param input [Object] + # + # @raise [ArgumentError] + # @return [Hash{Object=>Object}, nil] + def coerce_hash!(input) + case coerce_hash(input) + in Hash | nil => coerced + coerced + else + message = "Expected a #{Hash} or #{Amocrm::Internal::Type::BaseModel}, got #{data.inspect}" + raise ArgumentError.new(message) + end + end + end + + class << self + # @api private + # + # @param lhs [Object] + # @param rhs [Object] + # @param concat [Boolean] + # + # @return [Object] + private def deep_merge_lr(lhs, rhs, concat: false) + case [lhs, rhs, concat] + in [Hash, Hash, _] + lhs.merge(rhs) { deep_merge_lr(_2, _3, concat: concat) } + in [Array, Array, true] + lhs.concat(rhs) + else + rhs + end + end + + # @api private + # + # Recursively merge one hash with another. If the values at a given key are not + # both hashes, just take the new value. + # + # @param values [Array] + # + # @param sentinel [Object, nil] the value to return if no values are provided. + # + # @param concat [Boolean] whether to merge sequences by concatenation. + # + # @return [Object] + def deep_merge(*values, sentinel: nil, concat: false) + case values + in [value, *values] + values.reduce(value) do |acc, val| + deep_merge_lr(acc, val, concat: concat) + end + else + sentinel + end + end + + # @api private + # + # @param data [Hash{Symbol=>Object}, Array, Object] + # @param pick [Symbol, Integer, Array, Proc, nil] + # @param blk [Proc, nil] + # + # @return [Object, nil] + def dig(data, pick, &blk) + case [data, pick] + in [_, nil] + data + in [Hash, Symbol] | [Array, Integer] + data.fetch(pick) { blk&.call } + in [Hash | Array, Array] + pick.reduce(data) do |acc, key| + case acc + in Hash if acc.key?(key) + acc.fetch(key) + in Array if key.is_a?(Integer) && key < acc.length + acc[key] + else + return blk&.call + end + end + in [_, Proc] + pick.call(data) + else + blk&.call + end + end + end + + class << self + # @api private + # + # @param uri [URI::Generic] + # + # @return [String] + def uri_origin(uri) + "#{uri.scheme}://#{uri.host}#{":#{uri.port}" unless uri.port == uri.default_port}" + end + + # @api private + # + # @param path [String, Array] + # + # @return [String] + def interpolate_path(path) + case path + in String + path + in [] + "" + in [String => p, *interpolations] + encoded = interpolations.map { ERB::Util.url_encode(_1) } + format(p, *encoded) + end + end + end + + class << self + # @api private + # + # @param query [String, nil] + # + # @return [Hash{String=>Array}] + def decode_query(query) + CGI.parse(query.to_s) + end + + # @api private + # + # @param query [Hash{String=>Array, String, nil}, nil] + # + # @return [String, nil] + def encode_query(query) + query.to_h.empty? ? nil : URI.encode_www_form(query) + end + end + + class << self + # @api private + # + # @param url [URI::Generic, String] + # + # @return [Hash{Symbol=>String, Integer, nil}] + def parse_uri(url) + parsed = URI::Generic.component.zip(URI.split(url)).to_h + {**parsed, query: decode_query(parsed.fetch(:query))} + end + + # @api private + # + # @param parsed [Hash{Symbol=>String, Integer, nil}] . + # + # @option parsed [String, nil] :scheme + # + # @option parsed [String, nil] :host + # + # @option parsed [Integer, nil] :port + # + # @option parsed [String, nil] :path + # + # @option parsed [Hash{String=>Array}] :query + # + # @return [URI::Generic] + def unparse_uri(parsed) + URI::Generic.build(**parsed, query: encode_query(parsed.fetch(:query))) + end + + # @api private + # + # @param lhs [Hash{Symbol=>String, Integer, nil}] . + # + # @option lhs [String, nil] :scheme + # + # @option lhs [String, nil] :host + # + # @option lhs [Integer, nil] :port + # + # @option lhs [String, nil] :path + # + # @option lhs [Hash{String=>Array}] :query + # + # @param rhs [Hash{Symbol=>String, Integer, nil}] . + # + # @option rhs [String, nil] :scheme + # + # @option rhs [String, nil] :host + # + # @option rhs [Integer, nil] :port + # + # @option rhs [String, nil] :path + # + # @option rhs [Hash{String=>Array}] :query + # + # @return [URI::Generic] + def join_parsed_uri(lhs, rhs) + base_path, base_query = lhs.fetch_values(:path, :query) + slashed = base_path.end_with?("/") ? base_path : "#{base_path}/" + + merged = {**parse_uri(rhs.fetch(:path)), **rhs.except(:path, :query)} + parsed_path, parsed_query = merged.fetch_values(:path, :query) + override = URI::Generic.build(**merged.slice(:scheme, :host, :port), path: parsed_path) + + joined = URI.join(URI::Generic.build(lhs.except(:path, :query)), slashed, override) + query = deep_merge( + joined.path == base_path ? base_query : {}, + parsed_query, + rhs[:query].to_h, + concat: true + ) + + joined.query = encode_query(query) + joined + end + end + + class << self + # @api private + # + # @param headers [Hash{String=>String, Integer, Array, nil}] + # + # @return [Hash{String=>String}] + def normalized_headers(*headers) + {}.merge(*headers.compact).to_h do |key, val| + value = + case val + in Array + val.filter_map { _1&.to_s&.strip }.join(", ") + else + val&.to_s&.strip + end + [key.downcase, value] + end + end + end + + # @api private + # + # An adapter that satisfies the IO interface required by `::IO.copy_stream` + class ReadIOAdapter + # @api private + # + # @return [Boolean, nil] + def close? = @closing + + # @api private + def close + case @stream + in Enumerator + Amocrm::Internal::Util.close_fused!(@stream) + in IO if close? + @stream.close + else + end + end + + # @api private + # + # @param max_len [Integer, nil] + # + # @return [String] + private def read_enum(max_len) + case max_len + in nil + @stream.to_a.join + in Integer + @buf << @stream.next while @buf.length < max_len + @buf.slice!(..max_len) + end + rescue StopIteration + @stream = nil + @buf.slice!(0..) + end + + # @api private + # + # @param max_len [Integer, nil] + # @param out_string [String, nil] + # + # @return [String, nil] + def read(max_len = nil, out_string = nil) + case @stream + in nil + nil + in IO | StringIO + @stream.read(max_len, out_string) + in Enumerator + read = read_enum(max_len) + case out_string + in String + out_string.replace(read) + in nil + read + end + end + .tap(&@blk) + end + + # @api private + # + # @param src [String, Pathname, StringIO, Enumerable] + # @param blk [Proc] + # + # @yieldparam [String] + def initialize(src, &blk) + @stream = + case src + in String + StringIO.new(src) + in Pathname + @closing = true + src.open(binmode: true) + else + src + end + @buf = String.new + @blk = blk + end + end + + class << self + # @param blk [Proc] + # + # @yieldparam [Enumerator::Yielder] + # @return [Enumerable] + def writable_enum(&blk) + Enumerator.new do |y| + y.define_singleton_method(:write) do + self << _1.dup + _1.bytesize + end + + blk.call(y) + end + end + end + + # @type [Regexp] + JSON_CONTENT = %r{^application/(?:vnd(?:\.[^.]+)*\+)?json(?!l)} + # @type [Regexp] + JSONL_CONTENT = %r{^application/(:?x-(?:n|l)djson)|(:?(?:x-)?jsonl)} + + class << self + # @api private + # + # @param y [Enumerator::Yielder] + # @param val [Object] + # @param closing [Array] + # @param content_type [String, nil] + private def write_multipart_content(y, val:, closing:, content_type: nil) + content_line = "Content-Type: %s\r\n\r\n" + + case val + in Amocrm::FilePart + return write_multipart_content( + y, + val: val.content, + closing: closing, + content_type: val.content_type + ) + in Pathname + y << format(content_line, content_type || "application/octet-stream") + io = val.open(binmode: true) + closing << io.method(:close) + IO.copy_stream(io, y) + in IO + y << format(content_line, content_type || "application/octet-stream") + IO.copy_stream(val, y) + in StringIO + y << format(content_line, content_type || "application/octet-stream") + y << val.string + in -> { primitive?(_1) } + y << format(content_line, content_type || "text/plain") + y << val.to_s + else + y << format(content_line, content_type || "application/json") + y << JSON.generate(val) + end + y << "\r\n" + end + + # @api private + # + # @param y [Enumerator::Yielder] + # @param boundary [String] + # @param key [Symbol, String] + # @param val [Object] + # @param closing [Array] + private def write_multipart_chunk(y, boundary:, key:, val:, closing:) + y << "--#{boundary}\r\n" + y << "Content-Disposition: form-data" + + unless key.nil? + name = ERB::Util.url_encode(key.to_s) + y << "; name=\"#{name}\"" + end + + case val + in Amocrm::FilePart unless val.filename.nil? + filename = ERB::Util.url_encode(val.filename) + y << "; filename=\"#{filename}\"" + in Pathname | IO + filename = ERB::Util.url_encode(::File.basename(val.to_path)) + y << "; filename=\"#{filename}\"" + else + end + y << "\r\n" + + write_multipart_content(y, val: val, closing: closing) + end + + # @api private + # + # https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#special-considerations-for-multipart-content + # + # @param body [Object] + # + # @return [Array(String, Enumerable)] + private def encode_multipart_streaming(body) + # RFC 1521 Section 7.2.1 says we should have 70 char maximum for boundary length + boundary = SecureRandom.urlsafe_base64(46) + + closing = [] + strio = writable_enum do |y| + case body + in Hash + body.each do |key, val| + case val + in Array if val.all? { primitive?(_1) } + val.each do |v| + write_multipart_chunk(y, boundary: boundary, key: key, val: v, closing: closing) + end + else + write_multipart_chunk(y, boundary: boundary, key: key, val: val, closing: closing) + end + end + else + write_multipart_chunk(y, boundary: boundary, key: nil, val: body, closing: closing) + end + y << "--#{boundary}--\r\n" + end + + fused_io = fused_enum(strio) { closing.each(&:call) } + [boundary, fused_io] + end + + # @api private + # + # @param headers [Hash{String=>String}] + # @param body [Object] + # + # @return [Object] + def encode_content(headers, body) + # rubocop:disable Style/CaseEquality + # rubocop:disable Layout/LineLength + content_type = headers["content-type"] + case [content_type, body] + in [Amocrm::Internal::Util::JSON_CONTENT, Hash | Array | -> { primitive?(_1) }] + [headers, JSON.generate(body)] + in [Amocrm::Internal::Util::JSONL_CONTENT, Enumerable] unless Amocrm::Internal::Type::FileInput === body + [headers, body.lazy.map { JSON.generate(_1) }] + in [%r{^multipart/form-data}, Hash | Amocrm::Internal::Type::FileInput] + boundary, strio = encode_multipart_streaming(body) + headers = {**headers, "content-type" => "#{content_type}; boundary=#{boundary}"} + [headers, strio] + in [_, Symbol | Numeric] + [headers, body.to_s] + in [_, StringIO] + [headers, body.string] + in [_, Amocrm::FilePart] + [headers, body.content] + else + [headers, body] + end + # rubocop:enable Layout/LineLength + # rubocop:enable Style/CaseEquality + end + + # @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}] + # @param stream [Enumerable] + # @param suppress_error [Boolean] + # + # @raise [JSON::ParserError] + # @return [Object] + def decode_content(headers, stream:, suppress_error: false) + case (content_type = headers["content-type"]) + in Amocrm::Internal::Util::JSON_CONTENT + return nil if (json = stream.to_a.join).empty? + + begin + JSON.parse(json, symbolize_names: true) + rescue JSON::ParserError => e + raise e unless suppress_error + json + end + in Amocrm::Internal::Util::JSONL_CONTENT + lines = decode_lines(stream) + chain_fused(lines) do |y| + lines.each do + next if _1.empty? + + y << JSON.parse(_1, symbolize_names: true) + end + end + in %r{^text/event-stream} + lines = decode_lines(stream) + decode_sse(lines) + else + text = stream.to_a.join + force_charset!(content_type, text: text) + StringIO.new(text) + end + end + end + + class << self + # @api private + # + # https://doc.rust-lang.org/std/iter/trait.FusedIterator.html + # + # @param enum [Enumerable] + # @param external [Boolean] + # @param close [Proc] + # + # @return [Enumerable] + def fused_enum(enum, external: false, &close) + fused = false + iter = Enumerator.new do |y| + next if fused + + fused = true + if external + loop { y << enum.next } + else + enum.each(&y) + end + ensure + close&.call + close = nil + end + + iter.define_singleton_method(:rewind) do + fused = true + self + end + iter + end + + # @api private + # + # @param enum [Enumerable, nil] + def close_fused!(enum) + return unless enum.is_a?(Enumerator) + + # rubocop:disable Lint/UnreachableLoop + enum.rewind.each { break } + # rubocop:enable Lint/UnreachableLoop + end + + # @api private + # + # @param enum [Enumerable, nil] + # @param blk [Proc] + # + # @yieldparam [Enumerator::Yielder] + # @return [Enumerable] + def chain_fused(enum, &blk) + iter = Enumerator.new { blk.call(_1) } + fused_enum(iter) { close_fused!(enum) } + end + end + + 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 + cr_seen = nil + + chain_fused(enum) do |y| + enum.each do |row| + offset = buffer.bytesize + buffer << row + while (match = re.match(buffer, cr_seen&.to_i || offset)) + case [match.captures.first, cr_seen] + in ["\r", nil] + cr_seen = match.end(1) + next + in ["\r" | "\r\n", Integer] + y << buffer.slice!(..(cr_seen.pred)) + else + y << buffer.slice!(..(match.end(1).pred)) + end + offset = 0 + cr_seen = nil + end + end + + y << buffer.slice!(..(cr_seen.pred)) unless cr_seen.nil? + y << buffer unless buffer.empty? + end + 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`. + # + # @param lines [Enumerable] + # + # @return [EnumerableObject}>] + def decode_sse(lines) + # rubocop:disable Metrics/BlockLength + chain_fused(lines) do |y| + blank = {event: nil, data: nil, id: nil, retry: nil} + current = {} + + lines.each do |line| + case line.sub(/\R$/, "") + in "" + next if current.empty? + y << {**blank, **current} + current = {} + in /^:/ + next + in /^([^:]+):\s?(.*)$/ + field, value = Regexp.last_match.captures + case field + in "event" + current.merge!(event: value) + in "data" + (current[:data] ||= String.new) << (value << "\n") + in "id" unless value.include?("\0") + current.merge!(id: value) + in "retry" if /^\d+$/ =~ value + current.merge!(retry: Integer(value)) + else + end + else + end + end + # rubocop:enable Metrics/BlockLength + + y << {**blank, **current} unless current.empty? + end + end + end + + # @api private + module SorbetRuntimeSupport + class MissingSorbetRuntimeError < ::RuntimeError + end + + # @api private + # + # @return [Hash{Symbol=>Object}] + private def sorbet_runtime_constants = @sorbet_runtime_constants ||= {} + + # @api private + # + # @param name [Symbol] + def const_missing(name) + super unless sorbet_runtime_constants.key?(name) + + unless Object.const_defined?(:T) + message = "Trying to access a Sorbet constant #{name.inspect} without `sorbet-runtime`." + raise MissingSorbetRuntimeError.new(message) + end + + sorbet_runtime_constants.fetch(name).call + end + + # @api private + # + # @param name [Symbol] + # + # @return [Boolean] + def sorbet_constant_defined?(name) = sorbet_runtime_constants.key?(name) + + # @api private + # + # @param name [Symbol] + # @param blk [Proc] + def define_sorbet_constant!(name, &blk) = sorbet_runtime_constants.store(name, blk) + + # @api private + # + # @return [Object] + def to_sorbet_type = raise NotImplementedError + + class << self + # @api private + # + # @param type [Amocrm::Internal::Util::SorbetRuntimeSupport, Object] + # + # @return [Object] + def to_sorbet_type(type) + case type + in Amocrm::Internal::Util::SorbetRuntimeSupport + type.to_sorbet_type + in Class | Module + type + in true | false + T::Boolean + else + type.class + end + end + end + end + + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + define_sorbet_constant!(:ParsedUri) do + T.type_alias do + { + scheme: T.nilable(String), + host: T.nilable(String), + port: T.nilable(Integer), + path: T.nilable(String), + query: T::Hash[String, T::Array[String]] + } + end + end + + define_sorbet_constant!(:ServerSentEvent) do + T.type_alias do + { + event: T.nilable(String), + data: T.nilable(String), + id: T.nilable(String), + retry: T.nilable(Integer) + } + end + end + end + end +end diff --git a/lib/amocrm/models.rb b/lib/amocrm/models.rb new file mode 100644 index 0000000..0905240 --- /dev/null +++ b/lib/amocrm/models.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Amocrm + [Amocrm::Internal::Type::BaseModel, *Amocrm::Internal::Type::BaseModel.subclasses].each do |cls| + cls.define_sorbet_constant!(:OrHash) { T.type_alias { T.any(cls, Amocrm::Internal::AnyHash) } } + end + + Amocrm::Internal::Util.walk_namespaces(Amocrm::Models).each do |mod| + case mod + in Amocrm::Internal::Type::Enum | Amocrm::Internal::Type::Union + mod.constants.each do |name| + case mod.const_get(name) + in true | false + mod.define_sorbet_constant!(:TaggedBoolean) { T.type_alias { T::Boolean } } + mod.define_sorbet_constant!(:OrBoolean) { T.type_alias { T::Boolean } } + in Integer + mod.define_sorbet_constant!(:TaggedInteger) { T.type_alias { Integer } } + mod.define_sorbet_constant!(:OrInteger) { T.type_alias { Integer } } + in Float + mod.define_sorbet_constant!(:TaggedFloat) { T.type_alias { Float } } + mod.define_sorbet_constant!(:OrFloat) { T.type_alias { Float } } + in Symbol + mod.define_sorbet_constant!(:TaggedSymbol) { T.type_alias { Symbol } } + mod.define_sorbet_constant!(:OrSymbol) { T.type_alias { T.any(Symbol, String) } } + else + end + end + else + end + end + + Amocrm::Internal::Util.walk_namespaces(Amocrm::Models) + .lazy + .grep(Amocrm::Internal::Type::Union) + .each do |mod| + const = :Variants + next if mod.sorbet_constant_defined?(const) + + mod.define_sorbet_constant!(const) { T.type_alias { mod.to_sorbet_type } } + end + + V4 = Amocrm::Models::V4 +end diff --git a/lib/amocrm/models/v4/leads/unsorted_accept_params.rb b/lib/amocrm/models/v4/leads/unsorted_accept_params.rb new file mode 100644 index 0000000..a8f788d --- /dev/null +++ b/lib/amocrm/models/v4/leads/unsorted_accept_params.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Amocrm + module Models + module V4 + module Leads + # @see Amocrm::Resources::V4::Leads::Unsorted#accept + class UnsortedAcceptParams < Amocrm::Internal::Type::BaseModel + extend Amocrm::Internal::Type::RequestParameters::Converter + include Amocrm::Internal::Type::RequestParameters + + # @!attribute status_id + # Status id for the created lead + # + # @return [Integer, nil] + optional :status_id, Integer + + # @!attribute user_id + # User id on whose behalf the item is accepted + # + # @return [Integer, nil] + optional :user_id, Integer + + # @!method initialize(status_id: nil, user_id: nil, request_options: {}) + # @param status_id [Integer] Status id for the created lead + # + # @param user_id [Integer] User id on whose behalf the item is accepted + # + # @param request_options [Amocrm::RequestOptions, Hash{Symbol=>Object}] + end + end + end + end +end diff --git a/lib/amocrm/models/v4/leads/unsorted_accept_response.rb b/lib/amocrm/models/v4/leads/unsorted_accept_response.rb new file mode 100644 index 0000000..daf327d --- /dev/null +++ b/lib/amocrm/models/v4/leads/unsorted_accept_response.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +module Amocrm + module Models + module V4 + module Leads + # @see Amocrm::Resources::V4::Leads::Unsorted#accept + module UnsortedAcceptResponse + extend Amocrm::Internal::Type::Union + + variant -> { Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse } + + variant -> { Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Problem } + + class UnsortedAcceptResponse < Amocrm::Internal::Type::BaseModel + # @!attribute _embedded + # + # @return [Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded, nil] + optional :_embedded, + -> { Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded } + + # @!attribute category + # + # @return [Symbol, Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category, nil] + optional :category, + enum: -> { Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category } + + # @!attribute created_at + # + # @return [Integer, nil] + optional :created_at, Integer + + # @!attribute pipeline_id + # + # @return [Integer, nil] + optional :pipeline_id, Integer + + # @!attribute uid + # + # @return [String, nil] + optional :uid, String + + # @!method initialize(_embedded: nil, category: nil, created_at: nil, pipeline_id: nil, uid: nil) + # @param _embedded [Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded] + # @param category [Symbol, Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category] + # @param created_at [Integer] + # @param pipeline_id [Integer] + # @param uid [String] + + # @see Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse#_embedded + class Embedded < Amocrm::Internal::Type::BaseModel + # @!attribute companies + # + # @return [Array, nil] + optional :companies, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company] } + + # @!attribute contacts + # + # @return [Array, nil] + optional :contacts, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact] } + + # @!attribute leads + # + # @return [Array, nil] + optional :leads, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead] } + + # @!method initialize(companies: nil, contacts: nil, leads: nil) + # @param companies [Array] + # @param contacts [Array] + # @param leads [Array] + + class Company < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!method initialize(id: nil, _links: nil) + # @param id [Integer] + # @param _links [Object] + end + + class Contact < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!method initialize(id: nil, _links: nil) + # @param id [Integer] + # @param _links [Object] + end + + class Lead < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!method initialize(id: nil, _links: nil) + # @param id [Integer] + # @param _links [Object] + end + end + + # @see Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse#category + module Category + extend Amocrm::Internal::Type::Enum + + SIP = :sip + MAIL = :mail + CHATS = :chats + FORMS = :forms + + # @!method self.values + # @return [Array] + end + end + + class Problem < Amocrm::Internal::Type::BaseModel + # @!attribute detail + # + # @return [String, nil] + optional :detail, String + + # @!attribute status + # + # @return [Integer, nil] + optional :status, Integer + + # @!attribute title + # + # @return [String, nil] + optional :title, String + + # @!attribute type + # + # @return [String, nil] + optional :type, String + + # @!method initialize(detail: nil, status: nil, title: nil, type: nil) + # @param detail [String] + # @param status [Integer] + # @param title [String] + # @param type [String] + end + + # @!method self.variants + # @return [Array(Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse, Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Problem)] + end + end + end + end +end diff --git a/lib/amocrm/models/v4/leads/unsorted_create_forms_params.rb b/lib/amocrm/models/v4/leads/unsorted_create_forms_params.rb new file mode 100644 index 0000000..702af6b --- /dev/null +++ b/lib/amocrm/models/v4/leads/unsorted_create_forms_params.rb @@ -0,0 +1,452 @@ +# frozen_string_literal: true + +module Amocrm + module Models + module V4 + module Leads + # @see Amocrm::Resources::V4::Leads::Unsorted#create_forms + class UnsortedCreateFormsParams < Amocrm::Internal::Type::BaseModel + extend Amocrm::Internal::Type::RequestParameters::Converter + include Amocrm::Internal::Type::RequestParameters + + # @!attribute body + # + # @return [Array] + required :body, -> { Amocrm::Internal::Type::ArrayOf[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body] } + + # @!method initialize(body:, request_options: {}) + # @param body [Array] + # @param request_options [Amocrm::RequestOptions, Hash{Symbol=>Object}] + + class Body < Amocrm::Internal::Type::BaseModel + # @!attribute metadata + # Form metadata + # + # @return [Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata] + required :metadata, -> { Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata } + + # @!attribute source_name + # Human-readable source name shown in amoCRM (e.g. "Website form", "Landing page") + # + # @return [String] + required :source_name, String + + # @!attribute source_uid + # Your stable id of the source: site/form/widget/integration that sent the lead + # + # @return [String] + required :source_uid, String + + # @!attribute _embedded + # Embedded entities (lead/contact/company) + # + # @return [Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded, nil] + optional :_embedded, -> { Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded } + + # @!attribute created_at + # Unix timestamp (seconds) when the form entry was created + # + # @return [Integer, nil] + optional :created_at, Integer + + # @!attribute pipeline_id + # Pipeline id to place the created lead into + # + # @return [Integer, nil] + optional :pipeline_id, Integer + + # @!attribute request_id + # Your request id to match request items with response items + # + # @return [String, nil] + optional :request_id, String + + # @!method initialize(metadata:, source_name:, source_uid:, _embedded: nil, created_at: nil, pipeline_id: nil, request_id: nil) + # Some parameter documentations has been truncated, see + # {Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body} for more details. + # + # @param metadata [Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata] Form metadata + # + # @param source_name [String] Human-readable source name shown in amoCRM (e.g. "Website form", "Landing page") + # + # @param source_uid [String] Your stable id of the source: site/form/widget/integration that sent the lead + # + # @param _embedded [Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded] Embedded entities (lead/contact/company) + # + # @param created_at [Integer] Unix timestamp (seconds) when the form entry was created + # + # @param pipeline_id [Integer] Pipeline id to place the created lead into + # + # @param request_id [String] Your request id to match request items with response items + + # @see Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body#metadata + class Metadata < Amocrm::Internal::Type::BaseModel + # @!attribute form_id + # Form id in your system (string or numeric) + # + # @return [String, Integer, nil] + optional :form_id, union: -> { Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::FormID } + + # @!attribute form_name + # Form name (shown in amoCRM) + # + # @return [String, nil] + optional :form_name, String + + # @!attribute form_page + # Page URL where form lives + # + # @return [String, nil] + optional :form_page, String + + # @!attribute form_sent_at + # Unix timestamp (seconds) when the form was submitted + # + # @return [Integer, nil] + optional :form_sent_at, Integer + + # @!attribute form_type + # Form type code from amoCRM, use only if you know it + # + # @return [Integer, nil] + optional :form_type, Integer + + # @!attribute ip + # IP address of the submitter + # + # @return [String, nil] + optional :ip, String + + # @!attribute referer + # Referrer URL + # + # @return [String, nil] + optional :referer, String + + # @!attribute visitor_uid + # Visitor uid from tracking, if you have it + # + # @return [String, nil] + optional :visitor_uid, String + + # @!method initialize(form_id: nil, form_name: nil, form_page: nil, form_sent_at: nil, form_type: nil, ip: nil, referer: nil, visitor_uid: nil) + # Form metadata + # + # @param form_id [String, Integer] Form id in your system (string or numeric) + # + # @param form_name [String] Form name (shown in amoCRM) + # + # @param form_page [String] Page URL where form lives + # + # @param form_sent_at [Integer] Unix timestamp (seconds) when the form was submitted + # + # @param form_type [Integer] Form type code from amoCRM, use only if you know it + # + # @param ip [String] IP address of the submitter + # + # @param referer [String] Referrer URL + # + # @param visitor_uid [String] Visitor uid from tracking, if you have it + + # Form id in your system (string or numeric) + # + # @see Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata#form_id + module FormID + extend Amocrm::Internal::Type::Union + + variant String + + variant Integer + + # @!method self.variants + # @return [Array(String, Integer)] + end + end + + # @see Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body#_embedded + class Embedded < Amocrm::Internal::Type::BaseModel + # @!attribute companies + # + # @return [Array, nil] + optional :companies, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company] } + + # @!attribute contacts + # + # @return [Array, nil] + optional :contacts, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact] } + + # @!attribute leads + # Create related entities together with unsorted (lead/contact/company) + # + # @return [Array, nil] + optional :leads, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead] } + + # @!method initialize(companies: nil, contacts: nil, leads: nil) + # Embedded entities (lead/contact/company) + # + # @param companies [Array] + # + # @param contacts [Array] + # + # @param leads [Array] Create related entities together with unsorted (lead/contact/company) + + class Company < Amocrm::Internal::Type::BaseModel + # @!attribute name + # + # @return [String, nil] + optional :name, String + + # @!method initialize(name: nil) + # @param name [String] + end + + class Contact < Amocrm::Internal::Type::BaseModel + # @!attribute custom_fields_values + # Custom fields payload (same as contact create API) + # + # @return [Array, nil] + optional :custom_fields_values, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue] } + + # @!attribute first_name + # + # @return [String, nil] + optional :first_name, String + + # @!attribute last_name + # + # @return [String, nil] + optional :last_name, String + + # @!attribute name + # + # @return [String, nil] + optional :name, String + + # @!method initialize(custom_fields_values: nil, first_name: nil, last_name: nil, name: nil) + # @param custom_fields_values [Array] Custom fields payload (same as contact create API) + # + # @param first_name [String] + # + # @param last_name [String] + # + # @param name [String] + + class CustomFieldsValue < Amocrm::Internal::Type::BaseModel + # @!attribute values + # Values for the field (multiple values allowed) + # + # @return [Array] + required :values, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value] } + + # @!attribute field_code + # Field code (e.g. PHONE/EMAIL) + # + # @return [String, nil] + optional :field_code, String + + # @!attribute field_id + # Use either field_id or field_code (e.g. PHONE/EMAIL) + # + # @return [Integer, nil] + optional :field_id, Integer + + # @!method initialize(values:, field_code: nil, field_id: nil) + # @param values [Array] Values for the field (multiple values allowed) + # + # @param field_code [String] Field code (e.g. PHONE/EMAIL) + # + # @param field_id [Integer] Use either field_id or field_code (e.g. PHONE/EMAIL) + + class Value < Amocrm::Internal::Type::BaseModel + # @!attribute value + # + # @return [String, Integer, Boolean] + required :value, + union: -> { Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::Value } + + # @!attribute enum_code + # Option code for list/select custom fields (if you use code instead of id) + # + # @return [String, nil] + optional :enum_code, String + + # @!attribute enum_id + # Option id for list/select custom fields (one of predefined options) + # + # @return [Integer, nil] + optional :enum_id, Integer + + # @!method initialize(value:, enum_code: nil, enum_id: nil) + # @param value [String, Integer, Boolean] + # + # @param enum_code [String] Option code for list/select custom fields (if you use code instead of id) + # + # @param enum_id [Integer] Option id for list/select custom fields (one of predefined options) + + # @see Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value#value + module Value + extend Amocrm::Internal::Type::Union + + variant String + + variant Integer + + variant Amocrm::Internal::Type::Boolean + + # @!method self.variants + # @return [Array(String, Integer, Boolean)] + end + end + end + end + + class Lead < Amocrm::Internal::Type::BaseModel + # @!attribute _embedded + # + # @return [Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded, nil] + optional :_embedded, -> { Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded } + + # @!attribute custom_fields_values + # Custom fields payload (same as lead create API) + # + # @return [Array, nil] + optional :custom_fields_values, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue] } + + # @!attribute name + # + # @return [String, nil] + optional :name, String + + # @!attribute price + # + # @return [Integer, nil] + optional :price, Integer + + # @!attribute visitor_uid + # Website visitor uid from tracking; links this lead to a site visit + # + # @return [String, nil] + optional :visitor_uid, String + + # @!method initialize(_embedded: nil, custom_fields_values: nil, name: nil, price: nil, visitor_uid: nil) + # @param _embedded [Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded] + # + # @param custom_fields_values [Array] Custom fields payload (same as lead create API) + # + # @param name [String] + # + # @param price [Integer] + # + # @param visitor_uid [String] Website visitor uid from tracking; links this lead to a site visit + + # @see Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead#_embedded + class Embedded < Amocrm::Internal::Type::BaseModel + # @!attribute tags + # Tags to attach + # + # @return [Array, nil] + optional :tags, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag] } + + # @!method initialize(tags: nil) + # @param tags [Array] Tags to attach + + class Tag < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute name + # + # @return [String, nil] + optional :name, String + + # @!method initialize(id: nil, name: nil) + # @param id [Integer] + # @param name [String] + end + end + + class CustomFieldsValue < Amocrm::Internal::Type::BaseModel + # @!attribute values + # Values for the field (multiple values allowed) + # + # @return [Array] + required :values, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value] } + + # @!attribute field_code + # Field code (e.g. PHONE/EMAIL) + # + # @return [String, nil] + optional :field_code, String + + # @!attribute field_id + # Use either field_id or field_code (e.g. PHONE/EMAIL) + # + # @return [Integer, nil] + optional :field_id, Integer + + # @!method initialize(values:, field_code: nil, field_id: nil) + # @param values [Array] Values for the field (multiple values allowed) + # + # @param field_code [String] Field code (e.g. PHONE/EMAIL) + # + # @param field_id [Integer] Use either field_id or field_code (e.g. PHONE/EMAIL) + + class Value < Amocrm::Internal::Type::BaseModel + # @!attribute value + # + # @return [String, Integer, Boolean] + required :value, + union: -> { Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::Value } + + # @!attribute enum_code + # Option code for list/select custom fields (if you use code instead of id) + # + # @return [String, nil] + optional :enum_code, String + + # @!attribute enum_id + # Option id for list/select custom fields (one of predefined options) + # + # @return [Integer, nil] + optional :enum_id, Integer + + # @!method initialize(value:, enum_code: nil, enum_id: nil) + # @param value [String, Integer, Boolean] + # + # @param enum_code [String] Option code for list/select custom fields (if you use code instead of id) + # + # @param enum_id [Integer] Option id for list/select custom fields (one of predefined options) + + # @see Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value#value + module Value + extend Amocrm::Internal::Type::Union + + variant String + + variant Integer + + variant Amocrm::Internal::Type::Boolean + + # @!method self.variants + # @return [Array(String, Integer, Boolean)] + end + end + end + end + end + end + end + end + end + end +end diff --git a/lib/amocrm/models/v4/leads/unsorted_create_forms_response.rb b/lib/amocrm/models/v4/leads/unsorted_create_forms_response.rb new file mode 100644 index 0000000..47ddbc3 --- /dev/null +++ b/lib/amocrm/models/v4/leads/unsorted_create_forms_response.rb @@ -0,0 +1,204 @@ +# frozen_string_literal: true + +module Amocrm + module Models + module V4 + module Leads + # @see Amocrm::Resources::V4::Leads::Unsorted#create_forms + module UnsortedCreateFormsResponse + extend Amocrm::Internal::Type::Union + + variant -> { Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse } + + variant -> { Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::Problem } + + class UnsortedCreateResponse < Amocrm::Internal::Type::BaseModel + # @!attribute _embedded + # + # @return [Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded, nil] + optional :_embedded, + -> { Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded } + + # @!attribute _total_items + # + # @return [Integer, nil] + optional :_total_items, Integer + + # @!method initialize(_embedded: nil, _total_items: nil) + # @param _embedded [Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded] + # @param _total_items [Integer] + + # @see Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse#_embedded + class Embedded < Amocrm::Internal::Type::BaseModel + # @!attribute unsorted + # + # @return [Array, nil] + optional :unsorted, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted] } + + # @!method initialize(unsorted: nil) + # @param unsorted [Array] + + class Unsorted < Amocrm::Internal::Type::BaseModel + # @!attribute _embedded + # + # @return [Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded, nil] + optional :_embedded, + -> { Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded } + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!attribute account_id + # + # @return [Integer, nil] + optional :account_id, Integer + + # @!attribute request_id + # Echoed request id + # + # @return [String, nil] + optional :request_id, String + + # @!attribute uid + # + # @return [String, nil] + optional :uid, String + + # @!method initialize(_embedded: nil, _links: nil, account_id: nil, request_id: nil, uid: nil) + # @param _embedded [Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded] + # + # @param _links [Object] + # + # @param account_id [Integer] + # + # @param request_id [String] Echoed request id + # + # @param uid [String] + + # @see Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted#_embedded + class Embedded < Amocrm::Internal::Type::BaseModel + # @!attribute companies + # + # @return [Array, nil] + optional :companies, + -> do + Amocrm::Internal::Type::ArrayOf[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company + ] + end + + # @!attribute contacts + # + # @return [Array, nil] + optional :contacts, + -> do + Amocrm::Internal::Type::ArrayOf[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact + ] + end + + # @!attribute leads + # + # @return [Array, nil] + optional :leads, + -> do + Amocrm::Internal::Type::ArrayOf[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead + ] + end + + # @!method initialize(companies: nil, contacts: nil, leads: nil) + # @param companies [Array] + # @param contacts [Array] + # @param leads [Array] + + class Company < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!method initialize(id: nil, _links: nil) + # @param id [Integer] + # @param _links [Object] + end + + class Contact < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!method initialize(id: nil, _links: nil) + # @param id [Integer] + # @param _links [Object] + end + + class Lead < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!method initialize(id: nil, _links: nil) + # @param id [Integer] + # @param _links [Object] + end + end + end + end + end + + class Problem < Amocrm::Internal::Type::BaseModel + # @!attribute detail + # + # @return [String, nil] + optional :detail, String + + # @!attribute status + # + # @return [Integer, nil] + optional :status, Integer + + # @!attribute title + # + # @return [String, nil] + optional :title, String + + # @!attribute type + # + # @return [String, nil] + optional :type, String + + # @!method initialize(detail: nil, status: nil, title: nil, type: nil) + # @param detail [String] + # @param status [Integer] + # @param title [String] + # @param type [String] + end + + # @!method self.variants + # @return [Array(Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse, Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::Problem)] + end + end + end + end +end diff --git a/lib/amocrm/models/v4/leads/unsorted_decline_params.rb b/lib/amocrm/models/v4/leads/unsorted_decline_params.rb new file mode 100644 index 0000000..35240f3 --- /dev/null +++ b/lib/amocrm/models/v4/leads/unsorted_decline_params.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Amocrm + module Models + module V4 + module Leads + # @see Amocrm::Resources::V4::Leads::Unsorted#decline + class UnsortedDeclineParams < Amocrm::Internal::Type::BaseModel + extend Amocrm::Internal::Type::RequestParameters::Converter + include Amocrm::Internal::Type::RequestParameters + + # @!attribute user_id + # User id on whose behalf the item is declined + # + # @return [Integer, nil] + optional :user_id, Integer + + # @!method initialize(user_id: nil, request_options: {}) + # @param user_id [Integer] User id on whose behalf the item is declined + # + # @param request_options [Amocrm::RequestOptions, Hash{Symbol=>Object}] + end + end + end + end +end diff --git a/lib/amocrm/models/v4/leads/unsorted_decline_response.rb b/lib/amocrm/models/v4/leads/unsorted_decline_response.rb new file mode 100644 index 0000000..06355b5 --- /dev/null +++ b/lib/amocrm/models/v4/leads/unsorted_decline_response.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +module Amocrm + module Models + module V4 + module Leads + # @see Amocrm::Resources::V4::Leads::Unsorted#decline + module UnsortedDeclineResponse + extend Amocrm::Internal::Type::Union + + variant -> { Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse } + + variant -> { Amocrm::Models::V4::Leads::UnsortedDeclineResponse::Problem } + + class UnsortedAcceptResponse < Amocrm::Internal::Type::BaseModel + # @!attribute _embedded + # + # @return [Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded, nil] + optional :_embedded, + -> { Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded } + + # @!attribute category + # + # @return [Symbol, Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category, nil] + optional :category, + enum: -> { Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category } + + # @!attribute created_at + # + # @return [Integer, nil] + optional :created_at, Integer + + # @!attribute pipeline_id + # + # @return [Integer, nil] + optional :pipeline_id, Integer + + # @!attribute uid + # + # @return [String, nil] + optional :uid, String + + # @!method initialize(_embedded: nil, category: nil, created_at: nil, pipeline_id: nil, uid: nil) + # @param _embedded [Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded] + # @param category [Symbol, Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category] + # @param created_at [Integer] + # @param pipeline_id [Integer] + # @param uid [String] + + # @see Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse#_embedded + class Embedded < Amocrm::Internal::Type::BaseModel + # @!attribute companies + # + # @return [Array, nil] + optional :companies, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company] } + + # @!attribute contacts + # + # @return [Array, nil] + optional :contacts, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact] } + + # @!attribute leads + # + # @return [Array, nil] + optional :leads, + -> { Amocrm::Internal::Type::ArrayOf[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead] } + + # @!method initialize(companies: nil, contacts: nil, leads: nil) + # @param companies [Array] + # @param contacts [Array] + # @param leads [Array] + + class Company < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!method initialize(id: nil, _links: nil) + # @param id [Integer] + # @param _links [Object] + end + + class Contact < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!method initialize(id: nil, _links: nil) + # @param id [Integer] + # @param _links [Object] + end + + class Lead < Amocrm::Internal::Type::BaseModel + # @!attribute id + # + # @return [Integer, nil] + optional :id, Integer + + # @!attribute _links + # + # @return [Object, nil] + optional :_links, Amocrm::Internal::Type::Unknown + + # @!method initialize(id: nil, _links: nil) + # @param id [Integer] + # @param _links [Object] + end + end + + # @see Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse#category + module Category + extend Amocrm::Internal::Type::Enum + + SIP = :sip + MAIL = :mail + CHATS = :chats + FORMS = :forms + + # @!method self.values + # @return [Array] + end + end + + class Problem < Amocrm::Internal::Type::BaseModel + # @!attribute detail + # + # @return [String, nil] + optional :detail, String + + # @!attribute status + # + # @return [Integer, nil] + optional :status, Integer + + # @!attribute title + # + # @return [String, nil] + optional :title, String + + # @!attribute type + # + # @return [String, nil] + optional :type, String + + # @!method initialize(detail: nil, status: nil, title: nil, type: nil) + # @param detail [String] + # @param status [Integer] + # @param title [String] + # @param type [String] + end + + # @!method self.variants + # @return [Array(Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse, Amocrm::Models::V4::Leads::UnsortedDeclineResponse::Problem)] + end + end + end + end +end diff --git a/lib/amocrm/request_options.rb b/lib/amocrm/request_options.rb new file mode 100644 index 0000000..cf5185e --- /dev/null +++ b/lib/amocrm/request_options.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Amocrm + # Specify HTTP behaviour to use for a specific request. These options supplement + # or override those provided at the client level. + # + # When making a request, you can pass an actual {RequestOptions} instance, or + # simply pass a Hash with symbol keys matching the attributes on this class. + class RequestOptions < Amocrm::Internal::Type::BaseModel + # @api private + # + # @param opts [Amocrm::RequestOptions, Hash{Symbol=>Object}] + # + # @raise [ArgumentError] + def self.validate!(opts) + case opts + in Amocrm::RequestOptions | Hash + opts.to_h.each_key do |k| + unless fields.include?(k) + raise ArgumentError.new("Request `opts` keys must be one of #{fields.keys}, got #{k.inspect}") + end + end + else + raise ArgumentError.new("Request `opts` must be a Hash or RequestOptions, got #{opts.inspect}") + end + end + + # @!attribute idempotency_key + # Idempotency key to send with request and all associated retries. Will only be + # sent for write requests. + # + # @return [String, nil] + optional :idempotency_key, String + + # @!attribute extra_query + # Extra query params to send with the request. These are `.merge`’d into any + # `query` given at the client level. + # + # @return [Hash{String=>Array, String, nil}, nil] + optional :extra_query, Amocrm::Internal::Type::HashOf[Amocrm::Internal::Type::ArrayOf[String]] + + # @!attribute extra_headers + # Extra headers to send with the request. These are `.merged`’d into any + # `extra_headers` given at the client level. + # + # @return [Hash{String=>String, nil}, nil] + optional :extra_headers, Amocrm::Internal::Type::HashOf[String, nil?: true] + + # @!attribute extra_body + # Extra data to send with the request. These are deep merged into any data + # generated as part of the normal request. + # + # @return [Object, nil] + optional :extra_body, Amocrm::Internal::Type::HashOf[Amocrm::Internal::Type::Unknown] + + # @!attribute max_retries + # Maximum number of retries to attempt after a failed initial request. + # + # @return [Integer, nil] + optional :max_retries, Integer + + # @!attribute timeout + # Request timeout in seconds. + # + # @return [Float, nil] + optional :timeout, Float + + # @!method initialize(values = {}) + # Returns a new instance of RequestOptions. + # + # @param values [Hash{Symbol=>Object}] + + define_sorbet_constant!(:OrHash) do + T.type_alias { T.any(Amocrm::RequestOptions, Amocrm::Internal::AnyHash) } + end + end +end diff --git a/lib/amocrm/resources/v4.rb b/lib/amocrm/resources/v4.rb new file mode 100644 index 0000000..19b4af5 --- /dev/null +++ b/lib/amocrm/resources/v4.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Amocrm + module Resources + class V4 + # @return [Amocrm::Resources::V4::Leads] + attr_reader :leads + + # @api private + # + # @param client [Amocrm::Client] + def initialize(client:) + @client = client + @leads = Amocrm::Resources::V4::Leads.new(client: client) + end + end + end +end diff --git a/lib/amocrm/resources/v4/leads.rb b/lib/amocrm/resources/v4/leads.rb new file mode 100644 index 0000000..f0a7d41 --- /dev/null +++ b/lib/amocrm/resources/v4/leads.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Amocrm + module Resources + class V4 + class Leads + # @return [Amocrm::Resources::V4::Leads::Unsorted] + attr_reader :unsorted + + # @api private + # + # @param client [Amocrm::Client] + def initialize(client:) + @client = client + @unsorted = Amocrm::Resources::V4::Leads::Unsorted.new(client: client) + end + end + end + end +end diff --git a/lib/amocrm/resources/v4/leads/unsorted.rb b/lib/amocrm/resources/v4/leads/unsorted.rb new file mode 100644 index 0000000..a06b59a --- /dev/null +++ b/lib/amocrm/resources/v4/leads/unsorted.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Amocrm + module Resources + class V4 + class Leads + class Unsorted + # @overload accept(uid, status_id: nil, user_id: nil, request_options: {}) + # + # @param uid [String] + # + # @param status_id [Integer] Status id for the created lead + # + # @param user_id [Integer] User id on whose behalf the item is accepted + # + # @param request_options [Amocrm::RequestOptions, Hash{Symbol=>Object}, nil] + # + # @return [Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse, Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Problem] + # + # @see Amocrm::Models::V4::Leads::UnsortedAcceptParams + def accept(uid, params = {}) + parsed, options = Amocrm::V4::Leads::UnsortedAcceptParams.dump_request(params) + @client.request( + method: :post, + path: ["api/v4/leads/unsorted/%1$s/accept", uid], + body: parsed, + model: Amocrm::Models::V4::Leads::UnsortedAcceptResponse, + options: options + ) + end + + # @overload create_forms(body:, request_options: {}) + # + # @param body [Array] + # @param request_options [Amocrm::RequestOptions, Hash{Symbol=>Object}, nil] + # + # @return [Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse, Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::Problem] + # + # @see Amocrm::Models::V4::Leads::UnsortedCreateFormsParams + def create_forms(params) + parsed, options = Amocrm::V4::Leads::UnsortedCreateFormsParams.dump_request(params) + @client.request( + method: :post, + path: "api/v4/leads/unsorted/forms", + body: parsed[:body], + model: Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse, + options: options + ) + end + + # @overload decline(uid, user_id: nil, request_options: {}) + # + # @param uid [String] + # + # @param user_id [Integer] User id on whose behalf the item is declined + # + # @param request_options [Amocrm::RequestOptions, Hash{Symbol=>Object}, nil] + # + # @return [Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse, Amocrm::Models::V4::Leads::UnsortedDeclineResponse::Problem] + # + # @see Amocrm::Models::V4::Leads::UnsortedDeclineParams + def decline(uid, params = {}) + parsed, options = Amocrm::V4::Leads::UnsortedDeclineParams.dump_request(params) + @client.request( + method: :delete, + path: ["api/v4/leads/unsorted/%1$s/decline", uid], + body: parsed, + model: Amocrm::Models::V4::Leads::UnsortedDeclineResponse, + options: options + ) + end + + # @api private + # + # @param client [Amocrm::Client] + def initialize(client:) + @client = client + end + end + end + end + end +end diff --git a/lib/amocrm/version.rb b/lib/amocrm/version.rb new file mode 100644 index 0000000..e8627f2 --- /dev/null +++ b/lib/amocrm/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Amocrm + VERSION = "0.0.1" +end diff --git a/manifest.yaml b/manifest.yaml new file mode 100644 index 0000000..a1fd74a --- /dev/null +++ b/manifest.yaml @@ -0,0 +1,17 @@ +dependencies: + - English + - base64 + - cgi + - date + - erb + - etc + - json + - net/http + - openssl + - pathname + - rbconfig + - securerandom + - set + - stringio + - time + - uri diff --git a/rbi/amocrm/client.rbi b/rbi/amocrm/client.rbi new file mode 100644 index 0000000..0cb8e0b --- /dev/null +++ b/rbi/amocrm/client.rbi @@ -0,0 +1,44 @@ +# typed: strong + +module Amocrm + class Client < Amocrm::Internal::Transport::BaseClient + DEFAULT_MAX_RETRIES = 2 + + DEFAULT_TIMEOUT_IN_SECONDS = T.let(60.0, Float) + + DEFAULT_INITIAL_RETRY_DELAY = T.let(0.5, Float) + + DEFAULT_MAX_RETRY_DELAY = T.let(8.0, Float) + + sig { returns(String) } + attr_reader :api_key + + sig { returns(Amocrm::Resources::V4) } + attr_reader :v4 + + # Creates and returns a new client for interacting with the API. + sig do + params( + api_key: T.nilable(String), + base_url: T.nilable(String), + max_retries: Integer, + timeout: Float, + initial_retry_delay: Float, + max_retry_delay: Float + ).returns(T.attached_class) + end + def self.new( + # Defaults to `ENV["AMOCRM_API_KEY"]` + api_key: ENV["AMOCRM_API_KEY"], + # Override the default base URL for the API, e.g., + # `"https://api.example.com/v2/"`. Defaults to `ENV["AMOCRM_BASE_URL"]` + base_url: ENV["AMOCRM_BASE_URL"], + # Max number of retries to attempt after a failed retryable request. + max_retries: Amocrm::Client::DEFAULT_MAX_RETRIES, + timeout: Amocrm::Client::DEFAULT_TIMEOUT_IN_SECONDS, + initial_retry_delay: Amocrm::Client::DEFAULT_INITIAL_RETRY_DELAY, + max_retry_delay: Amocrm::Client::DEFAULT_MAX_RETRY_DELAY + ) + end + end +end diff --git a/rbi/amocrm/errors.rbi b/rbi/amocrm/errors.rbi new file mode 100644 index 0000000..52421b0 --- /dev/null +++ b/rbi/amocrm/errors.rbi @@ -0,0 +1,205 @@ +# typed: strong + +module Amocrm + module Errors + class Error < StandardError + sig { returns(T.nilable(StandardError)) } + attr_accessor :cause + end + + class ConversionError < Amocrm::Errors::Error + sig { returns(T.nilable(StandardError)) } + def cause + end + + # @api private + sig do + params( + on: T::Class[StandardError], + method: Symbol, + target: T.anything, + value: T.anything, + cause: T.nilable(StandardError) + ).returns(T.attached_class) + end + def self.new(on:, method:, target:, value:, cause: nil) + end + end + + class APIError < Amocrm::Errors::Error + sig { returns(URI::Generic) } + attr_accessor :url + + sig { returns(T.nilable(Integer)) } + attr_accessor :status + + sig { returns(T.nilable(T::Hash[String, String])) } + attr_accessor :headers + + sig { returns(T.nilable(T.anything)) } + attr_accessor :body + + # @api private + sig do + params( + url: URI::Generic, + status: T.nilable(Integer), + headers: T.nilable(T::Hash[String, String]), + body: T.nilable(Object), + request: NilClass, + response: NilClass, + message: T.nilable(String) + ).returns(T.attached_class) + end + def self.new( + url:, + status: nil, + headers: nil, + body: nil, + request: nil, + response: nil, + message: nil + ) + end + end + + class APIConnectionError < Amocrm::Errors::APIError + sig { returns(NilClass) } + attr_accessor :status + + sig { returns(NilClass) } + attr_accessor :body + + # @api private + sig do + params( + url: URI::Generic, + status: NilClass, + headers: T.nilable(T::Hash[String, String]), + body: NilClass, + request: NilClass, + response: NilClass, + message: T.nilable(String) + ).returns(T.attached_class) + end + def self.new( + url:, + status: nil, + headers: nil, + body: nil, + request: nil, + response: nil, + message: "Connection error." + ) + end + end + + class APITimeoutError < Amocrm::Errors::APIConnectionError + # @api private + sig do + params( + url: URI::Generic, + status: NilClass, + headers: T.nilable(T::Hash[String, String]), + body: NilClass, + request: NilClass, + response: NilClass, + message: T.nilable(String) + ).returns(T.attached_class) + end + def self.new( + url:, + status: nil, + headers: nil, + body: nil, + request: nil, + response: nil, + message: "Request timed out." + ) + end + end + + class APIStatusError < Amocrm::Errors::APIError + # @api private + sig do + params( + url: URI::Generic, + status: Integer, + headers: T.nilable(T::Hash[String, String]), + body: T.nilable(Object), + request: NilClass, + response: NilClass, + message: T.nilable(String) + ).returns(T.attached_class) + end + def self.for( + url:, + status:, + headers:, + body:, + request:, + response:, + message: nil + ) + end + + sig { returns(Integer) } + attr_accessor :status + + # @api private + sig do + params( + url: URI::Generic, + status: Integer, + headers: T.nilable(T::Hash[String, String]), + body: T.nilable(Object), + request: NilClass, + response: NilClass, + message: T.nilable(String) + ).returns(T.attached_class) + end + def self.new( + url:, + status:, + headers:, + body:, + request:, + response:, + message: nil + ) + end + end + + class BadRequestError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 400 + end + + class AuthenticationError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 401 + end + + class PermissionDeniedError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 403 + end + + class NotFoundError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 404 + end + + class ConflictError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 409 + end + + class UnprocessableEntityError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 422 + end + + class RateLimitError < Amocrm::Errors::APIStatusError + HTTP_STATUS = 429 + end + + class InternalServerError < Amocrm::Errors::APIStatusError + HTTP_STATUS = T.let((500..), T::Range[Integer]) + end + end +end diff --git a/rbi/amocrm/file_part.rbi b/rbi/amocrm/file_part.rbi new file mode 100644 index 0000000..2db65a3 --- /dev/null +++ b/rbi/amocrm/file_part.rbi @@ -0,0 +1,37 @@ +# typed: strong + +module Amocrm + class FilePart + sig { returns(T.any(Pathname, StringIO, IO, String)) } + attr_reader :content + + sig { returns(T.nilable(String)) } + attr_reader :content_type + + sig { returns(T.nilable(String)) } + attr_reader :filename + + # @api private + sig { returns(String) } + private def read + end + + sig { params(a: T.anything).returns(String) } + def to_json(*a) + end + + sig { params(a: T.anything).returns(String) } + def to_yaml(*a) + end + + sig do + params( + content: T.any(Pathname, StringIO, IO, String), + filename: T.nilable(T.any(Pathname, String)), + content_type: T.nilable(String) + ).returns(T.attached_class) + end + def self.new(content, filename: nil, content_type: nil) + end + end +end diff --git a/rbi/amocrm/internal.rbi b/rbi/amocrm/internal.rbi new file mode 100644 index 0000000..7fc492a --- /dev/null +++ b/rbi/amocrm/internal.rbi @@ -0,0 +1,16 @@ +# typed: strong + +module Amocrm + module Internal + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + # Due to the current WIP status of Shapes support in Sorbet, types referencing + # this alias might be refined in the future. + AnyHash = T.type_alias { T::Hash[Symbol, T.anything] } + + FileInput = + T.type_alias { T.any(Pathname, StringIO, IO, String, Amocrm::FilePart) } + + OMIT = T.let(Object.new.freeze, T.anything) + end +end diff --git a/rbi/amocrm/internal/transport/base_client.rbi b/rbi/amocrm/internal/transport/base_client.rbi new file mode 100644 index 0000000..e1a551d --- /dev/null +++ b/rbi/amocrm/internal/transport/base_client.rbi @@ -0,0 +1,292 @@ +# typed: strong + +module Amocrm + module Internal + module Transport + # @api private + class BaseClient + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + abstract! + + RequestComponents = + T.type_alias do + { + method: Symbol, + path: T.any(String, T::Array[String]), + query: + T.nilable( + T::Hash[String, T.nilable(T.any(T::Array[String], String))] + ), + headers: + T.nilable( + T::Hash[ + String, + T.nilable( + T.any( + String, + Integer, + T::Array[T.nilable(T.any(String, Integer))] + ) + ) + ] + ), + body: T.nilable(T.anything), + unwrap: + T.nilable( + T.any( + Symbol, + Integer, + T::Array[T.any(Symbol, Integer)], + T.proc.params(arg0: T.anything).returns(T.anything) + ) + ), + page: + T.nilable( + T::Class[ + Amocrm::Internal::Type::BasePage[ + Amocrm::Internal::Type::BaseModel + ] + ] + ), + stream: T.nilable(T::Class[T.anything]), + model: T.nilable(Amocrm::Internal::Type::Converter::Input), + options: T.nilable(Amocrm::RequestOptions::OrHash) + } + end + + RequestInput = + T.type_alias do + { + method: Symbol, + url: URI::Generic, + headers: T::Hash[String, String], + body: T.anything, + max_retries: Integer, + timeout: Float + } + end + + # from whatwg fetch spec + MAX_REDIRECTS = 20 + + PLATFORM_HEADERS = T::Hash[String, String] + + class << self + # @api private + sig do + params( + req: Amocrm::Internal::Transport::BaseClient::RequestComponents + ).void + end + def validate!(req) + end + + # @api private + sig do + params(status: Integer, headers: T::Hash[String, String]).returns( + T::Boolean + ) + end + def should_retry?(status, headers:) + end + + # @api private + sig do + params( + request: Amocrm::Internal::Transport::BaseClient::RequestInput, + status: Integer, + response_headers: T::Hash[String, String] + ).returns(Amocrm::Internal::Transport::BaseClient::RequestInput) + end + def follow_redirect(request, status:, response_headers:) + end + + # @api private + sig do + params( + status: T.any(Integer, Amocrm::Errors::APIConnectionError), + stream: T.nilable(T::Enumerable[String]) + ).void + end + def reap_connection!(status, stream:) + end + end + + sig { returns(URI::Generic) } + attr_reader :base_url + + sig { returns(Float) } + attr_reader :timeout + + sig { returns(Integer) } + attr_reader :max_retries + + sig { returns(Float) } + attr_reader :initial_retry_delay + + sig { returns(Float) } + attr_reader :max_retry_delay + + sig { returns(T::Hash[String, String]) } + attr_reader :headers + + sig { returns(T.nilable(String)) } + attr_reader :idempotency_header + + # @api private + sig { returns(Amocrm::Internal::Transport::PooledNetRequester) } + attr_reader :requester + + # @api private + sig do + params( + base_url: String, + timeout: Float, + max_retries: Integer, + initial_retry_delay: Float, + max_retry_delay: Float, + headers: + T::Hash[ + String, + T.nilable( + T.any( + String, + Integer, + T::Array[T.nilable(T.any(String, Integer))] + ) + ) + ], + idempotency_header: T.nilable(String) + ).returns(T.attached_class) + end + def self.new( + base_url:, + timeout: 0.0, + max_retries: 0, + initial_retry_delay: 0.0, + max_retry_delay: 0.0, + headers: {}, + idempotency_header: nil + ) + end + + # @api private + sig { returns(String) } + private def user_agent + end + + # @api private + sig { returns(String) } + private def generate_idempotency_key + end + + # @api private + sig do + overridable + .params( + req: Amocrm::Internal::Transport::BaseClient::RequestComponents, + opts: Amocrm::Internal::AnyHash + ) + .returns(Amocrm::Internal::Transport::BaseClient::RequestInput) + end + private def build_request(req, opts) + end + + # @api private + sig do + params( + headers: T::Hash[String, String], + retry_count: Integer + ).returns(Float) + end + private def retry_delay(headers, retry_count:) + end + + # @api private + sig do + params( + request: Amocrm::Internal::Transport::BaseClient::RequestInput, + redirect_count: Integer, + retry_count: Integer, + send_retry_header: T::Boolean + ).returns([Integer, Net::HTTPResponse, T::Enumerable[String]]) + end + def send_request( + request, + redirect_count:, + retry_count:, + send_retry_header: + ) + end + + # Execute the request specified by `req`. This is the method that all resource + # methods call into. + # + # @overload request(method, path, query: {}, headers: {}, body: nil, unwrap: nil, page: nil, stream: nil, model: Amocrm::Internal::Type::Unknown, options: {}) + sig do + params( + method: Symbol, + path: T.any(String, T::Array[String]), + query: + T.nilable( + T::Hash[String, T.nilable(T.any(T::Array[String], String))] + ), + headers: + T.nilable( + T::Hash[ + String, + T.nilable( + T.any( + String, + Integer, + T::Array[T.nilable(T.any(String, Integer))] + ) + ) + ] + ), + body: T.nilable(T.anything), + unwrap: + T.nilable( + T.any( + Symbol, + Integer, + T::Array[T.any(Symbol, Integer)], + T.proc.params(arg0: T.anything).returns(T.anything) + ) + ), + page: + T.nilable( + T::Class[ + Amocrm::Internal::Type::BasePage[ + Amocrm::Internal::Type::BaseModel + ] + ] + ), + stream: T.nilable(T::Class[T.anything]), + model: T.nilable(Amocrm::Internal::Type::Converter::Input), + options: T.nilable(Amocrm::RequestOptions::OrHash) + ).returns(T.anything) + end + def request( + method, + path, + query: {}, + headers: {}, + body: nil, + unwrap: nil, + page: nil, + stream: nil, + model: Amocrm::Internal::Type::Unknown, + options: {} + ) + end + + # @api private + sig { returns(String) } + def inspect + end + end + end + end +end diff --git a/rbi/amocrm/internal/transport/pooled_net_requester.rbi b/rbi/amocrm/internal/transport/pooled_net_requester.rbi new file mode 100644 index 0000000..50d8a37 --- /dev/null +++ b/rbi/amocrm/internal/transport/pooled_net_requester.rbi @@ -0,0 +1,82 @@ +# typed: strong + +module Amocrm + module Internal + module Transport + # @api private + class PooledNetRequester + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + Request = + T.type_alias do + { + method: Symbol, + url: URI::Generic, + headers: T::Hash[String, String], + body: T.anything, + deadline: Float + } + end + + # from the golang stdlib + # https://github.com/golang/go/blob/c8eced8580028328fde7c03cbfcb720ce15b2358/src/net/http/transport.go#L49 + KEEP_ALIVE_TIMEOUT = 30 + + DEFAULT_MAX_CONNECTIONS = T.let(T.unsafe(nil), Integer) + + class << self + # @api private + sig do + params(cert_store: OpenSSL::X509::Store, url: URI::Generic).returns( + Net::HTTP + ) + end + def connect(cert_store:, url:) + end + + # @api private + sig { params(conn: Net::HTTP, deadline: Float).void } + def calibrate_socket_timeout(conn, deadline) + end + + # @api private + sig do + params( + request: Amocrm::Internal::Transport::PooledNetRequester::Request, + blk: T.proc.params(arg0: String).void + ).returns([Net::HTTPGenericRequest, T.proc.void]) + end + def build_request(request, &blk) + end + end + + # @api private + sig do + params( + url: URI::Generic, + deadline: Float, + blk: T.proc.params(arg0: Net::HTTP).void + ).void + end + private def with_pool(url, deadline:, &blk) + end + + # @api private + sig do + params( + request: Amocrm::Internal::Transport::PooledNetRequester::Request + ).returns([Integer, Net::HTTPResponse, T::Enumerable[String]]) + end + def execute(request) + end + + # @api private + sig { params(size: Integer).returns(T.attached_class) } + def self.new( + size: Amocrm::Internal::Transport::PooledNetRequester::DEFAULT_MAX_CONNECTIONS + ) + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/array_of.rbi b/rbi/amocrm/internal/type/array_of.rbi new file mode 100644 index 0000000..dd88c22 --- /dev/null +++ b/rbi/amocrm/internal/type/array_of.rbi @@ -0,0 +1,104 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + # + # Array of items of a given type. + class ArrayOf + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + abstract! + + Elem = type_member(:out) + + sig do + params( + type_info: + T.any( + Amocrm::Internal::AnyHash, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ), + spec: Amocrm::Internal::AnyHash + ).returns(T.attached_class) + end + def self.[](type_info, spec = {}) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ===(other) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ==(other) + end + + sig { returns(Integer) } + def hash + end + + # @api private + sig do + override + .params( + value: T.any(T::Array[T.anything], T.anything), + state: Amocrm::Internal::Type::Converter::CoerceState + ) + .returns(T.any(T::Array[T.anything], T.anything)) + end + def coerce(value, state:) + end + + # @api private + sig do + override + .params( + value: T.any(T::Array[T.anything], T.anything), + state: Amocrm::Internal::Type::Converter::DumpState + ) + .returns(T.any(T::Array[T.anything], T.anything)) + end + def dump(value, state:) + end + + # @api private + sig { returns(T.anything) } + def to_sorbet_type + end + + # @api private + sig { returns(Elem) } + protected def item_type + end + + # @api private + sig { returns(T::Boolean) } + protected def nilable? + end + + # @api private + sig do + params( + type_info: + T.any( + Amocrm::Internal::AnyHash, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ), + spec: Amocrm::Internal::AnyHash + ).void + end + def initialize(type_info, spec = {}) + end + + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0) + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/base_model.rbi b/rbi/amocrm/internal/type/base_model.rbi new file mode 100644 index 0000000..57c53b4 --- /dev/null +++ b/rbi/amocrm/internal/type/base_model.rbi @@ -0,0 +1,299 @@ +# typed: strong + +module Amocrm + module Internal + module Type + class BaseModel + extend Amocrm::Internal::Type::Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + abstract! + + KnownField = + T.type_alias do + { + mode: T.nilable(Symbol), + required: T::Boolean, + nilable: T::Boolean + } + end + + OrHash = + T.type_alias do + T.any(Amocrm::Internal::Type::BaseModel, Amocrm::Internal::AnyHash) + end + + class << self + # @api private + # + # Assumes superclass fields are totally defined before fields are accessed / + # defined on subclasses. + sig { params(child: Amocrm::Internal::Type::BaseModel).void } + def inherited(child) + end + + # @api private + sig do + returns( + T::Hash[ + Symbol, + T.all( + Amocrm::Internal::Type::BaseModel::KnownField, + { + type_fn: + T.proc.returns(Amocrm::Internal::Type::Converter::Input) + } + ) + ] + ) + end + def known_fields + end + + # @api private + sig do + returns( + T::Hash[ + Symbol, + T.all( + Amocrm::Internal::Type::BaseModel::KnownField, + { type: Amocrm::Internal::Type::Converter::Input } + ) + ] + ) + end + def fields + end + + # @api private + sig do + params( + name_sym: Symbol, + required: T::Boolean, + type_info: + T.any( + { + const: + T.nilable( + T.any(NilClass, T::Boolean, Integer, Float, Symbol) + ), + enum: + T.nilable( + T.proc.returns(Amocrm::Internal::Type::Converter::Input) + ), + union: + T.nilable( + T.proc.returns(Amocrm::Internal::Type::Converter::Input) + ), + api_name: Symbol, + nil?: T::Boolean + }, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ), + spec: Amocrm::Internal::AnyHash + ).void + end + private def add_field(name_sym, required:, type_info:, spec:) + end + + # @api private + sig do + params( + name_sym: Symbol, + type_info: + T.any( + Amocrm::Internal::AnyHash, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ), + spec: Amocrm::Internal::AnyHash + ).void + end + def required(name_sym, type_info, spec = {}) + end + + # @api private + sig do + params( + name_sym: Symbol, + type_info: + T.any( + Amocrm::Internal::AnyHash, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ), + spec: Amocrm::Internal::AnyHash + ).void + end + def optional(name_sym, type_info, spec = {}) + end + + # @api private + # + # `request_only` attributes not excluded from `.#coerce` when receiving responses + # even if well behaved servers should not send them + sig { params(blk: T.proc.void).void } + private def request_only(&blk) + end + + # @api private + # + # `response_only` attributes are omitted from `.#dump` when making requests + sig { params(blk: T.proc.void).void } + private def response_only(&blk) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ==(other) + end + + sig { returns(Integer) } + def hash + end + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ==(other) + end + + sig { returns(Integer) } + def hash + end + + class << self + # @api private + sig do + override + .params( + value: + T.any( + Amocrm::Internal::Type::BaseModel, + T::Hash[T.anything, T.anything], + T.anything + ), + state: Amocrm::Internal::Type::Converter::CoerceState + ) + .returns(T.any(T.attached_class, T.anything)) + end + def coerce(value, state:) + end + + # @api private + sig do + override + .params( + value: T.any(T.attached_class, T.anything), + state: Amocrm::Internal::Type::Converter::DumpState + ) + .returns(T.any(T::Hash[T.anything, T.anything], T.anything)) + end + def dump(value, state:) + end + + # @api private + sig { returns(T.anything) } + def to_sorbet_type + end + end + + class << self + # @api private + sig do + params( + model: Amocrm::Internal::Type::BaseModel, + convert: T::Boolean + ).returns(Amocrm::Internal::AnyHash) + end + def recursively_to_h(model, convert:) + end + end + + # Returns the raw value associated with the given key, if found. Otherwise, nil is + # returned. + # + # It is valid to lookup keys that are not in the API spec, for example to access + # undocumented features. This method does not parse response data into + # higher-level types. Lookup by anything other than a Symbol is an ArgumentError. + sig { params(key: Symbol).returns(T.nilable(T.anything)) } + def [](key) + end + + # Returns a Hash of the data underlying this object. O(1) + # + # Keys are Symbols and values are the raw values from the response. The return + # value indicates which values were ever set on the object. i.e. there will be a + # key in this hash if they ever were, even if the set value was nil. + # + # This method is not recursive. The returned value is shared by the object, so it + # should not be mutated. + sig { overridable.returns(Amocrm::Internal::AnyHash) } + def to_h + end + + # Returns a Hash of the data underlying this object. O(1) + # + # Keys are Symbols and values are the raw values from the response. The return + # value indicates which values were ever set on the object. i.e. there will be a + # key in this hash if they ever were, even if the set value was nil. + # + # This method is not recursive. The returned value is shared by the object, so it + # should not be mutated. + sig { overridable.returns(Amocrm::Internal::AnyHash) } + def to_hash + end + + # In addition to the behaviour of `#to_h`, this method will recursively call + # `#to_h` on nested models. + sig { overridable.returns(Amocrm::Internal::AnyHash) } + def deep_to_h + end + + sig do + params(keys: T.nilable(T::Array[Symbol])).returns( + Amocrm::Internal::AnyHash + ) + end + def deconstruct_keys(keys) + end + + sig { params(a: T.anything).returns(String) } + def to_json(*a) + end + + sig { params(a: T.anything).returns(String) } + def to_yaml(*a) + end + + # Create a new instance of a model. + sig do + params( + data: + T.any( + T::Hash[Symbol, T.anything], + Amocrm::Internal::Type::BaseModel + ) + ).returns(T.attached_class) + end + def self.new(data = {}) + end + + class << self + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0) + end + end + + sig { returns(String) } + def to_s + end + + # @api private + sig { returns(String) } + def inspect + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/base_page.rbi b/rbi/amocrm/internal/type/base_page.rbi new file mode 100644 index 0000000..ae91d87 --- /dev/null +++ b/rbi/amocrm/internal/type/base_page.rbi @@ -0,0 +1,42 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + # + # This module provides a base implementation for paginated responses in the SDK. + module BasePage + Elem = type_member(:out) + + sig { overridable.returns(T::Boolean) } + def next_page? + end + + sig { overridable.returns(T.self_type) } + def next_page + end + + sig { overridable.params(blk: T.proc.params(arg0: Elem).void).void } + def auto_paging_each(&blk) + end + + sig { returns(T::Enumerable[Elem]) } + def to_enum + end + + # @api private + sig do + params( + client: Amocrm::Internal::Transport::BaseClient, + req: Amocrm::Internal::Transport::BaseClient::RequestComponents, + headers: T::Hash[String, String], + page_data: T.anything + ).void + end + def initialize(client:, req:, headers:, page_data:) + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/boolean.rbi b/rbi/amocrm/internal/type/boolean.rbi new file mode 100644 index 0000000..a56bfe3 --- /dev/null +++ b/rbi/amocrm/internal/type/boolean.rbi @@ -0,0 +1,58 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + # + # Ruby has no Boolean class; this is something for models to refer to. + class Boolean + extend Amocrm::Internal::Type::Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + abstract! + + sig { params(other: T.anything).returns(T::Boolean) } + def self.===(other) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def self.==(other) + end + + class << self + # @api private + # + # Coerce value to Boolean if possible, otherwise return the original value. + sig do + override + .params( + value: T.any(T::Boolean, T.anything), + state: Amocrm::Internal::Type::Converter::CoerceState + ) + .returns(T.any(T::Boolean, T.anything)) + end + def coerce(value, state:) + end + + # @api private + sig do + override + .params( + value: T.any(T::Boolean, T.anything), + state: Amocrm::Internal::Type::Converter::DumpState + ) + .returns(T.any(T::Boolean, T.anything)) + end + def dump(value, state:) + end + + # @api private + sig { returns(T.anything) } + def to_sorbet_type + end + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/converter.rbi b/rbi/amocrm/internal/type/converter.rbi new file mode 100644 index 0000000..1889227 --- /dev/null +++ b/rbi/amocrm/internal/type/converter.rbi @@ -0,0 +1,204 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + module Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + Input = + T.type_alias do + T.any(Amocrm::Internal::Type::Converter, T::Class[T.anything]) + end + + CoerceState = + T.type_alias do + { + translate_names: T::Boolean, + strictness: T::Boolean, + exactness: { + yes: Integer, + no: Integer, + maybe: Integer + }, + error: T::Class[StandardError], + branched: Integer + } + end + + DumpState = T.type_alias { { can_retry: T::Boolean } } + + # @api private + sig do + overridable + .params( + value: T.anything, + state: Amocrm::Internal::Type::Converter::CoerceState + ) + .returns(T.anything) + end + def coerce(value, state:) + end + + # @api private + sig do + overridable + .params( + value: T.anything, + state: Amocrm::Internal::Type::Converter::DumpState + ) + .returns(T.anything) + 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 + params( + spec: + T.any( + { + const: + T.nilable( + T.any(NilClass, T::Boolean, Integer, Float, Symbol) + ), + enum: + T.nilable( + T.proc.returns(Amocrm::Internal::Type::Converter::Input) + ), + union: + T.nilable( + T.proc.returns(Amocrm::Internal::Type::Converter::Input) + ) + }, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ) + ).returns(T.proc.returns(T.anything)) + end + def self.type_info(spec) + end + + # @api private + sig do + params( + type_info: + T.any( + { + const: + T.nilable( + T.any(NilClass, T::Boolean, Integer, Float, Symbol) + ), + enum: + T.nilable( + T.proc.returns(Amocrm::Internal::Type::Converter::Input) + ), + union: + T.nilable( + T.proc.returns(Amocrm::Internal::Type::Converter::Input) + ) + }, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ), + spec: + T.any( + { + const: + T.nilable( + T.any(NilClass, T::Boolean, Integer, Float, Symbol) + ), + enum: + T.nilable( + T.proc.returns(Amocrm::Internal::Type::Converter::Input) + ), + union: + T.nilable( + T.proc.returns(Amocrm::Internal::Type::Converter::Input) + ) + }, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ) + ).returns(Amocrm::Internal::AnyHash) + end + def self.meta_info(type_info, spec) + end + + # @api private + sig do + params(translate_names: T::Boolean).returns( + Amocrm::Internal::Type::Converter::CoerceState + ) + end + def self.new_coerce_state(translate_names: true) + end + + # @api private + # + # Based on `target`, transform `value` into `target`, to the extent possible: + # + # 1. if the given `value` conforms to `target` already, return the given `value` + # 2. if it's possible and safe to convert the given `value` to `target`, then the + # converted value + # 3. otherwise, the given `value` unaltered + # + # The coercion process is subject to improvement between minor release versions. + # See https://docs.pydantic.dev/latest/concepts/unions/#smart-mode + sig do + params( + target: Amocrm::Internal::Type::Converter::Input, + value: T.anything, + state: Amocrm::Internal::Type::Converter::CoerceState + ).returns(T.anything) + end + def self.coerce( + target, + value, + # The `strictness` is one of `true`, `false`. This informs the coercion strategy + # when we have to decide between multiple possible conversion targets: + # + # - `true`: the conversion must be exact, with minimum coercion. + # - `false`: the conversion can be approximate, with some coercion. + # + # The `exactness` is `Hash` with keys being one of `yes`, `no`, or `maybe`. For + # any given conversion attempt, the exactness will be updated based on how closely + # the value recursively matches the target type: + # + # - `yes`: the value can be converted to the target type with minimum coercion. + # - `maybe`: the value can be converted to the target type with some reasonable + # coercion. + # - `no`: the value cannot be converted to the target type. + # + # See implementation below for more details. + state: Amocrm::Internal::Type::Converter.new_coerce_state + ) + end + + # @api private + sig do + params( + target: Amocrm::Internal::Type::Converter::Input, + value: T.anything, + state: Amocrm::Internal::Type::Converter::DumpState + ).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 + end +end diff --git a/rbi/amocrm/internal/type/enum.rbi b/rbi/amocrm/internal/type/enum.rbi new file mode 100644 index 0000000..c93cb0d --- /dev/null +++ b/rbi/amocrm/internal/type/enum.rbi @@ -0,0 +1,82 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + # + # A value from among a specified list of options. OpenAPI enum values map to Ruby + # values in the SDK as follows: + # + # 1. boolean => true | false + # 2. integer => Integer + # 3. float => Float + # 4. string => Symbol + # + # We can therefore convert string values to Symbols, but can't convert other + # values safely. + module Enum + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + # All of the valid Symbol values for this enum. + sig do + overridable.returns( + T::Array[T.any(NilClass, T::Boolean, Integer, Float, Symbol)] + ) + end + def values + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ===(other) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ==(other) + end + + sig { returns(Integer) } + def hash + end + + # @api private + # + # Unlike with primitives, `Enum` additionally validates that the value is a member + # of the enum. + sig do + override + .params( + value: T.any(String, Symbol, T.anything), + state: Amocrm::Internal::Type::Converter::CoerceState + ) + .returns(T.any(Symbol, T.anything)) + end + def coerce(value, state:) + end + + # @api private + sig do + override + .params( + value: T.any(Symbol, T.anything), + state: Amocrm::Internal::Type::Converter::DumpState + ) + .returns(T.any(Symbol, T.anything)) + end + def dump(value, state:) + end + + # @api private + sig { returns(T.anything) } + def to_sorbet_type + end + + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0) + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/file_input.rbi b/rbi/amocrm/internal/type/file_input.rbi new file mode 100644 index 0000000..800c38a --- /dev/null +++ b/rbi/amocrm/internal/type/file_input.rbi @@ -0,0 +1,59 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + # + # Either `Pathname` or `StringIO`, or `IO`, or + # `Amocrm::Internal::Type::FileInput`. + # + # Note: when `IO` is used, all retries are disabled, since many IO` streams are + # not rewindable. + class FileInput + extend Amocrm::Internal::Type::Converter + + abstract! + + sig { params(other: T.anything).returns(T::Boolean) } + def self.===(other) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def self.==(other) + end + + class << self + # @api private + sig do + override + .params( + value: T.any(StringIO, String, T.anything), + state: Amocrm::Internal::Type::Converter::CoerceState + ) + .returns(T.any(StringIO, T.anything)) + end + def coerce(value, state:) + end + + # @api private + sig do + override + .params( + value: T.any(Pathname, StringIO, IO, String, T.anything), + state: Amocrm::Internal::Type::Converter::DumpState + ) + .returns(T.any(Pathname, StringIO, IO, String, T.anything)) + end + def dump(value, state:) + end + + # @api private + sig { returns(T.anything) } + def to_sorbet_type + end + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/hash_of.rbi b/rbi/amocrm/internal/type/hash_of.rbi new file mode 100644 index 0000000..2f0fb00 --- /dev/null +++ b/rbi/amocrm/internal/type/hash_of.rbi @@ -0,0 +1,104 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + # + # Hash of items of a given type. + class HashOf + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + abstract! + + Elem = type_member(:out) + + sig do + params( + type_info: + T.any( + Amocrm::Internal::AnyHash, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ), + spec: Amocrm::Internal::AnyHash + ).returns(T.attached_class) + end + def self.[](type_info, spec = {}) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ===(other) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ==(other) + end + + sig { returns(Integer) } + def hash + end + + # @api private + sig do + override + .params( + value: T.any(T::Hash[T.anything, T.anything], T.anything), + state: Amocrm::Internal::Type::Converter::CoerceState + ) + .returns(T.any(Amocrm::Internal::AnyHash, T.anything)) + end + def coerce(value, state:) + end + + # @api private + sig do + override + .params( + value: T.any(T::Hash[T.anything, T.anything], T.anything), + state: Amocrm::Internal::Type::Converter::DumpState + ) + .returns(T.any(Amocrm::Internal::AnyHash, T.anything)) + end + def dump(value, state:) + end + + # @api private + sig { returns(T.anything) } + def to_sorbet_type + end + + # @api private + sig { returns(Elem) } + protected def item_type + end + + # @api private + sig { returns(T::Boolean) } + protected def nilable? + end + + # @api private + sig do + params( + type_info: + T.any( + Amocrm::Internal::AnyHash, + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::Type::Converter::Input + ), + spec: Amocrm::Internal::AnyHash + ).void + end + def initialize(type_info, spec = {}) + end + + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0) + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/request_parameters.rbi b/rbi/amocrm/internal/type/request_parameters.rbi new file mode 100644 index 0000000..b181028 --- /dev/null +++ b/rbi/amocrm/internal/type/request_parameters.rbi @@ -0,0 +1,29 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + module RequestParameters + # Options to specify HTTP behaviour for this request. + sig { returns(Amocrm::RequestOptions) } + attr_reader :request_options + + sig { params(request_options: Amocrm::RequestOptions::OrHash).void } + attr_writer :request_options + + # @api private + module Converter + # @api private + sig do + params(params: T.anything).returns( + [T.anything, Amocrm::Internal::AnyHash] + ) + end + def dump_request(params) + end + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/union.rbi b/rbi/amocrm/internal/type/union.rbi new file mode 100644 index 0000000..5f2c460 --- /dev/null +++ b/rbi/amocrm/internal/type/union.rbi @@ -0,0 +1,126 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + module Union + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + # @api private + # + # All of the specified variant info for this union. + sig do + returns( + T::Array[ + [ + T.nilable(Symbol), + T.proc.returns(Amocrm::Internal::Type::Converter::Input), + Amocrm::Internal::AnyHash + ] + ] + ) + end + private def known_variants + end + + # @api private + sig do + returns( + T::Array[[T.nilable(Symbol), T.anything, Amocrm::Internal::AnyHash]] + ) + end + protected def derefed_variants + end + + # All of the specified variants for this union. + sig { overridable.returns(T::Array[T.anything]) } + def variants + end + + # @api private + sig { params(property: Symbol).void } + private def discriminator(property) + end + + # @api private + sig do + params( + key: + T.any( + Symbol, + Amocrm::Internal::AnyHash, + T.proc.returns(T.anything), + T.anything + ), + spec: + T.any( + Amocrm::Internal::AnyHash, + T.proc.returns(T.anything), + T.anything + ) + ).void + end + private def variant(key, spec = nil) + end + + # @api private + sig { params(value: T.anything).returns(T.nilable(T.anything)) } + private def resolve_variant(value) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ===(other) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ==(other) + end + + sig { returns(Integer) } + def hash + end + + # @api private + # + # Tries to efficiently coerce the given value to one of the known variants. + # + # If the value cannot match any of the known variants, the coercion is considered + # non-viable and returns the original value. + sig do + override + .params( + value: T.anything, + state: Amocrm::Internal::Type::Converter::CoerceState + ) + .returns(T.anything) + end + def coerce(value, state:) + end + + # @api private + sig do + override + .params( + value: T.anything, + state: Amocrm::Internal::Type::Converter::DumpState + ) + .returns(T.anything) + end + def dump(value, state:) + end + + # @api private + sig { returns(T.anything) } + def to_sorbet_type + end + + # @api private + sig { params(depth: Integer).returns(String) } + def inspect(depth: 0) + end + end + end + end +end diff --git a/rbi/amocrm/internal/type/unknown.rbi b/rbi/amocrm/internal/type/unknown.rbi new file mode 100644 index 0000000..82932be --- /dev/null +++ b/rbi/amocrm/internal/type/unknown.rbi @@ -0,0 +1,58 @@ +# typed: strong + +module Amocrm + module Internal + module Type + # @api private + # + # When we don't know what to expect for the value. + class Unknown + extend Amocrm::Internal::Type::Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + abstract! + + sig { params(other: T.anything).returns(T::Boolean) } + def self.===(other) + end + + sig { params(other: T.anything).returns(T::Boolean) } + def self.==(other) + end + + class << self + # @api private + # + # No coercion needed for Unknown type. + sig do + override + .params( + value: T.anything, + state: Amocrm::Internal::Type::Converter::CoerceState + ) + .returns(T.anything) + end + def coerce(value, state:) + end + + # @api private + sig do + override + .params( + value: T.anything, + state: Amocrm::Internal::Type::Converter::DumpState + ) + .returns(T.anything) + end + def dump(value, state:) + end + + # @api private + sig { returns(T.anything) } + def to_sorbet_type + end + end + end + end + end +end diff --git a/rbi/amocrm/internal/util.rbi b/rbi/amocrm/internal/util.rbi new file mode 100644 index 0000000..812cdd9 --- /dev/null +++ b/rbi/amocrm/internal/util.rbi @@ -0,0 +1,484 @@ +# typed: strong + +module Amocrm + module Internal + # @api private + module Util + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + # @api private + sig { returns(Float) } + def self.monotonic_secs + end + + # @api private + sig do + params(ns: T.any(Module, T::Class[T.anything])).returns( + T::Enumerable[T.any(Module, T::Class[T.anything])] + ) + end + def self.walk_namespaces(ns) + end + + class << self + # @api private + sig { returns(String) } + def arch + end + + # @api private + sig { returns(String) } + def os + end + end + + class << self + # @api private + sig { params(input: T.anything).returns(T::Boolean) } + def primitive?(input) + end + + # @api private + sig do + params(input: T.any(String, T::Boolean)).returns( + T.any(T::Boolean, T.anything) + ) + end + def coerce_boolean(input) + end + + # @api private + sig do + params(input: T.any(String, T::Boolean)).returns( + T.nilable(T::Boolean) + ) + end + def coerce_boolean!(input) + end + + # @api private + sig do + params(input: T.any(String, Integer)).returns( + T.any(Integer, T.anything) + ) + end + def coerce_integer(input) + end + + # @api private + sig do + params(input: T.any(String, Integer, Float)).returns( + T.any(Float, T.anything) + ) + end + def coerce_float(input) + end + + # @api private + sig do + params(input: T.anything).returns( + T.any(T::Hash[T.anything, T.anything], T.anything) + ) + end + def coerce_hash(input) + end + + # @api private + sig do + params(input: T.anything).returns( + T.nilable(T::Hash[T.anything, T.anything]) + ) + end + def coerce_hash!(input) + end + end + + class << self + # @api private + sig do + params(lhs: T.anything, rhs: T.anything, concat: T::Boolean).returns( + T.anything + ) + end + private def deep_merge_lr(lhs, rhs, concat: false) + end + + # @api private + # + # Recursively merge one hash with another. If the values at a given key are not + # both hashes, just take the new value. + sig do + params( + values: T::Array[T.anything], + sentinel: T.nilable(T.anything), + concat: T::Boolean + ).returns(T.anything) + end + def deep_merge( + *values, + # the value to return if no values are provided. + sentinel: nil, + # whether to merge sequences by concatenation. + concat: false + ) + end + + # @api private + sig do + params( + data: + T.any( + Amocrm::Internal::AnyHash, + T::Array[T.anything], + T.anything + ), + pick: + T.nilable( + T.any( + Symbol, + Integer, + T::Array[T.any(Symbol, Integer)], + T.proc.params(arg0: T.anything).returns(T.anything) + ) + ), + blk: T.nilable(T.proc.returns(T.anything)) + ).returns(T.nilable(T.anything)) + end + def dig(data, pick, &blk) + end + end + + class << self + # @api private + sig { params(uri: URI::Generic).returns(String) } + def uri_origin(uri) + end + + # @api private + sig { params(path: T.any(String, T::Array[String])).returns(String) } + def interpolate_path(path) + end + end + + class << self + # @api private + sig do + params(query: T.nilable(String)).returns( + T::Hash[String, T::Array[String]] + ) + end + def decode_query(query) + end + + # @api private + sig do + params( + query: + T.nilable( + T::Hash[String, T.nilable(T.any(T::Array[String], String))] + ) + ).returns(T.nilable(String)) + end + def encode_query(query) + end + end + + ParsedUri = + T.type_alias do + { + scheme: T.nilable(String), + host: T.nilable(String), + port: T.nilable(Integer), + path: T.nilable(String), + query: T::Hash[String, T::Array[String]] + } + end + + class << self + # @api private + sig do + params(url: T.any(URI::Generic, String)).returns( + Amocrm::Internal::Util::ParsedUri + ) + end + def parse_uri(url) + end + + # @api private + sig do + params(parsed: Amocrm::Internal::Util::ParsedUri).returns( + URI::Generic + ) + end + def unparse_uri(parsed) + end + + # @api private + sig do + params( + lhs: Amocrm::Internal::Util::ParsedUri, + rhs: Amocrm::Internal::Util::ParsedUri + ).returns(URI::Generic) + end + def join_parsed_uri(lhs, rhs) + end + end + + class << self + # @api private + sig do + params( + headers: + T::Hash[ + String, + T.nilable( + T.any( + String, + Integer, + T::Array[T.nilable(T.any(String, Integer))] + ) + ) + ] + ).returns(T::Hash[String, String]) + end + def normalized_headers(*headers) + end + end + + # @api private + # + # An adapter that satisfies the IO interface required by `::IO.copy_stream` + class ReadIOAdapter + # @api private + sig { returns(T.nilable(T::Boolean)) } + def close? + end + + # @api private + sig { void } + def close + end + + # @api private + sig { params(max_len: T.nilable(Integer)).returns(String) } + private def read_enum(max_len) + end + + # @api private + sig do + params( + max_len: T.nilable(Integer), + out_string: T.nilable(String) + ).returns(T.nilable(String)) + end + def read(max_len = nil, out_string = nil) + end + + # @api private + sig do + params( + src: T.any(String, Pathname, StringIO, T::Enumerable[String]), + blk: T.proc.params(arg0: String).void + ).returns(T.attached_class) + end + def self.new(src, &blk) + end + end + + class << self + sig do + params(blk: T.proc.params(y: Enumerator::Yielder).void).returns( + T::Enumerable[String] + ) + end + def writable_enum(&blk) + end + end + + JSON_CONTENT = + T.let(%r{^application/(?:vnd(?:\.[^.]+)*\+)?json(?!l)}, Regexp) + JSONL_CONTENT = + T.let(%r{^application/(:?x-(?:n|l)djson)|(:?(?:x-)?jsonl)}, Regexp) + + class << self + # @api private + sig do + params( + y: Enumerator::Yielder, + val: T.anything, + closing: T::Array[T.proc.void], + content_type: T.nilable(String) + ).void + end + private def write_multipart_content( + y, + val:, + closing:, + content_type: nil + ) + end + + # @api private + sig do + params( + y: Enumerator::Yielder, + boundary: String, + key: T.any(Symbol, String), + val: T.anything, + closing: T::Array[T.proc.void] + ).void + end + private def write_multipart_chunk(y, boundary:, key:, val:, closing:) + end + + # @api private + # + # https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#special-considerations-for-multipart-content + sig do + params(body: T.anything).returns([String, T::Enumerable[String]]) + end + private def encode_multipart_streaming(body) + end + + # @api private + sig do + params(headers: T::Hash[String, String], body: T.anything).returns( + T.anything + ) + end + 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::Hash[String, String], + stream: T::Enumerable[String], + suppress_error: T::Boolean + ).returns(T.anything) + end + def decode_content(headers, stream:, suppress_error: false) + end + end + + class << self + # @api private + # + # https://doc.rust-lang.org/std/iter/trait.FusedIterator.html + sig do + params( + enum: T::Enumerable[T.anything], + external: T::Boolean, + close: T.proc.void + ).returns(T::Enumerable[T.anything]) + end + def fused_enum(enum, external: false, &close) + end + + # @api private + sig { params(enum: T.nilable(T::Enumerable[T.anything])).void } + def close_fused!(enum) + end + + # @api private + sig do + params( + enum: T.nilable(T::Enumerable[T.anything]), + blk: T.proc.params(arg0: Enumerator::Yielder).void + ).returns(T::Enumerable[T.anything]) + end + def chain_fused(enum, &blk) + end + end + + ServerSentEvent = + T.type_alias do + { + event: T.nilable(String), + data: T.nilable(String), + id: T.nilable(String), + retry: T.nilable(Integer) + } + end + + 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 do + params(enum: T::Enumerable[String]).returns(T::Enumerable[String]) + end + 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[Amocrm::Internal::Util::ServerSentEvent] + ) + end + def decode_sse(lines) + end + end + + # @api private + module SorbetRuntimeSupport + class MissingSorbetRuntimeError < ::RuntimeError + end + + # @api private + sig { returns(T::Hash[Symbol, T.anything]) } + private def sorbet_runtime_constants + end + + # @api private + sig { params(name: Symbol).void } + def const_missing(name) + end + + # @api private + sig { params(name: Symbol).returns(T::Boolean) } + def sorbet_constant_defined?(name) + end + + # @api private + sig { params(name: Symbol, blk: T.proc.returns(T.anything)).void } + def define_sorbet_constant!(name, &blk) + end + + # @api private + sig { returns(T.anything) } + def to_sorbet_type + end + + class << self + # @api private + sig do + params( + type: + T.any(Amocrm::Internal::Util::SorbetRuntimeSupport, T.anything) + ).returns(T.anything) + end + def to_sorbet_type(type) + end + end + end + end + end +end diff --git a/rbi/amocrm/models.rbi b/rbi/amocrm/models.rbi new file mode 100644 index 0000000..96b2e61 --- /dev/null +++ b/rbi/amocrm/models.rbi @@ -0,0 +1,5 @@ +# typed: strong + +module Amocrm + V4 = Amocrm::Models::V4 +end diff --git a/rbi/amocrm/models/v4/leads/unsorted_accept_params.rbi b/rbi/amocrm/models/v4/leads/unsorted_accept_params.rbi new file mode 100644 index 0000000..aecb7de --- /dev/null +++ b/rbi/amocrm/models/v4/leads/unsorted_accept_params.rbi @@ -0,0 +1,64 @@ +# typed: strong + +module Amocrm + module Models + module V4 + module Leads + class UnsortedAcceptParams < Amocrm::Internal::Type::BaseModel + extend Amocrm::Internal::Type::RequestParameters::Converter + include Amocrm::Internal::Type::RequestParameters + + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedAcceptParams, + Amocrm::Internal::AnyHash + ) + end + + # Status id for the created lead + sig { returns(T.nilable(Integer)) } + attr_reader :status_id + + sig { params(status_id: Integer).void } + attr_writer :status_id + + # User id on whose behalf the item is accepted + sig { returns(T.nilable(Integer)) } + attr_reader :user_id + + sig { params(user_id: Integer).void } + attr_writer :user_id + + sig do + params( + status_id: Integer, + user_id: Integer, + request_options: Amocrm::RequestOptions::OrHash + ).returns(T.attached_class) + end + def self.new( + # Status id for the created lead + status_id: nil, + # User id on whose behalf the item is accepted + user_id: nil, + request_options: {} + ) + end + + sig do + override.returns( + { + status_id: Integer, + user_id: Integer, + request_options: Amocrm::RequestOptions + } + ) + end + def to_hash + end + end + end + end + end +end diff --git a/rbi/amocrm/models/v4/leads/unsorted_accept_response.rbi b/rbi/amocrm/models/v4/leads/unsorted_accept_response.rbi new file mode 100644 index 0000000..c48321f --- /dev/null +++ b/rbi/amocrm/models/v4/leads/unsorted_accept_response.rbi @@ -0,0 +1,441 @@ +# typed: strong + +module Amocrm + module Models + module V4 + module Leads + module UnsortedAcceptResponse + extend Amocrm::Internal::Type::Union + + Variants = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse, + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Problem + ) + end + + class UnsortedAcceptResponse < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded + ) + ) + end + attr_reader :_embedded + + sig do + params( + _embedded: + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::OrHash + ).void + end + attr_writer :_embedded + + sig do + returns( + T.nilable( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + ) + end + attr_reader :category + + sig do + params( + category: + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category::OrSymbol + ).void + end + attr_writer :category + + sig { returns(T.nilable(Integer)) } + attr_reader :created_at + + sig { params(created_at: Integer).void } + attr_writer :created_at + + sig { returns(T.nilable(Integer)) } + attr_reader :pipeline_id + + sig { params(pipeline_id: Integer).void } + attr_writer :pipeline_id + + sig { returns(T.nilable(String)) } + attr_reader :uid + + sig { params(uid: String).void } + attr_writer :uid + + sig do + params( + _embedded: + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::OrHash, + category: + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category::OrSymbol, + created_at: Integer, + pipeline_id: Integer, + uid: String + ).returns(T.attached_class) + end + def self.new( + _embedded: nil, + category: nil, + created_at: nil, + pipeline_id: nil, + uid: nil + ) + end + + sig do + override.returns( + { + _embedded: + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded, + category: + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category::TaggedSymbol, + created_at: Integer, + pipeline_id: Integer, + uid: String + } + ) + end + def to_hash + end + + class Embedded < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company + ] + ) + ) + end + attr_reader :companies + + sig do + params( + companies: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company::OrHash + ] + ).void + end + attr_writer :companies + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact + ] + ) + ) + end + attr_reader :contacts + + sig do + params( + contacts: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact::OrHash + ] + ).void + end + attr_writer :contacts + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead + ] + ) + ) + end + attr_reader :leads + + sig do + params( + leads: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead::OrHash + ] + ).void + end + attr_writer :leads + + sig do + params( + companies: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company::OrHash + ], + contacts: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact::OrHash + ], + leads: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead::OrHash + ] + ).returns(T.attached_class) + end + def self.new(companies: nil, contacts: nil, leads: nil) + end + + sig do + override.returns( + { + companies: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company + ], + contacts: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact + ], + leads: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead + ] + } + ) + end + def to_hash + end + + class Company < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig do + params(id: Integer, _links: T.anything).returns( + T.attached_class + ) + end + def self.new(id: nil, _links: nil) + end + + sig { override.returns({ id: Integer, _links: T.anything }) } + def to_hash + end + end + + class Contact < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig do + params(id: Integer, _links: T.anything).returns( + T.attached_class + ) + end + def self.new(id: nil, _links: nil) + end + + sig { override.returns({ id: Integer, _links: T.anything }) } + def to_hash + end + end + + class Lead < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig do + params(id: Integer, _links: T.anything).returns( + T.attached_class + ) + end + def self.new(id: nil, _links: nil) + end + + sig { override.returns({ id: Integer, _links: T.anything }) } + def to_hash + end + end + end + + module Category + extend Amocrm::Internal::Type::Enum + + TaggedSymbol = + T.type_alias do + T.all( + Symbol, + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category + ) + end + OrSymbol = T.type_alias { T.any(Symbol, String) } + + SIP = + T.let( + :sip, + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + MAIL = + T.let( + :mail, + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + CHATS = + T.let( + :chats, + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + FORMS = + T.let( + :forms, + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + + sig do + override.returns( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ] + ) + end + def self.values + end + end + end + + class Problem < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Problem, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(String)) } + attr_reader :detail + + sig { params(detail: String).void } + attr_writer :detail + + sig { returns(T.nilable(Integer)) } + attr_reader :status + + sig { params(status: Integer).void } + attr_writer :status + + sig { returns(T.nilable(String)) } + attr_reader :title + + sig { params(title: String).void } + attr_writer :title + + sig { returns(T.nilable(String)) } + attr_reader :type + + sig { params(type: String).void } + attr_writer :type + + sig do + params( + detail: String, + status: Integer, + title: String, + type: String + ).returns(T.attached_class) + end + def self.new(detail: nil, status: nil, title: nil, type: nil) + end + + sig do + override.returns( + { detail: String, status: Integer, title: String, type: String } + ) + end + def to_hash + end + end + + sig do + override.returns( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Variants + ] + ) + end + def self.variants + end + end + end + end + end +end diff --git a/rbi/amocrm/models/v4/leads/unsorted_create_forms_params.rbi b/rbi/amocrm/models/v4/leads/unsorted_create_forms_params.rbi new file mode 100644 index 0000000..ddeb003 --- /dev/null +++ b/rbi/amocrm/models/v4/leads/unsorted_create_forms_params.rbi @@ -0,0 +1,1048 @@ +# typed: strong + +module Amocrm + module Models + module V4 + module Leads + class UnsortedCreateFormsParams < Amocrm::Internal::Type::BaseModel + extend Amocrm::Internal::Type::RequestParameters::Converter + include Amocrm::Internal::Type::RequestParameters + + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body] + ) + end + attr_accessor :body + + sig do + params( + body: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::OrHash + ], + request_options: Amocrm::RequestOptions::OrHash + ).returns(T.attached_class) + end + def self.new(body:, request_options: {}) + end + + sig do + override.returns( + { + body: + T::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body], + request_options: Amocrm::RequestOptions + } + ) + end + def to_hash + end + + class Body < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body, + Amocrm::Internal::AnyHash + ) + end + + # Form metadata + sig do + returns( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata + ) + end + attr_reader :metadata + + sig do + params( + metadata: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::OrHash + ).void + end + attr_writer :metadata + + # Human-readable source name shown in amoCRM (e.g. "Website form", "Landing page") + sig { returns(String) } + attr_accessor :source_name + + # Your stable id of the source: site/form/widget/integration that sent the lead + sig { returns(String) } + attr_accessor :source_uid + + # Embedded entities (lead/contact/company) + sig do + returns( + T.nilable( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded + ) + ) + end + attr_reader :_embedded + + sig do + params( + _embedded: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::OrHash + ).void + end + attr_writer :_embedded + + # Unix timestamp (seconds) when the form entry was created + sig { returns(T.nilable(Integer)) } + attr_reader :created_at + + sig { params(created_at: Integer).void } + attr_writer :created_at + + # Pipeline id to place the created lead into + sig { returns(T.nilable(Integer)) } + attr_reader :pipeline_id + + sig { params(pipeline_id: Integer).void } + attr_writer :pipeline_id + + # Your request id to match request items with response items + sig { returns(T.nilable(String)) } + attr_reader :request_id + + sig { params(request_id: String).void } + attr_writer :request_id + + sig do + params( + metadata: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::OrHash, + source_name: String, + source_uid: String, + _embedded: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::OrHash, + created_at: Integer, + pipeline_id: Integer, + request_id: String + ).returns(T.attached_class) + end + def self.new( + # Form metadata + metadata:, + # Human-readable source name shown in amoCRM (e.g. "Website form", "Landing page") + source_name:, + # Your stable id of the source: site/form/widget/integration that sent the lead + source_uid:, + # Embedded entities (lead/contact/company) + _embedded: nil, + # Unix timestamp (seconds) when the form entry was created + created_at: nil, + # Pipeline id to place the created lead into + pipeline_id: nil, + # Your request id to match request items with response items + request_id: nil + ) + end + + sig do + override.returns( + { + metadata: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata, + source_name: String, + source_uid: String, + _embedded: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded, + created_at: Integer, + pipeline_id: Integer, + request_id: String + } + ) + end + def to_hash + end + + class Metadata < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata, + Amocrm::Internal::AnyHash + ) + end + + # Form id in your system (string or numeric) + sig do + returns( + T.nilable( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::FormID::Variants + ) + ) + end + attr_reader :form_id + + sig do + params( + form_id: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::FormID::Variants + ).void + end + attr_writer :form_id + + # Form name (shown in amoCRM) + sig { returns(T.nilable(String)) } + attr_reader :form_name + + sig { params(form_name: String).void } + attr_writer :form_name + + # Page URL where form lives + sig { returns(T.nilable(String)) } + attr_reader :form_page + + sig { params(form_page: String).void } + attr_writer :form_page + + # Unix timestamp (seconds) when the form was submitted + sig { returns(T.nilable(Integer)) } + attr_reader :form_sent_at + + sig { params(form_sent_at: Integer).void } + attr_writer :form_sent_at + + # Form type code from amoCRM, use only if you know it + sig { returns(T.nilable(Integer)) } + attr_reader :form_type + + sig { params(form_type: Integer).void } + attr_writer :form_type + + # IP address of the submitter + sig { returns(T.nilable(String)) } + attr_reader :ip + + sig { params(ip: String).void } + attr_writer :ip + + # Referrer URL + sig { returns(T.nilable(String)) } + attr_reader :referer + + sig { params(referer: String).void } + attr_writer :referer + + # Visitor uid from tracking, if you have it + sig { returns(T.nilable(String)) } + attr_reader :visitor_uid + + sig { params(visitor_uid: String).void } + attr_writer :visitor_uid + + # Form metadata + sig do + params( + form_id: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::FormID::Variants, + form_name: String, + form_page: String, + form_sent_at: Integer, + form_type: Integer, + ip: String, + referer: String, + visitor_uid: String + ).returns(T.attached_class) + end + def self.new( + # Form id in your system (string or numeric) + form_id: nil, + # Form name (shown in amoCRM) + form_name: nil, + # Page URL where form lives + form_page: nil, + # Unix timestamp (seconds) when the form was submitted + form_sent_at: nil, + # Form type code from amoCRM, use only if you know it + form_type: nil, + # IP address of the submitter + ip: nil, + # Referrer URL + referer: nil, + # Visitor uid from tracking, if you have it + visitor_uid: nil + ) + end + + sig do + override.returns( + { + form_id: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::FormID::Variants, + form_name: String, + form_page: String, + form_sent_at: Integer, + form_type: Integer, + ip: String, + referer: String, + visitor_uid: String + } + ) + end + def to_hash + end + + # Form id in your system (string or numeric) + module FormID + extend Amocrm::Internal::Type::Union + + Variants = T.type_alias { T.any(String, Integer) } + + sig do + override.returns( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::FormID::Variants + ] + ) + end + def self.variants + end + end + end + + class Embedded < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company + ] + ) + ) + end + attr_reader :companies + + sig do + params( + companies: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company::OrHash + ] + ).void + end + attr_writer :companies + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact + ] + ) + ) + end + attr_reader :contacts + + sig do + params( + contacts: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::OrHash + ] + ).void + end + attr_writer :contacts + + # Create related entities together with unsorted (lead/contact/company) + sig do + returns( + T.nilable( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead + ] + ) + ) + end + attr_reader :leads + + sig do + params( + leads: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::OrHash + ] + ).void + end + attr_writer :leads + + # Embedded entities (lead/contact/company) + sig do + params( + companies: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company::OrHash + ], + contacts: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::OrHash + ], + leads: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::OrHash + ] + ).returns(T.attached_class) + end + def self.new( + companies: nil, + contacts: nil, + # Create related entities together with unsorted (lead/contact/company) + leads: nil + ) + end + + sig do + override.returns( + { + companies: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company + ], + contacts: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact + ], + leads: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead + ] + } + ) + end + def to_hash + end + + class Company < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(String)) } + attr_reader :name + + sig { params(name: String).void } + attr_writer :name + + sig { params(name: String).returns(T.attached_class) } + def self.new(name: nil) + end + + sig { override.returns({ name: String }) } + def to_hash + end + end + + class Contact < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact, + Amocrm::Internal::AnyHash + ) + end + + # Custom fields payload (same as contact create API) + sig do + returns( + T.nilable( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue + ] + ) + ) + end + attr_reader :custom_fields_values + + sig do + params( + custom_fields_values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::OrHash + ] + ).void + end + attr_writer :custom_fields_values + + sig { returns(T.nilable(String)) } + attr_reader :first_name + + sig { params(first_name: String).void } + attr_writer :first_name + + sig { returns(T.nilable(String)) } + attr_reader :last_name + + sig { params(last_name: String).void } + attr_writer :last_name + + sig { returns(T.nilable(String)) } + attr_reader :name + + sig { params(name: String).void } + attr_writer :name + + sig do + params( + custom_fields_values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::OrHash + ], + first_name: String, + last_name: String, + name: String + ).returns(T.attached_class) + end + def self.new( + # Custom fields payload (same as contact create API) + custom_fields_values: nil, + first_name: nil, + last_name: nil, + name: nil + ) + end + + sig do + override.returns( + { + custom_fields_values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue + ], + first_name: String, + last_name: String, + name: String + } + ) + end + def to_hash + end + + class CustomFieldsValue < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue, + Amocrm::Internal::AnyHash + ) + end + + # Values for the field (multiple values allowed) + sig do + returns( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value + ] + ) + end + attr_accessor :values + + # Field code (e.g. PHONE/EMAIL) + sig { returns(T.nilable(String)) } + attr_reader :field_code + + sig { params(field_code: String).void } + attr_writer :field_code + + # Use either field_id or field_code (e.g. PHONE/EMAIL) + sig { returns(T.nilable(Integer)) } + attr_reader :field_id + + sig { params(field_id: Integer).void } + attr_writer :field_id + + sig do + params( + values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::OrHash + ], + field_code: String, + field_id: Integer + ).returns(T.attached_class) + end + def self.new( + # Values for the field (multiple values allowed) + values:, + # Field code (e.g. PHONE/EMAIL) + field_code: nil, + # Use either field_id or field_code (e.g. PHONE/EMAIL) + field_id: nil + ) + end + + sig do + override.returns( + { + values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value + ], + field_code: String, + field_id: Integer + } + ) + end + def to_hash + end + + class Value < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::Value::Variants + ) + end + attr_accessor :value + + # Option code for list/select custom fields (if you use code instead of id) + sig { returns(T.nilable(String)) } + attr_reader :enum_code + + sig { params(enum_code: String).void } + attr_writer :enum_code + + # Option id for list/select custom fields (one of predefined options) + sig { returns(T.nilable(Integer)) } + attr_reader :enum_id + + sig { params(enum_id: Integer).void } + attr_writer :enum_id + + sig do + params( + value: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::Value::Variants, + enum_code: String, + enum_id: Integer + ).returns(T.attached_class) + end + def self.new( + value:, + # Option code for list/select custom fields (if you use code instead of id) + enum_code: nil, + # Option id for list/select custom fields (one of predefined options) + enum_id: nil + ) + end + + sig do + override.returns( + { + value: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::Value::Variants, + enum_code: String, + enum_id: Integer + } + ) + end + def to_hash + end + + module Value + extend Amocrm::Internal::Type::Union + + Variants = + T.type_alias { T.any(String, Integer, T::Boolean) } + + sig do + override.returns( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::Value::Variants + ] + ) + end + def self.variants + end + end + end + end + end + + class Lead < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded + ) + ) + end + attr_reader :_embedded + + sig do + params( + _embedded: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::OrHash + ).void + end + attr_writer :_embedded + + # Custom fields payload (same as lead create API) + sig do + returns( + T.nilable( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue + ] + ) + ) + end + attr_reader :custom_fields_values + + sig do + params( + custom_fields_values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::OrHash + ] + ).void + end + attr_writer :custom_fields_values + + sig { returns(T.nilable(String)) } + attr_reader :name + + sig { params(name: String).void } + attr_writer :name + + sig { returns(T.nilable(Integer)) } + attr_reader :price + + sig { params(price: Integer).void } + attr_writer :price + + # Website visitor uid from tracking; links this lead to a site visit + sig { returns(T.nilable(String)) } + attr_reader :visitor_uid + + sig { params(visitor_uid: String).void } + attr_writer :visitor_uid + + sig do + params( + _embedded: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::OrHash, + custom_fields_values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::OrHash + ], + name: String, + price: Integer, + visitor_uid: String + ).returns(T.attached_class) + end + def self.new( + _embedded: nil, + # Custom fields payload (same as lead create API) + custom_fields_values: nil, + name: nil, + price: nil, + # Website visitor uid from tracking; links this lead to a site visit + visitor_uid: nil + ) + end + + sig do + override.returns( + { + _embedded: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded, + custom_fields_values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue + ], + name: String, + price: Integer, + visitor_uid: String + } + ) + end + def to_hash + end + + class Embedded < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded, + Amocrm::Internal::AnyHash + ) + end + + # Tags to attach + sig do + returns( + T.nilable( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag + ] + ) + ) + end + attr_reader :tags + + sig do + params( + tags: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag::OrHash + ] + ).void + end + attr_writer :tags + + sig do + params( + tags: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag::OrHash + ] + ).returns(T.attached_class) + end + def self.new( + # Tags to attach + tags: nil + ) + end + + sig do + override.returns( + { + tags: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag + ] + } + ) + end + def to_hash + end + + class Tag < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(String)) } + attr_reader :name + + sig { params(name: String).void } + attr_writer :name + + sig do + params(id: Integer, name: String).returns( + T.attached_class + ) + end + def self.new(id: nil, name: nil) + end + + sig { override.returns({ id: Integer, name: String }) } + def to_hash + end + end + end + + class CustomFieldsValue < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue, + Amocrm::Internal::AnyHash + ) + end + + # Values for the field (multiple values allowed) + sig do + returns( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value + ] + ) + end + attr_accessor :values + + # Field code (e.g. PHONE/EMAIL) + sig { returns(T.nilable(String)) } + attr_reader :field_code + + sig { params(field_code: String).void } + attr_writer :field_code + + # Use either field_id or field_code (e.g. PHONE/EMAIL) + sig { returns(T.nilable(Integer)) } + attr_reader :field_id + + sig { params(field_id: Integer).void } + attr_writer :field_id + + sig do + params( + values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::OrHash + ], + field_code: String, + field_id: Integer + ).returns(T.attached_class) + end + def self.new( + # Values for the field (multiple values allowed) + values:, + # Field code (e.g. PHONE/EMAIL) + field_code: nil, + # Use either field_id or field_code (e.g. PHONE/EMAIL) + field_id: nil + ) + end + + sig do + override.returns( + { + values: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value + ], + field_code: String, + field_id: Integer + } + ) + end + def to_hash + end + + class Value < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::Value::Variants + ) + end + attr_accessor :value + + # Option code for list/select custom fields (if you use code instead of id) + sig { returns(T.nilable(String)) } + attr_reader :enum_code + + sig { params(enum_code: String).void } + attr_writer :enum_code + + # Option id for list/select custom fields (one of predefined options) + sig { returns(T.nilable(Integer)) } + attr_reader :enum_id + + sig { params(enum_id: Integer).void } + attr_writer :enum_id + + sig do + params( + value: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::Value::Variants, + enum_code: String, + enum_id: Integer + ).returns(T.attached_class) + end + def self.new( + value:, + # Option code for list/select custom fields (if you use code instead of id) + enum_code: nil, + # Option id for list/select custom fields (one of predefined options) + enum_id: nil + ) + end + + sig do + override.returns( + { + value: + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::Value::Variants, + enum_code: String, + enum_id: Integer + } + ) + end + def to_hash + end + + module Value + extend Amocrm::Internal::Type::Union + + Variants = + T.type_alias { T.any(String, Integer, T::Boolean) } + + sig do + override.returns( + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::Value::Variants + ] + ) + end + def self.variants + end + end + end + end + end + end + end + end + end + end + end +end diff --git a/rbi/amocrm/models/v4/leads/unsorted_create_forms_response.rbi b/rbi/amocrm/models/v4/leads/unsorted_create_forms_response.rbi new file mode 100644 index 0000000..4dc61bd --- /dev/null +++ b/rbi/amocrm/models/v4/leads/unsorted_create_forms_response.rbi @@ -0,0 +1,502 @@ +# typed: strong + +module Amocrm + module Models + module V4 + module Leads + module UnsortedCreateFormsResponse + extend Amocrm::Internal::Type::Union + + Variants = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse, + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::Problem + ) + end + + class UnsortedCreateResponse < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded + ) + ) + end + attr_reader :_embedded + + sig do + params( + _embedded: + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::OrHash + ).void + end + attr_writer :_embedded + + sig { returns(T.nilable(Integer)) } + attr_reader :_total_items + + sig { params(_total_items: Integer).void } + attr_writer :_total_items + + sig do + params( + _embedded: + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::OrHash, + _total_items: Integer + ).returns(T.attached_class) + end + def self.new(_embedded: nil, _total_items: nil) + end + + sig do + override.returns( + { + _embedded: + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded, + _total_items: Integer + } + ) + end + def to_hash + end + + class Embedded < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted + ] + ) + ) + end + attr_reader :unsorted + + sig do + params( + unsorted: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::OrHash + ] + ).void + end + attr_writer :unsorted + + sig do + params( + unsorted: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::OrHash + ] + ).returns(T.attached_class) + end + def self.new(unsorted: nil) + end + + sig do + override.returns( + { + unsorted: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted + ] + } + ) + end + def to_hash + end + + class Unsorted < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded + ) + ) + end + attr_reader :_embedded + + sig do + params( + _embedded: + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::OrHash + ).void + end + attr_writer :_embedded + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig { returns(T.nilable(Integer)) } + attr_reader :account_id + + sig { params(account_id: Integer).void } + attr_writer :account_id + + # Echoed request id + sig { returns(T.nilable(String)) } + attr_reader :request_id + + sig { params(request_id: String).void } + attr_writer :request_id + + sig { returns(T.nilable(String)) } + attr_reader :uid + + sig { params(uid: String).void } + attr_writer :uid + + sig do + params( + _embedded: + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::OrHash, + _links: T.anything, + account_id: Integer, + request_id: String, + uid: String + ).returns(T.attached_class) + end + def self.new( + _embedded: nil, + _links: nil, + account_id: nil, + # Echoed request id + request_id: nil, + uid: nil + ) + end + + sig do + override.returns( + { + _embedded: + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded, + _links: T.anything, + account_id: Integer, + request_id: String, + uid: String + } + ) + end + def to_hash + end + + class Embedded < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company + ] + ) + ) + end + attr_reader :companies + + sig do + params( + companies: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company::OrHash + ] + ).void + end + attr_writer :companies + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact + ] + ) + ) + end + attr_reader :contacts + + sig do + params( + contacts: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact::OrHash + ] + ).void + end + attr_writer :contacts + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead + ] + ) + ) + end + attr_reader :leads + + sig do + params( + leads: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead::OrHash + ] + ).void + end + attr_writer :leads + + sig do + params( + companies: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company::OrHash + ], + contacts: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact::OrHash + ], + leads: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead::OrHash + ] + ).returns(T.attached_class) + end + def self.new(companies: nil, contacts: nil, leads: nil) + end + + sig do + override.returns( + { + companies: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company + ], + contacts: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact + ], + leads: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead + ] + } + ) + end + def to_hash + end + + class Company < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig do + params(id: Integer, _links: T.anything).returns( + T.attached_class + ) + end + def self.new(id: nil, _links: nil) + end + + sig do + override.returns({ id: Integer, _links: T.anything }) + end + def to_hash + end + end + + class Contact < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig do + params(id: Integer, _links: T.anything).returns( + T.attached_class + ) + end + def self.new(id: nil, _links: nil) + end + + sig do + override.returns({ id: Integer, _links: T.anything }) + end + def to_hash + end + end + + class Lead < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig do + params(id: Integer, _links: T.anything).returns( + T.attached_class + ) + end + def self.new(id: nil, _links: nil) + end + + sig do + override.returns({ id: Integer, _links: T.anything }) + end + def to_hash + end + end + end + end + end + end + + class Problem < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::Problem, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(String)) } + attr_reader :detail + + sig { params(detail: String).void } + attr_writer :detail + + sig { returns(T.nilable(Integer)) } + attr_reader :status + + sig { params(status: Integer).void } + attr_writer :status + + sig { returns(T.nilable(String)) } + attr_reader :title + + sig { params(title: String).void } + attr_writer :title + + sig { returns(T.nilable(String)) } + attr_reader :type + + sig { params(type: String).void } + attr_writer :type + + sig do + params( + detail: String, + status: Integer, + title: String, + type: String + ).returns(T.attached_class) + end + def self.new(detail: nil, status: nil, title: nil, type: nil) + end + + sig do + override.returns( + { detail: String, status: Integer, title: String, type: String } + ) + end + def to_hash + end + end + + sig do + override.returns( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::Variants + ] + ) + end + def self.variants + end + end + end + end + end +end diff --git a/rbi/amocrm/models/v4/leads/unsorted_decline_params.rbi b/rbi/amocrm/models/v4/leads/unsorted_decline_params.rbi new file mode 100644 index 0000000..6feab8f --- /dev/null +++ b/rbi/amocrm/models/v4/leads/unsorted_decline_params.rbi @@ -0,0 +1,50 @@ +# typed: strong + +module Amocrm + module Models + module V4 + module Leads + class UnsortedDeclineParams < Amocrm::Internal::Type::BaseModel + extend Amocrm::Internal::Type::RequestParameters::Converter + include Amocrm::Internal::Type::RequestParameters + + OrHash = + T.type_alias do + T.any( + Amocrm::V4::Leads::UnsortedDeclineParams, + Amocrm::Internal::AnyHash + ) + end + + # User id on whose behalf the item is declined + sig { returns(T.nilable(Integer)) } + attr_reader :user_id + + sig { params(user_id: Integer).void } + attr_writer :user_id + + sig do + params( + user_id: Integer, + request_options: Amocrm::RequestOptions::OrHash + ).returns(T.attached_class) + end + def self.new( + # User id on whose behalf the item is declined + user_id: nil, + request_options: {} + ) + end + + sig do + override.returns( + { user_id: Integer, request_options: Amocrm::RequestOptions } + ) + end + def to_hash + end + end + end + end + end +end diff --git a/rbi/amocrm/models/v4/leads/unsorted_decline_response.rbi b/rbi/amocrm/models/v4/leads/unsorted_decline_response.rbi new file mode 100644 index 0000000..cc5bb97 --- /dev/null +++ b/rbi/amocrm/models/v4/leads/unsorted_decline_response.rbi @@ -0,0 +1,441 @@ +# typed: strong + +module Amocrm + module Models + module V4 + module Leads + module UnsortedDeclineResponse + extend Amocrm::Internal::Type::Union + + Variants = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse, + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::Problem + ) + end + + class UnsortedAcceptResponse < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded + ) + ) + end + attr_reader :_embedded + + sig do + params( + _embedded: + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::OrHash + ).void + end + attr_writer :_embedded + + sig do + returns( + T.nilable( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + ) + end + attr_reader :category + + sig do + params( + category: + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category::OrSymbol + ).void + end + attr_writer :category + + sig { returns(T.nilable(Integer)) } + attr_reader :created_at + + sig { params(created_at: Integer).void } + attr_writer :created_at + + sig { returns(T.nilable(Integer)) } + attr_reader :pipeline_id + + sig { params(pipeline_id: Integer).void } + attr_writer :pipeline_id + + sig { returns(T.nilable(String)) } + attr_reader :uid + + sig { params(uid: String).void } + attr_writer :uid + + sig do + params( + _embedded: + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::OrHash, + category: + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category::OrSymbol, + created_at: Integer, + pipeline_id: Integer, + uid: String + ).returns(T.attached_class) + end + def self.new( + _embedded: nil, + category: nil, + created_at: nil, + pipeline_id: nil, + uid: nil + ) + end + + sig do + override.returns( + { + _embedded: + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded, + category: + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category::TaggedSymbol, + created_at: Integer, + pipeline_id: Integer, + uid: String + } + ) + end + def to_hash + end + + class Embedded < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded, + Amocrm::Internal::AnyHash + ) + end + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company + ] + ) + ) + end + attr_reader :companies + + sig do + params( + companies: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company::OrHash + ] + ).void + end + attr_writer :companies + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact + ] + ) + ) + end + attr_reader :contacts + + sig do + params( + contacts: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact::OrHash + ] + ).void + end + attr_writer :contacts + + sig do + returns( + T.nilable( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead + ] + ) + ) + end + attr_reader :leads + + sig do + params( + leads: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead::OrHash + ] + ).void + end + attr_writer :leads + + sig do + params( + companies: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company::OrHash + ], + contacts: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact::OrHash + ], + leads: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead::OrHash + ] + ).returns(T.attached_class) + end + def self.new(companies: nil, contacts: nil, leads: nil) + end + + sig do + override.returns( + { + companies: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company + ], + contacts: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact + ], + leads: + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead + ] + } + ) + end + def to_hash + end + + class Company < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig do + params(id: Integer, _links: T.anything).returns( + T.attached_class + ) + end + def self.new(id: nil, _links: nil) + end + + sig { override.returns({ id: Integer, _links: T.anything }) } + def to_hash + end + end + + class Contact < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig do + params(id: Integer, _links: T.anything).returns( + T.attached_class + ) + end + def self.new(id: nil, _links: nil) + end + + sig { override.returns({ id: Integer, _links: T.anything }) } + def to_hash + end + end + + class Lead < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :id + + sig { params(id: Integer).void } + attr_writer :id + + sig { returns(T.nilable(T.anything)) } + attr_reader :_links + + sig { params(_links: T.anything).void } + attr_writer :_links + + sig do + params(id: Integer, _links: T.anything).returns( + T.attached_class + ) + end + def self.new(id: nil, _links: nil) + end + + sig { override.returns({ id: Integer, _links: T.anything }) } + def to_hash + end + end + end + + module Category + extend Amocrm::Internal::Type::Enum + + TaggedSymbol = + T.type_alias do + T.all( + Symbol, + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category + ) + end + OrSymbol = T.type_alias { T.any(Symbol, String) } + + SIP = + T.let( + :sip, + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + MAIL = + T.let( + :mail, + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + CHATS = + T.let( + :chats, + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + FORMS = + T.let( + :forms, + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ) + + sig do + override.returns( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Category::TaggedSymbol + ] + ) + end + def self.values + end + end + end + + class Problem < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::Problem, + Amocrm::Internal::AnyHash + ) + end + + sig { returns(T.nilable(String)) } + attr_reader :detail + + sig { params(detail: String).void } + attr_writer :detail + + sig { returns(T.nilable(Integer)) } + attr_reader :status + + sig { params(status: Integer).void } + attr_writer :status + + sig { returns(T.nilable(String)) } + attr_reader :title + + sig { params(title: String).void } + attr_writer :title + + sig { returns(T.nilable(String)) } + attr_reader :type + + sig { params(type: String).void } + attr_writer :type + + sig do + params( + detail: String, + status: Integer, + title: String, + type: String + ).returns(T.attached_class) + end + def self.new(detail: nil, status: nil, title: nil, type: nil) + end + + sig do + override.returns( + { detail: String, status: Integer, title: String, type: String } + ) + end + def to_hash + end + end + + sig do + override.returns( + T::Array[ + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::Variants + ] + ) + end + def self.variants + end + end + end + end + end +end diff --git a/rbi/amocrm/request_options.rbi b/rbi/amocrm/request_options.rbi new file mode 100644 index 0000000..069a5ee --- /dev/null +++ b/rbi/amocrm/request_options.rbi @@ -0,0 +1,55 @@ +# typed: strong + +module Amocrm + # Specify HTTP behaviour to use for a specific request. These options supplement + # or override those provided at the client level. + # + # When making a request, you can pass an actual {RequestOptions} instance, or + # simply pass a Hash with symbol keys matching the attributes on this class. + class RequestOptions < Amocrm::Internal::Type::BaseModel + OrHash = + T.type_alias { T.any(Amocrm::RequestOptions, Amocrm::Internal::AnyHash) } + + # @api private + sig { params(opts: Amocrm::RequestOptions::OrHash).void } + def self.validate!(opts) + end + + # Idempotency key to send with request and all associated retries. Will only be + # sent for write requests. + sig { returns(T.nilable(String)) } + attr_accessor :idempotency_key + + # Extra query params to send with the request. These are `.merge`’d into any + # `query` given at the client level. + sig do + returns( + T.nilable(T::Hash[String, T.nilable(T.any(T::Array[String], String))]) + ) + end + attr_accessor :extra_query + + # Extra headers to send with the request. These are `.merged`’d into any + # `extra_headers` given at the client level. + sig { returns(T.nilable(T::Hash[String, T.nilable(String)])) } + attr_accessor :extra_headers + + # Extra data to send with the request. These are deep merged into any data + # generated as part of the normal request. + sig { returns(T.nilable(T.anything)) } + attr_accessor :extra_body + + # Maximum number of retries to attempt after a failed initial request. + sig { returns(T.nilable(Integer)) } + attr_accessor :max_retries + + # Request timeout in seconds. + sig { returns(T.nilable(Float)) } + attr_accessor :timeout + + # Returns a new instance of RequestOptions. + sig { params(values: Amocrm::Internal::AnyHash).returns(T.attached_class) } + def self.new(values = {}) + end + end +end diff --git a/rbi/amocrm/resources/v4.rbi b/rbi/amocrm/resources/v4.rbi new file mode 100644 index 0000000..6ba0998 --- /dev/null +++ b/rbi/amocrm/resources/v4.rbi @@ -0,0 +1,15 @@ +# typed: strong + +module Amocrm + module Resources + class V4 + sig { returns(Amocrm::Resources::V4::Leads) } + attr_reader :leads + + # @api private + sig { params(client: Amocrm::Client).returns(T.attached_class) } + def self.new(client:) + end + end + end +end diff --git a/rbi/amocrm/resources/v4/leads.rbi b/rbi/amocrm/resources/v4/leads.rbi new file mode 100644 index 0000000..76b5782 --- /dev/null +++ b/rbi/amocrm/resources/v4/leads.rbi @@ -0,0 +1,17 @@ +# typed: strong + +module Amocrm + module Resources + class V4 + class Leads + sig { returns(Amocrm::Resources::V4::Leads::Unsorted) } + attr_reader :unsorted + + # @api private + sig { params(client: Amocrm::Client).returns(T.attached_class) } + def self.new(client:) + end + end + end + end +end diff --git a/rbi/amocrm/resources/v4/leads/unsorted.rbi b/rbi/amocrm/resources/v4/leads/unsorted.rbi new file mode 100644 index 0000000..548618e --- /dev/null +++ b/rbi/amocrm/resources/v4/leads/unsorted.rbi @@ -0,0 +1,67 @@ +# typed: strong + +module Amocrm + module Resources + class V4 + class Leads + class Unsorted + sig do + params( + uid: String, + status_id: Integer, + user_id: Integer, + request_options: Amocrm::RequestOptions::OrHash + ).returns( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Variants + ) + end + def accept( + uid, + # Status id for the created lead + status_id: nil, + # User id on whose behalf the item is accepted + user_id: nil, + request_options: {} + ) + end + + sig do + params( + body: + T::Array[ + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::OrHash + ], + request_options: Amocrm::RequestOptions::OrHash + ).returns( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::Variants + ) + end + def create_forms(body:, request_options: {}) + end + + sig do + params( + uid: String, + user_id: Integer, + request_options: Amocrm::RequestOptions::OrHash + ).returns( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::Variants + ) + end + def decline( + uid, + # User id on whose behalf the item is declined + user_id: nil, + request_options: {} + ) + end + + # @api private + sig { params(client: Amocrm::Client).returns(T.attached_class) } + def self.new(client:) + end + end + end + end + end +end diff --git a/rbi/amocrm/version.rbi b/rbi/amocrm/version.rbi new file mode 100644 index 0000000..2e45b9a --- /dev/null +++ b/rbi/amocrm/version.rbi @@ -0,0 +1,5 @@ +# typed: strong + +module Amocrm + VERSION = T.let(T.unsafe(nil), String) +end diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..ef303e7 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,70 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "ruby", + "version-file": "lib/amocrm/version.rb", + "extra-files": [ + { + "type": "ruby-readme", + "path": "README.md" + } + ] +} \ No newline at end of file diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 0000000..3487864 --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -e + +cd -- "$(dirname -- "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then + brew bundle check >/dev/null 2>&1 || { + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo + } +fi + +echo "==> Installing Ruby dependencies…" + +exec -- bundle install "$@" diff --git a/scripts/fast-format b/scripts/fast-format new file mode 100755 index 0000000..6d5973f --- /dev/null +++ b/scripts/fast-format @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo "Script started with $# arguments" +echo "Arguments: $*" +echo "Script location: $(dirname "$0")" + +cd -- "$(dirname "$0")/.." +echo "Changed to directory: $PWD" + +if [ $# -eq 0 ]; then + echo "Usage: $0 [additional-formatter-args...]" + echo "The file should contain one file path per line" + exit 1 +fi + +exec -- bundle exec rake format FORMAT_FILE="$1" diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..177d1e6 --- /dev/null +++ b/scripts/format @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +cd -- "$(dirname -- "$0")/.." + +echo "==> Running formatters" + +exec -- bundle exec rake format "$@" diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..08b0dbe --- /dev/null +++ b/scripts/lint @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +cd -- "$(dirname -- "$0")/.." + +echo "==> Running linters" + +exec -- bundle exec rake lint "$@" diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 0000000..0b28f6e --- /dev/null +++ b/scripts/mock @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" && "$1" != '--'* ]]; then + URL="$1" + shift +else + URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" +fi + +# Check if the URL is empty +if [ -z "$URL" ]; then + echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" + exit 1 +fi + +echo "==> Starting mock server with URL ${URL}" + +# Run prism mock on the given spec +if [ "$1" == "--daemon" ]; then + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + + # Wait for server to come online + echo -n "Waiting for server" + while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + echo -n "." + sleep 0.1 + done + + if grep -q "✖ fatal" ".prism.log"; then + cat .prism.log + exit 1 + fi + + echo +else + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..e0dc137 --- /dev/null +++ b/scripts/test @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -e + +cd -- "$(dirname -- "$0")/.." + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +function prism_is_running() { + curl --silent "http://localhost:4010" >/dev/null 2>&1 +} + +kill_server_on_port() { + pids=$(lsof -t -i tcp:"$1" || echo "") + if [ "$pids" != "" ]; then + kill "$pids" + echo "Stopped $pids." + fi +} + +function is_overriding_api_base_url() { + [ -n "$TEST_API_BASE_URL" ] +} + +if ! is_overriding_api_base_url && ! prism_is_running ; then + # When we exit this script, make sure to kill the background mock server process + trap 'kill_server_on_port 4010' EXIT + + # Start the dev server + ./scripts/mock --daemon +fi + +if is_overriding_api_base_url ; then + echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" + echo +elif ! prism_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" + echo -e "running against your OpenAPI spec." + echo + echo -e "To run the server, pass in the path or url of your OpenAPI" + echo -e "spec to the prism command:" + echo + echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo + + exit 1 +else + echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo +fi + +echo "==> Running tests" +bundle exec rake test "$@" diff --git a/sig/amocrm/client.rbs b/sig/amocrm/client.rbs new file mode 100644 index 0000000..f189960 --- /dev/null +++ b/sig/amocrm/client.rbs @@ -0,0 +1,24 @@ +module Amocrm + class Client < Amocrm::Internal::Transport::BaseClient + DEFAULT_MAX_RETRIES: 2 + + DEFAULT_TIMEOUT_IN_SECONDS: Float + + DEFAULT_INITIAL_RETRY_DELAY: Float + + DEFAULT_MAX_RETRY_DELAY: Float + + attr_reader api_key: String + + attr_reader v4: Amocrm::Resources::V4 + + def initialize: ( + ?api_key: String?, + ?base_url: String?, + ?max_retries: Integer, + ?timeout: Float, + ?initial_retry_delay: Float, + ?max_retry_delay: Float + ) -> void + end +end diff --git a/sig/amocrm/errors.rbs b/sig/amocrm/errors.rbs new file mode 100644 index 0000000..a48a9c8 --- /dev/null +++ b/sig/amocrm/errors.rbs @@ -0,0 +1,117 @@ +module Amocrm + module Errors + class Error < StandardError + attr_accessor cause: StandardError? + end + + class ConversionError < Amocrm::Errors::Error + def cause: -> StandardError? + + def initialize: ( + on: Class, + method: Symbol, + target: top, + value: top, + ?cause: StandardError? + ) -> void + end + + class APIError < Amocrm::Errors::Error + attr_accessor url: URI::Generic + + attr_accessor status: Integer? + + attr_accessor headers: ::Hash[String, String]? + + attr_accessor body: top? + + def initialize: ( + url: URI::Generic, + ?status: Integer?, + ?headers: ::Hash[String, String]?, + ?body: Object?, + ?request: nil, + ?response: nil, + ?message: String? + ) -> void + end + + class APIConnectionError < Amocrm::Errors::APIError + def initialize: ( + url: URI::Generic, + ?status: nil, + ?headers: ::Hash[String, String]?, + ?body: nil, + ?request: nil, + ?response: nil, + ?message: String? + ) -> void + end + + class APITimeoutError < Amocrm::Errors::APIConnectionError + def initialize: ( + url: URI::Generic, + ?status: nil, + ?headers: ::Hash[String, String]?, + ?body: nil, + ?request: nil, + ?response: nil, + ?message: String? + ) -> void + end + + class APIStatusError < Amocrm::Errors::APIError + def self.for: ( + url: URI::Generic, + status: Integer, + headers: ::Hash[String, String]?, + body: Object?, + request: nil, + response: nil, + ?message: String? + ) -> instance + + def initialize: ( + url: URI::Generic, + status: Integer, + headers: ::Hash[String, String]?, + body: Object?, + request: nil, + response: nil, + ?message: String? + ) -> void + end + + class BadRequestError < Amocrm::Errors::APIStatusError + HTTP_STATUS: 400 + end + + class AuthenticationError < Amocrm::Errors::APIStatusError + HTTP_STATUS: 401 + end + + class PermissionDeniedError < Amocrm::Errors::APIStatusError + HTTP_STATUS: 403 + end + + class NotFoundError < Amocrm::Errors::APIStatusError + HTTP_STATUS: 404 + end + + class ConflictError < Amocrm::Errors::APIStatusError + HTTP_STATUS: 409 + end + + class UnprocessableEntityError < Amocrm::Errors::APIStatusError + HTTP_STATUS: 422 + end + + class RateLimitError < Amocrm::Errors::APIStatusError + HTTP_STATUS: 429 + end + + class InternalServerError < Amocrm::Errors::APIStatusError + HTTP_STATUS: Range[Integer] + end + end +end diff --git a/sig/amocrm/file_part.rbs b/sig/amocrm/file_part.rbs new file mode 100644 index 0000000..7058817 --- /dev/null +++ b/sig/amocrm/file_part.rbs @@ -0,0 +1,21 @@ +module Amocrm + class FilePart + attr_reader content: Pathname | StringIO | IO | String + + attr_reader content_type: String? + + attr_reader filename: String? + + private def read: -> String + + def to_json: (*top a) -> String + + def to_yaml: (*top a) -> String + + def initialize: ( + Pathname | StringIO | IO | String content, + ?filename: (Pathname | String)?, + ?content_type: String? + ) -> void + end +end diff --git a/sig/amocrm/internal.rbs b/sig/amocrm/internal.rbs new file mode 100644 index 0000000..8ade7e1 --- /dev/null +++ b/sig/amocrm/internal.rbs @@ -0,0 +1,9 @@ +module Amocrm + module Internal + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + type file_input = Pathname | StringIO | IO | String | Amocrm::FilePart + + OMIT: Object + end +end diff --git a/sig/amocrm/internal/transport/base_client.rbs b/sig/amocrm/internal/transport/base_client.rbs new file mode 100644 index 0000000..e057cad --- /dev/null +++ b/sig/amocrm/internal/transport/base_client.rbs @@ -0,0 +1,131 @@ +module Amocrm + module Internal + module Transport + class BaseClient + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + type request_components = + { + method: Symbol, + path: String | ::Array[String], + query: ::Hash[String, (::Array[String] | String)?]?, + headers: ::Hash[String, (String + | Integer + | ::Array[(String | Integer)?])?]?, + body: top?, + unwrap: (Symbol + | Integer + | ::Array[(Symbol | Integer)] + | (^(top arg0) -> top))?, + page: Class?, + stream: Class?, + model: Amocrm::Internal::Type::Converter::input?, + options: Amocrm::request_opts? + } + type request_input = + { + method: Symbol, + url: URI::Generic, + headers: ::Hash[String, String], + body: top, + max_retries: Integer, + timeout: Float + } + + MAX_REDIRECTS: 20 + + PLATFORM_HEADERS: ::Hash[String, String] + + def self.validate!: ( + Amocrm::Internal::Transport::BaseClient::request_components req + ) -> void + + def self.should_retry?: ( + Integer status, + headers: ::Hash[String, String] + ) -> bool + + def self.follow_redirect: ( + Amocrm::Internal::Transport::BaseClient::request_input request, + status: Integer, + response_headers: ::Hash[String, String] + ) -> Amocrm::Internal::Transport::BaseClient::request_input + + def self.reap_connection!: ( + Integer | Amocrm::Errors::APIConnectionError status, + stream: Enumerable[String]? + ) -> void + + attr_reader base_url: URI::Generic + + attr_reader timeout: Float + + attr_reader max_retries: Integer + + attr_reader initial_retry_delay: Float + + attr_reader max_retry_delay: Float + + attr_reader headers: ::Hash[String, String] + + attr_reader idempotency_header: String? + + # @api private + attr_reader requester: Amocrm::Internal::Transport::PooledNetRequester + + def initialize: ( + base_url: String, + ?timeout: Float, + ?max_retries: Integer, + ?initial_retry_delay: Float, + ?max_retry_delay: Float, + ?headers: ::Hash[String, (String + | Integer + | ::Array[(String | Integer)?])?], + ?idempotency_header: String? + ) -> void + + private def user_agent: -> String + + private def generate_idempotency_key: -> String + + private def build_request: ( + Amocrm::Internal::Transport::BaseClient::request_components req, + Amocrm::request_options opts + ) -> Amocrm::Internal::Transport::BaseClient::request_input + + private def retry_delay: ( + ::Hash[String, String] headers, + retry_count: Integer + ) -> Float + + def send_request: ( + Amocrm::Internal::Transport::BaseClient::request_input request, + redirect_count: Integer, + retry_count: Integer, + send_retry_header: bool + ) -> [Integer, top, Enumerable[String]] + + def request: ( + Symbol method, + String | ::Array[String] path, + ?query: ::Hash[String, (::Array[String] | String)?]?, + ?headers: ::Hash[String, (String + | Integer + | ::Array[(String | Integer)?])?]?, + ?body: top?, + ?unwrap: (Symbol + | Integer + | ::Array[(Symbol | Integer)] + | (^(top arg0) -> top))?, + ?page: Class?, + ?stream: Class?, + ?model: Amocrm::Internal::Type::Converter::input?, + ?options: Amocrm::request_opts? + ) -> top + + def inspect: -> String + end + end + end +end diff --git a/sig/amocrm/internal/transport/pooled_net_requester.rbs b/sig/amocrm/internal/transport/pooled_net_requester.rbs new file mode 100644 index 0000000..43f4d8f --- /dev/null +++ b/sig/amocrm/internal/transport/pooled_net_requester.rbs @@ -0,0 +1,48 @@ +module Amocrm + module Internal + module Transport + class PooledNetRequester + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + type request = + { + method: Symbol, + url: URI::Generic, + headers: ::Hash[String, String], + body: top, + deadline: Float + } + + KEEP_ALIVE_TIMEOUT: 30 + + DEFAULT_MAX_CONNECTIONS: Integer + + def self.connect: ( + cert_store: OpenSSL::X509::Store, + url: URI::Generic + ) -> top + + def self.calibrate_socket_timeout: (top conn, Float deadline) -> void + + def self.build_request: ( + Amocrm::Internal::Transport::PooledNetRequester::request request + ) { + (String arg0) -> void + } -> [top, (^-> void)] + + private def with_pool: ( + URI::Generic url, + deadline: Float + ) { + (top arg0) -> void + } -> void + + def execute: ( + Amocrm::Internal::Transport::PooledNetRequester::request request + ) -> [Integer, top, Enumerable[String]] + + def initialize: (?size: Integer) -> void + end + end + end +end diff --git a/sig/amocrm/internal/type/array_of.rbs b/sig/amocrm/internal/type/array_of.rbs new file mode 100644 index 0000000..af70a54 --- /dev/null +++ b/sig/amocrm/internal/type/array_of.rbs @@ -0,0 +1,48 @@ +module Amocrm + module Internal + module Type + class ArrayOf[Elem] + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + def self.[]: ( + ::Hash[Symbol, top] + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input type_info, + ?::Hash[Symbol, top] spec + ) -> instance + + def ===: (top other) -> bool + + def ==: (top other) -> bool + + def hash: -> Integer + + def coerce: ( + ::Array[top] | top value, + state: Amocrm::Internal::Type::Converter::coerce_state + ) -> (::Array[top] | top) + + def dump: ( + ::Array[top] | top value, + state: Amocrm::Internal::Type::Converter::dump_state + ) -> (::Array[top] | top) + + def to_sorbet_type: -> top + + def item_type: -> Elem + + def nilable?: -> bool + + def initialize: ( + ::Hash[Symbol, top] + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input type_info, + ?::Hash[Symbol, top] spec + ) -> void + + def inspect: (?depth: Integer) -> String + end + end + end +end diff --git a/sig/amocrm/internal/type/base_model.rbs b/sig/amocrm/internal/type/base_model.rbs new file mode 100644 index 0000000..ce465d1 --- /dev/null +++ b/sig/amocrm/internal/type/base_model.rbs @@ -0,0 +1,102 @@ +module Amocrm + module Internal + module Type + class BaseModel + extend Amocrm::Internal::Type::Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + type known_field = + { mode: (:coerce | :dump)?, required: bool, nilable: bool } + + def self.inherited: (self child) -> void + + def self.known_fields: -> ::Hash[Symbol, (Amocrm::Internal::Type::BaseModel::known_field + & { type_fn: (^-> Amocrm::Internal::Type::Converter::input) })] + + def self.fields: -> ::Hash[Symbol, (Amocrm::Internal::Type::BaseModel::known_field + & { type: Amocrm::Internal::Type::Converter::input })] + + private def self.add_field: ( + Symbol name_sym, + required: bool, + type_info: { + const: (nil | bool | Integer | Float | Symbol)?, + enum: ^-> Amocrm::Internal::Type::Converter::input?, + union: ^-> Amocrm::Internal::Type::Converter::input?, + api_name: Symbol + } + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input, + spec: ::Hash[Symbol, top] + ) -> void + + def self.required: ( + Symbol name_sym, + ::Hash[Symbol, top] + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input type_info, + ?::Hash[Symbol, top] spec + ) -> void + + def self.optional: ( + Symbol name_sym, + ::Hash[Symbol, top] + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input type_info, + ?::Hash[Symbol, top] spec + ) -> void + + private def self.request_only: { -> void } -> void + + private def self.response_only: { -> void } -> void + + def self.==: (top other) -> bool + + def self.hash: -> Integer + + def ==: (top other) -> bool + + def hash: -> Integer + + def self.coerce: ( + Amocrm::Internal::Type::BaseModel | ::Hash[top, top] | top value, + state: Amocrm::Internal::Type::Converter::coerce_state + ) -> (instance | top) + + def self.dump: ( + instance | top value, + state: Amocrm::Internal::Type::Converter::dump_state + ) -> (::Hash[top, top] | top) + + def self.to_sorbet_type: -> top + + def self.recursively_to_h: ( + Amocrm::Internal::Type::BaseModel model, + convert: bool + ) -> ::Hash[Symbol, top] + + def []: (Symbol key) -> top? + + def to_h: -> ::Hash[Symbol, top] + + alias to_hash to_h + + def deep_to_h: -> ::Hash[Symbol, top] + + def deconstruct_keys: (::Array[Symbol]? keys) -> ::Hash[Symbol, top] + + def to_json: (*top a) -> String + + def to_yaml: (*top a) -> String + + def initialize: (?::Hash[Symbol, top] | instance data) -> void + + def self.inspect: (?depth: Integer) -> String + + def to_s: -> String + + def inspect: -> String + end + end + end +end diff --git a/sig/amocrm/internal/type/base_page.rbs b/sig/amocrm/internal/type/base_page.rbs new file mode 100644 index 0000000..25c3e95 --- /dev/null +++ b/sig/amocrm/internal/type/base_page.rbs @@ -0,0 +1,24 @@ +module Amocrm + module Internal + module Type + module BasePage[Elem] + def next_page?: -> bool + + def next_page: -> instance + + def auto_paging_each: { (Elem arg0) -> void } -> void + + def to_enum: -> Enumerable[Elem] + + alias enum_for to_enum + + def initialize: ( + client: Amocrm::Internal::Transport::BaseClient, + req: Amocrm::Internal::Transport::BaseClient::request_components, + headers: ::Hash[String, String], + page_data: top + ) -> void + end + end + end +end diff --git a/sig/amocrm/internal/type/boolean.rbs b/sig/amocrm/internal/type/boolean.rbs new file mode 100644 index 0000000..4664f0c --- /dev/null +++ b/sig/amocrm/internal/type/boolean.rbs @@ -0,0 +1,26 @@ +module Amocrm + module Internal + module Type + class Boolean + extend Amocrm::Internal::Type::Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + def self.===: (top other) -> bool + + def self.==: (top other) -> bool + + def self.coerce: ( + bool | top value, + state: Amocrm::Internal::Type::Converter::coerce_state + ) -> (bool | top) + + def self.dump: ( + bool | top value, + state: Amocrm::Internal::Type::Converter::dump_state + ) -> (bool | top) + + def self.to_sorbet_type: -> top + end + end + end +end diff --git a/sig/amocrm/internal/type/converter.rbs b/sig/amocrm/internal/type/converter.rbs new file mode 100644 index 0000000..4f11aa0 --- /dev/null +++ b/sig/amocrm/internal/type/converter.rbs @@ -0,0 +1,79 @@ +module Amocrm + module Internal + module Type + module Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + type input = Amocrm::Internal::Type::Converter | Class + + type coerce_state = + { + translate_names: bool, + strictness: bool, + exactness: { yes: Integer, no: Integer, maybe: Integer }, + error: Class, + branched: Integer + } + + type dump_state = { can_retry: bool } + + def coerce: ( + top value, + state: Amocrm::Internal::Type::Converter::coerce_state + ) -> top + + def dump: ( + top value, + state: Amocrm::Internal::Type::Converter::dump_state + ) -> top + + def inspect: (?depth: Integer) -> String + + def self.type_info: ( + { + const: (nil | bool | Integer | Float | Symbol)?, + enum: ^-> Amocrm::Internal::Type::Converter::input?, + union: ^-> Amocrm::Internal::Type::Converter::input? + } + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input spec + ) -> (^-> top) + + def self.meta_info: ( + { + const: (nil | bool | Integer | Float | Symbol)?, + enum: ^-> Amocrm::Internal::Type::Converter::input?, + union: ^-> Amocrm::Internal::Type::Converter::input? + } + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input type_info, + { + const: (nil | bool | Integer | Float | Symbol)?, + enum: ^-> Amocrm::Internal::Type::Converter::input?, + union: ^-> Amocrm::Internal::Type::Converter::input? + } + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input spec + ) -> ::Hash[Symbol, top] + + def self.new_coerce_state: ( + ?translate_names: bool + ) -> Amocrm::Internal::Type::Converter::coerce_state + + def self.coerce: ( + Amocrm::Internal::Type::Converter::input target, + top value, + ?state: Amocrm::Internal::Type::Converter::coerce_state + ) -> top + + def self.dump: ( + Amocrm::Internal::Type::Converter::input target, + top value, + ?state: Amocrm::Internal::Type::Converter::dump_state + ) -> top + + def self.inspect: (top target, depth: Integer) -> String + end + end + end +end diff --git a/sig/amocrm/internal/type/enum.rbs b/sig/amocrm/internal/type/enum.rbs new file mode 100644 index 0000000..74e6093 --- /dev/null +++ b/sig/amocrm/internal/type/enum.rbs @@ -0,0 +1,32 @@ +module Amocrm + module Internal + module Type + module Enum + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + def self.values: -> ::Array[(nil | bool | Integer | Float | Symbol)] + + def ===: (top other) -> bool + + def ==: (top other) -> bool + + def hash: -> Integer + + def coerce: ( + String | Symbol | top value, + state: Amocrm::Internal::Type::Converter::coerce_state + ) -> (Symbol | top) + + def dump: ( + Symbol | top value, + state: Amocrm::Internal::Type::Converter::dump_state + ) -> (Symbol | top) + + def to_sorbet_type: -> top + + def inspect: (?depth: Integer) -> String + end + end + end +end diff --git a/sig/amocrm/internal/type/file_input.rbs b/sig/amocrm/internal/type/file_input.rbs new file mode 100644 index 0000000..c468b71 --- /dev/null +++ b/sig/amocrm/internal/type/file_input.rbs @@ -0,0 +1,25 @@ +module Amocrm + module Internal + module Type + class FileInput + extend Amocrm::Internal::Type::Converter + + def self.===: (top other) -> bool + + def self.==: (top other) -> bool + + def self.coerce: ( + StringIO | String | top value, + state: Amocrm::Internal::Type::Converter::coerce_state + ) -> (StringIO | top) + + def self.dump: ( + Pathname | StringIO | IO | String | top value, + state: Amocrm::Internal::Type::Converter::dump_state + ) -> (Pathname | StringIO | IO | String | top) + + def self.to_sorbet_type: -> top + end + end + end +end diff --git a/sig/amocrm/internal/type/hash_of.rbs b/sig/amocrm/internal/type/hash_of.rbs new file mode 100644 index 0000000..0dffd79 --- /dev/null +++ b/sig/amocrm/internal/type/hash_of.rbs @@ -0,0 +1,48 @@ +module Amocrm + module Internal + module Type + class HashOf[Elem] + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + def self.[]: ( + ::Hash[Symbol, top] + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input type_info, + ?::Hash[Symbol, top] spec + ) -> instance + + def ===: (top other) -> bool + + def ==: (top other) -> bool + + def hash: -> Integer + + def coerce: ( + ::Hash[top, top] | top value, + state: Amocrm::Internal::Type::Converter::coerce_state + ) -> (::Hash[Symbol, top] | top) + + def dump: ( + ::Hash[top, top] | top value, + state: Amocrm::Internal::Type::Converter::dump_state + ) -> (::Hash[Symbol, top] | top) + + def to_sorbet_type: -> top + + def item_type: -> Elem + + def nilable?: -> bool + + def initialize: ( + ::Hash[Symbol, top] + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input type_info, + ?::Hash[Symbol, top] spec + ) -> void + + def inspect: (?depth: Integer) -> String + end + end + end +end diff --git a/sig/amocrm/internal/type/request_parameters.rbs b/sig/amocrm/internal/type/request_parameters.rbs new file mode 100644 index 0000000..f78d74b --- /dev/null +++ b/sig/amocrm/internal/type/request_parameters.rbs @@ -0,0 +1,17 @@ +module Amocrm + module Internal + module Type + type request_parameters = { request_options: Amocrm::request_opts } + + module RequestParameters + attr_reader request_options: Amocrm::request_opts + + def request_options=: (Amocrm::request_opts) -> Amocrm::request_opts + + module Converter + def dump_request: (top params) -> [top, ::Hash[Symbol, top]] + end + end + end + end +end diff --git a/sig/amocrm/internal/type/union.rbs b/sig/amocrm/internal/type/union.rbs new file mode 100644 index 0000000..9361c0f --- /dev/null +++ b/sig/amocrm/internal/type/union.rbs @@ -0,0 +1,52 @@ +module Amocrm + module Internal + module Type + module Union + include Amocrm::Internal::Type::Converter + include Amocrm::Internal::Util::SorbetRuntimeSupport + + private def self.known_variants: -> ::Array[[Symbol?, (^-> Amocrm::Internal::Type::Converter::input), ::Hash[Symbol, top]]] + + def self.derefed_variants: -> ::Array[[Symbol?, top, ::Hash[Symbol, top]]] + + def self.variants: -> ::Array[top] + + private def self.discriminator: (Symbol property) -> void + + private def self.variant: ( + Symbol + | ::Hash[Symbol, top] + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input key, + ?::Hash[Symbol, top] + | ^-> Amocrm::Internal::Type::Converter::input + | Amocrm::Internal::Type::Converter::input spec + ) -> void + + private def self.resolve_variant: ( + top value + ) -> Amocrm::Internal::Type::Converter::input? + + def ===: (top other) -> bool + + def ==: (top other) -> bool + + def hash: -> Integer + + def coerce: ( + top value, + state: Amocrm::Internal::Type::Converter::coerce_state + ) -> top + + def dump: ( + top value, + state: Amocrm::Internal::Type::Converter::dump_state + ) -> top + + def to_sorbet_type: -> top + + def inspect: (?depth: Integer) -> String + end + end + end +end diff --git a/sig/amocrm/internal/type/unknown.rbs b/sig/amocrm/internal/type/unknown.rbs new file mode 100644 index 0000000..c5e6462 --- /dev/null +++ b/sig/amocrm/internal/type/unknown.rbs @@ -0,0 +1,26 @@ +module Amocrm + module Internal + module Type + class Unknown + extend Amocrm::Internal::Type::Converter + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + def self.===: (top other) -> bool + + def self.==: (top other) -> bool + + def self.coerce: ( + top value, + state: Amocrm::Internal::Type::Converter::coerce_state + ) -> top + + def self.dump: ( + top value, + state: Amocrm::Internal::Type::Converter::dump_state + ) -> top + + def self.to_sorbet_type: -> top + end + end + end +end diff --git a/sig/amocrm/internal/util.rbs b/sig/amocrm/internal/util.rbs new file mode 100644 index 0000000..c0b4741 --- /dev/null +++ b/sig/amocrm/internal/util.rbs @@ -0,0 +1,185 @@ +module Amocrm + module Internal + module Util + extend Amocrm::Internal::Util::SorbetRuntimeSupport + + def self?.monotonic_secs: -> Float + + def self?.walk_namespaces: ( + Module | Class ns + ) -> Enumerable[(Module | Class)] + + def self?.arch: -> String + + def self?.os: -> String + + def self?.primitive?: (top input) -> bool + + def self?.coerce_boolean: (String | bool input) -> (bool | top) + + def self?.coerce_boolean!: (String | bool input) -> bool? + + def self?.coerce_integer: (String | Integer input) -> (Integer | top) + + def self?.coerce_float: (String | Integer | Float input) -> (Float | top) + + def self?.coerce_hash: (top input) -> (::Hash[top, top] | top) + + def self?.coerce_hash!: (top input) -> ::Hash[top, top]? + + def self?.deep_merge_lr: (top lhs, top rhs, ?concat: bool) -> top + + def self?.deep_merge: ( + *::Array[top] values, + ?sentinel: top?, + ?concat: bool + ) -> top + + def self?.dig: ( + ::Hash[Symbol, top] | ::Array[top] | top data, + (Symbol + | Integer + | ::Array[(Symbol | Integer)] + | (^(top arg0) -> top))? pick + ) { + -> top? + } -> top? + + def self?.uri_origin: (URI::Generic uri) -> String + + def self?.interpolate_path: (String | ::Array[String] path) -> String + + def self?.decode_query: (String? query) -> ::Hash[String, ::Array[String]] + + def self?.encode_query: ( + ::Hash[String, (::Array[String] | String)?]? query + ) -> String? + + type parsed_uri = + { + scheme: String?, + host: String?, + port: Integer?, + path: String?, + query: ::Hash[String, ::Array[String]] + } + + def self?.parse_uri: ( + URI::Generic | String url + ) -> Amocrm::Internal::Util::parsed_uri + + def self?.unparse_uri: ( + Amocrm::Internal::Util::parsed_uri parsed + ) -> URI::Generic + + def self?.join_parsed_uri: ( + Amocrm::Internal::Util::parsed_uri lhs, + Amocrm::Internal::Util::parsed_uri rhs + ) -> URI::Generic + + def self?.normalized_headers: ( + *::Hash[String, (String + | Integer + | ::Array[(String | Integer)?])?] headers + ) -> ::Hash[String, String] + + class ReadIOAdapter + def close?: -> bool? + + def close: -> void + + private def read_enum: (Integer? max_len) -> String + + def read: (?Integer? max_len, ?String? out_string) -> String? + + def initialize: ( + String | Pathname | StringIO | Enumerable[String] src + ) { + (String arg0) -> void + } -> void + end + + def self?.writable_enum: { + (Enumerator::Yielder y) -> void + } -> Enumerable[String] + + JSON_CONTENT: Regexp + JSONL_CONTENT: Regexp + + def self?.write_multipart_content: ( + Enumerator::Yielder y, + val: top, + closing: ::Array[^-> void], + ?content_type: String? + ) -> void + + def self?.write_multipart_chunk: ( + Enumerator::Yielder y, + boundary: String, + key: Symbol | String, + val: top, + closing: ::Array[^-> void] + ) -> void + + def self?.encode_multipart_streaming: ( + top body + ) -> [String, Enumerable[String]] + + def self?.encode_content: ( + ::Hash[String, String] headers, + top body + ) -> top + + def self?.force_charset!: (String content_type, text: String) -> void + + def self?.decode_content: ( + ::Hash[String, String] headers, + stream: Enumerable[String], + ?suppress_error: bool + ) -> top + + def self?.fused_enum: ( + Enumerable[top] enum, + ?external: bool + ) { + -> void + } -> Enumerable[top] + + def self?.close_fused!: (Enumerable[top]? enum) -> void + + def self?.chain_fused: ( + Enumerable[top]? enum + ) { + (Enumerator::Yielder arg0) -> void + } -> Enumerable[top] + + type server_sent_event = + { event: String?, data: String?, id: String?, retry: Integer? } + + def self?.decode_lines: (Enumerable[String] enum) -> Enumerable[String] + + def self?.decode_sse: ( + Enumerable[String] lines + ) -> Enumerable[Amocrm::Internal::Util::server_sent_event] + + module SorbetRuntimeSupport + class MissingSorbetRuntimeError < ::RuntimeError + end + + private def sorbet_runtime_constants: -> ::Hash[Symbol, top] + + def const_missing: (Symbol name) -> void + + def sorbet_constant_defined?: (Symbol name) -> bool + + def define_sorbet_constant!: (Symbol name) { -> top } -> void + + def to_sorbet_type: -> top + + def self.to_sorbet_type: ( + Amocrm::Internal::Util::SorbetRuntimeSupport | top `type` + ) -> top + end + end + end +end diff --git a/sig/amocrm/models.rbs b/sig/amocrm/models.rbs new file mode 100644 index 0000000..279e230 --- /dev/null +++ b/sig/amocrm/models.rbs @@ -0,0 +1,3 @@ +module Amocrm + module V4 = Amocrm::Models::V4 +end diff --git a/sig/amocrm/models/v4/leads/unsorted_accept_params.rbs b/sig/amocrm/models/v4/leads/unsorted_accept_params.rbs new file mode 100644 index 0000000..c958427 --- /dev/null +++ b/sig/amocrm/models/v4/leads/unsorted_accept_params.rbs @@ -0,0 +1,36 @@ +module Amocrm + module Models + module V4 + module Leads + type unsorted_accept_params = + { status_id: Integer, user_id: Integer } + & Amocrm::Internal::Type::request_parameters + + class UnsortedAcceptParams < Amocrm::Internal::Type::BaseModel + extend Amocrm::Internal::Type::RequestParameters::Converter + include Amocrm::Internal::Type::RequestParameters + + attr_reader status_id: Integer? + + def status_id=: (Integer) -> Integer + + attr_reader user_id: Integer? + + def user_id=: (Integer) -> Integer + + def initialize: ( + ?status_id: Integer, + ?user_id: Integer, + ?request_options: Amocrm::request_opts + ) -> void + + def to_hash: -> { + status_id: Integer, + user_id: Integer, + request_options: Amocrm::RequestOptions + } + end + end + end + end +end diff --git a/sig/amocrm/models/v4/leads/unsorted_accept_response.rbs b/sig/amocrm/models/v4/leads/unsorted_accept_response.rbs new file mode 100644 index 0000000..f0d4eb0 --- /dev/null +++ b/sig/amocrm/models/v4/leads/unsorted_accept_response.rbs @@ -0,0 +1,203 @@ +module Amocrm + module Models + module V4 + module Leads + type unsorted_accept_response = + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse + | Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Problem + + module UnsortedAcceptResponse + extend Amocrm::Internal::Type::Union + + type unsorted_accept_response = + { + _embedded: Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded, + category: Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::category, + created_at: Integer, + pipeline_id: Integer, + uid: String + } + + class UnsortedAcceptResponse < Amocrm::Internal::Type::BaseModel + attr_reader _embedded: Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded? + + def _embedded=: ( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded + ) -> Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded + + attr_reader category: Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::category? + + def category=: ( + Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::category + ) -> Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::category + + attr_reader created_at: Integer? + + def created_at=: (Integer) -> Integer + + attr_reader pipeline_id: Integer? + + def pipeline_id=: (Integer) -> Integer + + attr_reader uid: String? + + def uid=: (String) -> String + + def initialize: ( + ?_embedded: Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded, + ?category: Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::category, + ?created_at: Integer, + ?pipeline_id: Integer, + ?uid: String + ) -> void + + def to_hash: -> { + _embedded: Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded, + category: Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::category, + created_at: Integer, + pipeline_id: Integer, + uid: String + } + + type embedded = + { + companies: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company], + contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact], + leads: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead] + } + + class Embedded < Amocrm::Internal::Type::BaseModel + attr_reader companies: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company]? + + def companies=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company] + + attr_reader contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact]? + + def contacts=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact] + + attr_reader leads: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead]? + + def leads=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead] + + def initialize: ( + ?companies: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company], + ?contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact], + ?leads: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead] + ) -> void + + def to_hash: -> { + companies: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Company], + contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Contact], + leads: ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::Embedded::Lead] + } + + type company = { id: Integer, _links: top } + + class Company < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader _links: top? + + def _links=: (top) -> top + + def initialize: (?id: Integer, ?_links: top) -> void + + def to_hash: -> { id: Integer, _links: top } + end + + type contact = { id: Integer, _links: top } + + class Contact < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader _links: top? + + def _links=: (top) -> top + + def initialize: (?id: Integer, ?_links: top) -> void + + def to_hash: -> { id: Integer, _links: top } + end + + type lead = { id: Integer, _links: top } + + class Lead < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader _links: top? + + def _links=: (top) -> top + + def initialize: (?id: Integer, ?_links: top) -> void + + def to_hash: -> { id: Integer, _links: top } + end + end + + type category = :sip | :mail | :chats | :forms + + module Category + extend Amocrm::Internal::Type::Enum + + SIP: :sip + MAIL: :mail + CHATS: :chats + FORMS: :forms + + def self?.values: -> ::Array[Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse::category] + end + end + + type problem = + { detail: String, status: Integer, title: String, type: String } + + class Problem < Amocrm::Internal::Type::BaseModel + attr_reader detail: String? + + def detail=: (String) -> String + + attr_reader status: Integer? + + def status=: (Integer) -> Integer + + attr_reader title: String? + + def title=: (String) -> String + + attr_reader type: String? + + def type=: (String) -> String + + def initialize: ( + ?detail: String, + ?status: Integer, + ?title: String, + ?type: String + ) -> void + + def to_hash: -> { + detail: String, + status: Integer, + title: String, + type: String + } + end + + def self?.variants: -> ::Array[Amocrm::Models::V4::Leads::unsorted_accept_response] + end + end + end + end +end diff --git a/sig/amocrm/models/v4/leads/unsorted_create_forms_params.rbs b/sig/amocrm/models/v4/leads/unsorted_create_forms_params.rbs new file mode 100644 index 0000000..fef32be --- /dev/null +++ b/sig/amocrm/models/v4/leads/unsorted_create_forms_params.rbs @@ -0,0 +1,484 @@ +module Amocrm + module Models + module V4 + module Leads + type unsorted_create_forms_params = + { body: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body] } + & Amocrm::Internal::Type::request_parameters + + class UnsortedCreateFormsParams < Amocrm::Internal::Type::BaseModel + extend Amocrm::Internal::Type::RequestParameters::Converter + include Amocrm::Internal::Type::RequestParameters + + attr_accessor body: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body] + + def initialize: ( + body: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body], + ?request_options: Amocrm::request_opts + ) -> void + + def to_hash: -> { + body: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body], + request_options: Amocrm::RequestOptions + } + + type body = + { + metadata: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata, + source_name: String, + source_uid: String, + _embedded: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded, + created_at: Integer, + pipeline_id: Integer, + request_id: String + } + + class Body < Amocrm::Internal::Type::BaseModel + attr_accessor metadata: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata + + attr_accessor source_name: String + + attr_accessor source_uid: String + + attr_reader _embedded: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded? + + def _embedded=: ( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded + ) -> Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded + + attr_reader created_at: Integer? + + def created_at=: (Integer) -> Integer + + attr_reader pipeline_id: Integer? + + def pipeline_id=: (Integer) -> Integer + + attr_reader request_id: String? + + def request_id=: (String) -> String + + def initialize: ( + metadata: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata, + source_name: String, + source_uid: String, + ?_embedded: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded, + ?created_at: Integer, + ?pipeline_id: Integer, + ?request_id: String + ) -> void + + def to_hash: -> { + metadata: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Metadata, + source_name: String, + source_uid: String, + _embedded: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded, + created_at: Integer, + pipeline_id: Integer, + request_id: String + } + + type metadata = + { + form_id: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::form_id, + form_name: String, + form_page: String, + form_sent_at: Integer, + form_type: Integer, + ip: String, + referer: String, + visitor_uid: String + } + + class Metadata < Amocrm::Internal::Type::BaseModel + attr_reader form_id: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::form_id? + + def form_id=: ( + Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::form_id + ) -> Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::form_id + + attr_reader form_name: String? + + def form_name=: (String) -> String + + attr_reader form_page: String? + + def form_page=: (String) -> String + + attr_reader form_sent_at: Integer? + + def form_sent_at=: (Integer) -> Integer + + attr_reader form_type: Integer? + + def form_type=: (Integer) -> Integer + + attr_reader ip: String? + + def ip=: (String) -> String + + attr_reader referer: String? + + def referer=: (String) -> String + + attr_reader visitor_uid: String? + + def visitor_uid=: (String) -> String + + def initialize: ( + ?form_id: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::form_id, + ?form_name: String, + ?form_page: String, + ?form_sent_at: Integer, + ?form_type: Integer, + ?ip: String, + ?referer: String, + ?visitor_uid: String + ) -> void + + def to_hash: -> { + form_id: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::form_id, + form_name: String, + form_page: String, + form_sent_at: Integer, + form_type: Integer, + ip: String, + referer: String, + visitor_uid: String + } + + type form_id = String | Integer + + module FormID + extend Amocrm::Internal::Type::Union + + def self?.variants: -> ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Metadata::form_id] + end + end + + type embedded = + { + companies: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company], + contacts: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact], + leads: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead] + } + + class Embedded < Amocrm::Internal::Type::BaseModel + attr_reader companies: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company]? + + def companies=: ( + ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company] + ) -> ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company] + + attr_reader contacts: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact]? + + def contacts=: ( + ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact] + ) -> ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact] + + attr_reader leads: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead]? + + def leads=: ( + ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead] + ) -> ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead] + + def initialize: ( + ?companies: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company], + ?contacts: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact], + ?leads: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead] + ) -> void + + def to_hash: -> { + companies: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Company], + contacts: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact], + leads: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead] + } + + type company = { name: String } + + class Company < Amocrm::Internal::Type::BaseModel + attr_reader name: String? + + def name=: (String) -> String + + def initialize: (?name: String) -> void + + def to_hash: -> { name: String } + end + + type contact = + { + custom_fields_values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue], + first_name: String, + last_name: String, + name: String + } + + class Contact < Amocrm::Internal::Type::BaseModel + attr_reader custom_fields_values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue]? + + def custom_fields_values=: ( + ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue] + ) -> ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue] + + attr_reader first_name: String? + + def first_name=: (String) -> String + + attr_reader last_name: String? + + def last_name=: (String) -> String + + attr_reader name: String? + + def name=: (String) -> String + + def initialize: ( + ?custom_fields_values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue], + ?first_name: String, + ?last_name: String, + ?name: String + ) -> void + + def to_hash: -> { + custom_fields_values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue], + first_name: String, + last_name: String, + name: String + } + + type custom_fields_value = + { + values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value], + field_code: String, + field_id: Integer + } + + class CustomFieldsValue < Amocrm::Internal::Type::BaseModel + attr_accessor values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value] + + attr_reader field_code: String? + + def field_code=: (String) -> String + + attr_reader field_id: Integer? + + def field_id=: (Integer) -> Integer + + def initialize: ( + values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value], + ?field_code: String, + ?field_id: Integer + ) -> void + + def to_hash: -> { + values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value], + field_code: String, + field_id: Integer + } + + type value = + { + value: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::value, + enum_code: String, + enum_id: Integer + } + + class Value < Amocrm::Internal::Type::BaseModel + attr_accessor value: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::value + + attr_reader enum_code: String? + + def enum_code=: (String) -> String + + attr_reader enum_id: Integer? + + def enum_id=: (Integer) -> Integer + + def initialize: ( + value: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::value, + ?enum_code: String, + ?enum_id: Integer + ) -> void + + def to_hash: -> { + value: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::value, + enum_code: String, + enum_id: Integer + } + + type value = String | Integer | bool + + module Value + extend Amocrm::Internal::Type::Union + + def self?.variants: -> ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Contact::CustomFieldsValue::Value::value] + end + end + end + end + + type lead = + { + _embedded: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded, + custom_fields_values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue], + name: String, + price: Integer, + visitor_uid: String + } + + class Lead < Amocrm::Internal::Type::BaseModel + attr_reader _embedded: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded? + + def _embedded=: ( + Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded + ) -> Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded + + attr_reader custom_fields_values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue]? + + def custom_fields_values=: ( + ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue] + ) -> ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue] + + attr_reader name: String? + + def name=: (String) -> String + + attr_reader price: Integer? + + def price=: (Integer) -> Integer + + attr_reader visitor_uid: String? + + def visitor_uid=: (String) -> String + + def initialize: ( + ?_embedded: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded, + ?custom_fields_values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue], + ?name: String, + ?price: Integer, + ?visitor_uid: String + ) -> void + + def to_hash: -> { + _embedded: Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded, + custom_fields_values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue], + name: String, + price: Integer, + visitor_uid: String + } + + type embedded = + { + tags: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag] + } + + class Embedded < Amocrm::Internal::Type::BaseModel + attr_reader tags: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag]? + + def tags=: ( + ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag] + ) -> ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag] + + def initialize: ( + ?tags: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag] + ) -> void + + def to_hash: -> { + tags: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::Embedded::Tag] + } + + type tag = { id: Integer, name: String } + + class Tag < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader name: String? + + def name=: (String) -> String + + def initialize: (?id: Integer, ?name: String) -> void + + def to_hash: -> { id: Integer, name: String } + end + end + + type custom_fields_value = + { + values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value], + field_code: String, + field_id: Integer + } + + class CustomFieldsValue < Amocrm::Internal::Type::BaseModel + attr_accessor values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value] + + attr_reader field_code: String? + + def field_code=: (String) -> String + + attr_reader field_id: Integer? + + def field_id=: (Integer) -> Integer + + def initialize: ( + values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value], + ?field_code: String, + ?field_id: Integer + ) -> void + + def to_hash: -> { + values: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value], + field_code: String, + field_id: Integer + } + + type value = + { + value: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::value, + enum_code: String, + enum_id: Integer + } + + class Value < Amocrm::Internal::Type::BaseModel + attr_accessor value: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::value + + attr_reader enum_code: String? + + def enum_code=: (String) -> String + + attr_reader enum_id: Integer? + + def enum_id=: (Integer) -> Integer + + def initialize: ( + value: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::value, + ?enum_code: String, + ?enum_id: Integer + ) -> void + + def to_hash: -> { + value: Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::value, + enum_code: String, + enum_id: Integer + } + + type value = String | Integer | bool + + module Value + extend Amocrm::Internal::Type::Union + + def self?.variants: -> ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsParams::Body::Embedded::Lead::CustomFieldsValue::Value::value] + end + end + end + end + end + end + end + end + end + end +end diff --git a/sig/amocrm/models/v4/leads/unsorted_create_forms_response.rbs b/sig/amocrm/models/v4/leads/unsorted_create_forms_response.rbs new file mode 100644 index 0000000..50a0f61 --- /dev/null +++ b/sig/amocrm/models/v4/leads/unsorted_create_forms_response.rbs @@ -0,0 +1,237 @@ +module Amocrm + module Models + module V4 + module Leads + type unsorted_create_forms_response = + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse + | Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::Problem + + module UnsortedCreateFormsResponse + extend Amocrm::Internal::Type::Union + + type unsorted_create_response = + { + _embedded: Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded, + _total_items: Integer + } + + class UnsortedCreateResponse < Amocrm::Internal::Type::BaseModel + attr_reader _embedded: Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded? + + def _embedded=: ( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded + ) -> Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded + + attr_reader _total_items: Integer? + + def _total_items=: (Integer) -> Integer + + def initialize: ( + ?_embedded: Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded, + ?_total_items: Integer + ) -> void + + def to_hash: -> { + _embedded: Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded, + _total_items: Integer + } + + type embedded = + { + unsorted: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted] + } + + class Embedded < Amocrm::Internal::Type::BaseModel + attr_reader unsorted: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted]? + + def unsorted=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted] + + def initialize: ( + ?unsorted: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted] + ) -> void + + def to_hash: -> { + unsorted: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted] + } + + type unsorted = + { + _embedded: Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded, + _links: top, + account_id: Integer, + request_id: String, + uid: String + } + + class Unsorted < Amocrm::Internal::Type::BaseModel + attr_reader _embedded: Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded? + + def _embedded=: ( + Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded + ) -> Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded + + attr_reader _links: top? + + def _links=: (top) -> top + + attr_reader account_id: Integer? + + def account_id=: (Integer) -> Integer + + attr_reader request_id: String? + + def request_id=: (String) -> String + + attr_reader uid: String? + + def uid=: (String) -> String + + def initialize: ( + ?_embedded: Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded, + ?_links: top, + ?account_id: Integer, + ?request_id: String, + ?uid: String + ) -> void + + def to_hash: -> { + _embedded: Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded, + _links: top, + account_id: Integer, + request_id: String, + uid: String + } + + type embedded = + { + companies: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company], + contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact], + leads: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead] + } + + class Embedded < Amocrm::Internal::Type::BaseModel + attr_reader companies: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company]? + + def companies=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company] + + attr_reader contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact]? + + def contacts=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact] + + attr_reader leads: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead]? + + def leads=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead] + + def initialize: ( + ?companies: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company], + ?contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact], + ?leads: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead] + ) -> void + + def to_hash: -> { + companies: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Company], + contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Contact], + leads: ::Array[Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse::Embedded::Unsorted::Embedded::Lead] + } + + type company = { id: Integer, _links: top } + + class Company < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader _links: top? + + def _links=: (top) -> top + + def initialize: (?id: Integer, ?_links: top) -> void + + def to_hash: -> { id: Integer, _links: top } + end + + type contact = { id: Integer, _links: top } + + class Contact < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader _links: top? + + def _links=: (top) -> top + + def initialize: (?id: Integer, ?_links: top) -> void + + def to_hash: -> { id: Integer, _links: top } + end + + type lead = { id: Integer, _links: top } + + class Lead < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader _links: top? + + def _links=: (top) -> top + + def initialize: (?id: Integer, ?_links: top) -> void + + def to_hash: -> { id: Integer, _links: top } + end + end + end + end + end + + type problem = + { detail: String, status: Integer, title: String, type: String } + + class Problem < Amocrm::Internal::Type::BaseModel + attr_reader detail: String? + + def detail=: (String) -> String + + attr_reader status: Integer? + + def status=: (Integer) -> Integer + + attr_reader title: String? + + def title=: (String) -> String + + attr_reader type: String? + + def type=: (String) -> String + + def initialize: ( + ?detail: String, + ?status: Integer, + ?title: String, + ?type: String + ) -> void + + def to_hash: -> { + detail: String, + status: Integer, + title: String, + type: String + } + end + + def self?.variants: -> ::Array[Amocrm::Models::V4::Leads::unsorted_create_forms_response] + end + end + end + end +end diff --git a/sig/amocrm/models/v4/leads/unsorted_decline_params.rbs b/sig/amocrm/models/v4/leads/unsorted_decline_params.rbs new file mode 100644 index 0000000..1b1e769 --- /dev/null +++ b/sig/amocrm/models/v4/leads/unsorted_decline_params.rbs @@ -0,0 +1,29 @@ +module Amocrm + module Models + module V4 + module Leads + type unsorted_decline_params = + { user_id: Integer } & Amocrm::Internal::Type::request_parameters + + class UnsortedDeclineParams < Amocrm::Internal::Type::BaseModel + extend Amocrm::Internal::Type::RequestParameters::Converter + include Amocrm::Internal::Type::RequestParameters + + attr_reader user_id: Integer? + + def user_id=: (Integer) -> Integer + + def initialize: ( + ?user_id: Integer, + ?request_options: Amocrm::request_opts + ) -> void + + def to_hash: -> { + user_id: Integer, + request_options: Amocrm::RequestOptions + } + end + end + end + end +end diff --git a/sig/amocrm/models/v4/leads/unsorted_decline_response.rbs b/sig/amocrm/models/v4/leads/unsorted_decline_response.rbs new file mode 100644 index 0000000..f7d52b7 --- /dev/null +++ b/sig/amocrm/models/v4/leads/unsorted_decline_response.rbs @@ -0,0 +1,203 @@ +module Amocrm + module Models + module V4 + module Leads + type unsorted_decline_response = + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse + | Amocrm::Models::V4::Leads::UnsortedDeclineResponse::Problem + + module UnsortedDeclineResponse + extend Amocrm::Internal::Type::Union + + type unsorted_accept_response = + { + _embedded: Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded, + category: Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::category, + created_at: Integer, + pipeline_id: Integer, + uid: String + } + + class UnsortedAcceptResponse < Amocrm::Internal::Type::BaseModel + attr_reader _embedded: Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded? + + def _embedded=: ( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded + ) -> Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded + + attr_reader category: Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::category? + + def category=: ( + Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::category + ) -> Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::category + + attr_reader created_at: Integer? + + def created_at=: (Integer) -> Integer + + attr_reader pipeline_id: Integer? + + def pipeline_id=: (Integer) -> Integer + + attr_reader uid: String? + + def uid=: (String) -> String + + def initialize: ( + ?_embedded: Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded, + ?category: Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::category, + ?created_at: Integer, + ?pipeline_id: Integer, + ?uid: String + ) -> void + + def to_hash: -> { + _embedded: Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded, + category: Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::category, + created_at: Integer, + pipeline_id: Integer, + uid: String + } + + type embedded = + { + companies: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company], + contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact], + leads: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead] + } + + class Embedded < Amocrm::Internal::Type::BaseModel + attr_reader companies: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company]? + + def companies=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company] + + attr_reader contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact]? + + def contacts=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact] + + attr_reader leads: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead]? + + def leads=: ( + ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead] + ) -> ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead] + + def initialize: ( + ?companies: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company], + ?contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact], + ?leads: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead] + ) -> void + + def to_hash: -> { + companies: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Company], + contacts: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Contact], + leads: ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::Embedded::Lead] + } + + type company = { id: Integer, _links: top } + + class Company < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader _links: top? + + def _links=: (top) -> top + + def initialize: (?id: Integer, ?_links: top) -> void + + def to_hash: -> { id: Integer, _links: top } + end + + type contact = { id: Integer, _links: top } + + class Contact < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader _links: top? + + def _links=: (top) -> top + + def initialize: (?id: Integer, ?_links: top) -> void + + def to_hash: -> { id: Integer, _links: top } + end + + type lead = { id: Integer, _links: top } + + class Lead < Amocrm::Internal::Type::BaseModel + attr_reader id: Integer? + + def id=: (Integer) -> Integer + + attr_reader _links: top? + + def _links=: (top) -> top + + def initialize: (?id: Integer, ?_links: top) -> void + + def to_hash: -> { id: Integer, _links: top } + end + end + + type category = :sip | :mail | :chats | :forms + + module Category + extend Amocrm::Internal::Type::Enum + + SIP: :sip + MAIL: :mail + CHATS: :chats + FORMS: :forms + + def self?.values: -> ::Array[Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse::category] + end + end + + type problem = + { detail: String, status: Integer, title: String, type: String } + + class Problem < Amocrm::Internal::Type::BaseModel + attr_reader detail: String? + + def detail=: (String) -> String + + attr_reader status: Integer? + + def status=: (Integer) -> Integer + + attr_reader title: String? + + def title=: (String) -> String + + attr_reader type: String? + + def type=: (String) -> String + + def initialize: ( + ?detail: String, + ?status: Integer, + ?title: String, + ?type: String + ) -> void + + def to_hash: -> { + detail: String, + status: Integer, + title: String, + type: String + } + end + + def self?.variants: -> ::Array[Amocrm::Models::V4::Leads::unsorted_decline_response] + end + end + end + end +end diff --git a/sig/amocrm/request_options.rbs b/sig/amocrm/request_options.rbs new file mode 100644 index 0000000..a23a964 --- /dev/null +++ b/sig/amocrm/request_options.rbs @@ -0,0 +1,34 @@ +module Amocrm + type request_opts = + Amocrm::RequestOptions | Amocrm::request_options | ::Hash[Symbol, top] + + type request_options = + { + idempotency_key: String?, + extra_query: ::Hash[String, (::Array[String] | String)?]?, + extra_headers: ::Hash[String, String?]?, + extra_body: top?, + max_retries: Integer?, + timeout: Float? + } + + class RequestOptions < Amocrm::Internal::Type::BaseModel + def self.validate!: (Amocrm::request_opts opts) -> void + + attr_accessor idempotency_key: String? + + attr_accessor extra_query: ::Hash[String, (::Array[String] | String)?]? + + attr_accessor extra_headers: ::Hash[String, String?]? + + attr_accessor extra_body: top? + + attr_accessor max_retries: Integer? + + attr_accessor timeout: Float? + + def initialize: ( + ?Amocrm::request_options | ::Hash[Symbol, top] values + ) -> void + end +end diff --git a/sig/amocrm/resources/v4.rbs b/sig/amocrm/resources/v4.rbs new file mode 100644 index 0000000..f71fbb5 --- /dev/null +++ b/sig/amocrm/resources/v4.rbs @@ -0,0 +1,9 @@ +module Amocrm + module Resources + class V4 + attr_reader leads: Amocrm::Resources::V4::Leads + + def initialize: (client: Amocrm::Client) -> void + end + end +end diff --git a/sig/amocrm/resources/v4/leads.rbs b/sig/amocrm/resources/v4/leads.rbs new file mode 100644 index 0000000..9183669 --- /dev/null +++ b/sig/amocrm/resources/v4/leads.rbs @@ -0,0 +1,11 @@ +module Amocrm + module Resources + class V4 + class Leads + attr_reader unsorted: Amocrm::Resources::V4::Leads::Unsorted + + def initialize: (client: Amocrm::Client) -> void + end + end + end +end diff --git a/sig/amocrm/resources/v4/leads/unsorted.rbs b/sig/amocrm/resources/v4/leads/unsorted.rbs new file mode 100644 index 0000000..1472786 --- /dev/null +++ b/sig/amocrm/resources/v4/leads/unsorted.rbs @@ -0,0 +1,29 @@ +module Amocrm + module Resources + class V4 + class Leads + class Unsorted + def accept: ( + String uid, + ?status_id: Integer, + ?user_id: Integer, + ?request_options: Amocrm::request_opts + ) -> Amocrm::Models::V4::Leads::unsorted_accept_response + + def create_forms: ( + body: ::Array[Amocrm::V4::Leads::UnsortedCreateFormsParams::Body], + ?request_options: Amocrm::request_opts + ) -> Amocrm::Models::V4::Leads::unsorted_create_forms_response + + def decline: ( + String uid, + ?user_id: Integer, + ?request_options: Amocrm::request_opts + ) -> Amocrm::Models::V4::Leads::unsorted_decline_response + + def initialize: (client: Amocrm::Client) -> void + end + end + end + end +end diff --git a/sig/amocrm/version.rbs b/sig/amocrm/version.rbs new file mode 100644 index 0000000..a23585c --- /dev/null +++ b/sig/amocrm/version.rbs @@ -0,0 +1,3 @@ +module Amocrm + VERSION: String +end diff --git a/sorbet/config b/sorbet/config new file mode 100644 index 0000000..6fe84ed --- /dev/null +++ b/sorbet/config @@ -0,0 +1,2 @@ +--dir=rbi/ +--ignore=test/ diff --git a/sorbet/rbi/.gitignore b/sorbet/rbi/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/sorbet/rbi/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/amocrm/client_test.rb b/test/amocrm/client_test.rb new file mode 100644 index 0000000..ebb3c9b --- /dev/null +++ b/test/amocrm/client_test.rb @@ -0,0 +1,331 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class AmocrmTest < Minitest::Test + extend Minitest::Serial + include WebMock::API + + def before_all + super + WebMock.enable! + end + + def setup + super + Thread.current.thread_variable_set(:mock_sleep, []) + end + + def teardown + Thread.current.thread_variable_set(:mock_sleep, nil) + WebMock.reset! + super + end + + def after_all + WebMock.disable! + super + end + + def test_raises_on_missing_non_nullable_opts + e = assert_raises(ArgumentError) do + Amocrm::Client.new + end + assert_match(/is required/, e.message) + end + + def test_client_default_request_default_retry_attempts + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json(status: 500, body: {}) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + assert_raises(Amocrm::Errors::InternalServerError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] + ) + end + + assert_requested(:any, /./, times: 3) + end + + def test_client_given_request_default_retry_attempts + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json(status: 500, body: {}) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key", max_retries: 3) + + assert_raises(Amocrm::Errors::InternalServerError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] + ) + end + + assert_requested(:any, /./, times: 4) + end + + def test_client_default_request_given_retry_attempts + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json(status: 500, body: {}) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + assert_raises(Amocrm::Errors::InternalServerError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {max_retries: 3} + ) + end + + assert_requested(:any, /./, times: 4) + end + + def test_client_given_request_given_retry_attempts + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json(status: 500, body: {}) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key", max_retries: 3) + + assert_raises(Amocrm::Errors::InternalServerError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {max_retries: 4} + ) + end + + assert_requested(:any, /./, times: 5) + end + + def test_client_retry_after_seconds + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json( + status: 500, + headers: {"retry-after" => "1.3"}, + body: {} + ) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key", max_retries: 1) + + assert_raises(Amocrm::Errors::InternalServerError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] + ) + end + + assert_requested(:any, /./, times: 2) + assert_equal(1.3, Thread.current.thread_variable_get(:mock_sleep).last) + end + + def test_client_retry_after_date + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json( + status: 500, + headers: {"retry-after" => (Time.now + 10).httpdate}, + body: {} + ) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key", max_retries: 1) + + assert_raises(Amocrm::Errors::InternalServerError) do + Thread.current.thread_variable_set(:time_now, Time.now) + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] + ) + Thread.current.thread_variable_set(:time_now, nil) + end + + assert_requested(:any, /./, times: 2) + assert_in_delta(10, Thread.current.thread_variable_get(:mock_sleep).last, 1.0) + end + + def test_client_retry_after_ms + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json( + status: 500, + headers: {"retry-after-ms" => "1300"}, + body: {} + ) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key", max_retries: 1) + + assert_raises(Amocrm::Errors::InternalServerError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] + ) + end + + assert_requested(:any, /./, times: 2) + assert_equal(1.3, Thread.current.thread_variable_get(:mock_sleep).last) + end + + def test_retry_count_header + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json(status: 500, body: {}) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + assert_raises(Amocrm::Errors::InternalServerError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] + ) + end + + 3.times do + assert_requested(:any, /./, headers: {"x-stainless-retry-count" => _1}) + end + end + + def test_omit_retry_count_header + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json(status: 500, body: {}) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + assert_raises(Amocrm::Errors::InternalServerError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {extra_headers: {"x-stainless-retry-count" => nil}} + ) + end + + assert_requested(:any, /./, times: 3) do + refute_includes(_1.headers.keys.map(&:downcase), "x-stainless-retry-count") + end + end + + def test_overwrite_retry_count_header + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json(status: 500, body: {}) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + assert_raises(Amocrm::Errors::InternalServerError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {extra_headers: {"x-stainless-retry-count" => "42"}} + ) + end + + assert_requested(:any, /./, headers: {"x-stainless-retry-count" => "42"}, times: 3) + end + + def test_client_redirect_307 + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json( + status: 307, + headers: {"location" => "/redirected"}, + body: {} + ) + stub_request(:any, "http://localhost/redirected").to_return( + status: 307, + headers: {"location" => "/redirected"} + ) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + assert_raises(Amocrm::Errors::APIConnectionError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {extra_headers: {}} + ) + end + + recorded, = WebMock::RequestRegistry.instance.requested_signatures.hash.first + + assert_requested(:any, "http://localhost/redirected", times: Amocrm::Client::MAX_REDIRECTS) do + assert_equal(recorded.method, _1.method) + assert_equal(recorded.body, _1.body) + assert_equal( + recorded.headers.transform_keys(&:downcase).fetch("content-type"), + _1.headers.transform_keys(&:downcase).fetch("content-type") + ) + end + end + + def test_client_redirect_303 + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json( + status: 303, + headers: {"location" => "/redirected"}, + body: {} + ) + stub_request(:get, "http://localhost/redirected").to_return( + status: 303, + headers: {"location" => "/redirected"} + ) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + assert_raises(Amocrm::Errors::APIConnectionError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {extra_headers: {}} + ) + end + + assert_requested(:get, "http://localhost/redirected", times: Amocrm::Client::MAX_REDIRECTS) do + headers = _1.headers.keys.map(&:downcase) + refute_includes(headers, "content-type") + assert_nil(_1.body) + end + end + + def test_client_redirect_auth_keep_same_origin + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json( + status: 307, + headers: {"location" => "/redirected"}, + body: {} + ) + stub_request(:any, "http://localhost/redirected").to_return( + status: 307, + headers: {"location" => "/redirected"} + ) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + assert_raises(Amocrm::Errors::APIConnectionError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {extra_headers: {"authorization" => "Bearer xyz"}} + ) + end + + recorded, = WebMock::RequestRegistry.instance.requested_signatures.hash.first + auth_header = recorded.headers.transform_keys(&:downcase).fetch("authorization") + + assert_equal("Bearer xyz", auth_header) + assert_requested(:any, "http://localhost/redirected", times: Amocrm::Client::MAX_REDIRECTS) do + auth_header = _1.headers.transform_keys(&:downcase).fetch("authorization") + assert_equal("Bearer xyz", auth_header) + end + end + + def test_client_redirect_auth_strip_cross_origin + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json( + status: 307, + headers: {"location" => "https://example.com/redirected"}, + body: {} + ) + stub_request(:any, "https://example.com/redirected").to_return( + status: 307, + headers: {"location" => "https://example.com/redirected"} + ) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + assert_raises(Amocrm::Errors::APIConnectionError) do + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}], + request_options: {extra_headers: {"authorization" => "Bearer xyz"}} + ) + end + + assert_requested(:any, "https://example.com/redirected", times: Amocrm::Client::MAX_REDIRECTS) do + headers = _1.headers.keys.map(&:downcase) + refute_includes(headers, "authorization") + end + end + + def test_default_headers + stub_request(:post, "http://localhost/api/v4/leads/unsorted/forms").to_return_json(status: 200, body: {}) + + amocrm = Amocrm::Client.new(base_url: "http://localhost", api_key: "My API Key") + + amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] + ) + + assert_requested(:any, /./) do |req| + headers = req.headers.transform_keys(&:downcase).fetch_values("accept", "content-type") + headers.each { refute_empty(_1) } + end + end +end diff --git a/test/amocrm/file_part_test.rb b/test/amocrm/file_part_test.rb new file mode 100644 index 0000000..5b46101 --- /dev/null +++ b/test/amocrm/file_part_test.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class Amocrm::Test::FilePartTest < Minitest::Test + def test_to_json + text = "gray" + filepart = Amocrm::FilePart.new(StringIO.new(text)) + + assert_equal(text.to_json, filepart.to_json) + assert_equal(text.to_yaml, filepart.to_yaml) + end +end diff --git a/test/amocrm/internal/sorbet_runtime_support_test.rb b/test/amocrm/internal/sorbet_runtime_support_test.rb new file mode 100644 index 0000000..6cc6535 --- /dev/null +++ b/test/amocrm/internal/sorbet_runtime_support_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +class Amocrm::Test::SorbetRuntimeSupportTest < Minitest::Test + extend Minitest::Serial + + i_suck_and_my_tests_are_order_dependent! + + module E + extend Amocrm::Internal::Type::Enum + + define_sorbet_constant!(:TaggedSymbol) { 1 } + end + + module U + extend Amocrm::Internal::Type::Union + + define_sorbet_constant!(:Variants) { 2 } + end + + class M < Amocrm::Internal::Type::BaseModel + define_sorbet_constant!(:OrHash) { 3 } + end + + def test_nil_aliases + err = Amocrm::Internal::Util::SorbetRuntimeSupport::MissingSorbetRuntimeError + + assert_raises(err) { Amocrm::Internal::AnyHash } + assert_raises(err) { Amocrm::Internal::FileInput } + assert_raises(err) { Amocrm::Internal::Type::Converter::Input } + assert_raises(err) { Amocrm::Internal::Type::Converter::CoerceState } + assert_raises(err) { Amocrm::Internal::Type::Converter::DumpState } + assert_raises(err) { Amocrm::Internal::Type::BaseModel::KnownField } + assert_raises(err) { Amocrm::Internal::Util::ParsedUri } + assert_raises(err) { Amocrm::Internal::Util::ServerSentEvent } + assert_raises(err) { Amocrm::Internal::Transport::BaseClient::RequestComponents } + assert_raises(err) { Amocrm::Internal::Transport::BaseClient::RequestInput } + assert_raises(err) { Amocrm::Internal::Transport::PooledNetRequester::Request } + assert_raises(err) { E::TaggedSymbol } + assert_raises(err) { U::Variants } + assert_raises(err) { M::OrHash } + end + + def test_stubbed_aliases + Kernel.instance_eval { const_set(:T, nil) } + + assert_equal(1, E::TaggedSymbol) + assert_equal(2, U::Variants) + assert_equal(3, M::OrHash) + end +end diff --git a/test/amocrm/internal/type/base_model_test.rb b/test/amocrm/internal/type/base_model_test.rb new file mode 100644 index 0000000..a0c6098 --- /dev/null +++ b/test/amocrm/internal/type/base_model_test.rb @@ -0,0 +1,727 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +class Amocrm::Test::PrimitiveModelTest < Minitest::Test + A = Amocrm::Internal::Type::ArrayOf[-> { Integer }] + H = Amocrm::Internal::Type::HashOf[-> { Integer }, nil?: true] + + module E + extend Amocrm::Internal::Type::Enum + end + + module U + extend Amocrm::Internal::Type::Union + end + + class B < Amocrm::Internal::Type::BaseModel + optional :a, Integer + optional :b, B + end + + def test_typing + converters = [ + Amocrm::Internal::Type::Unknown, + Amocrm::Internal::Type::Boolean, + A, + H, + E, + U, + B + ] + + converters.each do |conv| + assert_pattern do + conv => Amocrm::Internal::Type::Converter + end + end + end + + def test_coerce + cases = { + [Amocrm::Internal::Type::Unknown, :a] => [{yes: 1}, :a], + [NilClass, :a] => [{maybe: 1}, nil], + [NilClass, nil] => [{yes: 1}, nil], + [Amocrm::Internal::Type::Boolean, true] => [{yes: 1}, true], + [Amocrm::Internal::Type::Boolean, "true"] => [{no: 1}, "true"], + [Integer, 1] => [{yes: 1}, 1], + [Integer, 1.0] => [{maybe: 1}, 1], + [Integer, "1"] => [{maybe: 1}, 1], + [Integer, "one"] => [{no: 1}, "one"], + [Float, 1] => [{yes: 1}, 1.0], + [Float, "1"] => [{maybe: 1}, 1.0], + [Float, :one] => [{no: 1}, :one], + [String, :str] => [{yes: 1}, "str"], + [String, "str"] => [{yes: 1}, "str"], + [String, 1] => [{maybe: 1}, "1"], + [:a, "a"] => [{yes: 1}, :a], + [Date, "1990-09-19"] => [{yes: 1}, Date.new(1990, 9, 19)], + [Date, Date.new(1990, 9, 19)] => [{yes: 1}, Date.new(1990, 9, 19)], + [Date, "one"] => [{no: 1}, "one"], + [Time, "1990-09-19"] => [{yes: 1}, Time.new(1990, 9, 19)], + [Time, Time.new(1990, 9, 19)] => [{yes: 1}, Time.new(1990, 9, 19)], + [Time, "one"] => [{no: 1}, "one"] + } + + cases.each do |lhs, rhs| + target, input = lhs + exactness, expect = rhs + state = Amocrm::Internal::Type::Converter.new_coerce_state + assert_pattern do + Amocrm::Internal::Type::Converter.coerce(target, input, state: state) => ^expect + state.fetch(:exactness).filter { _2.nonzero? }.to_h => ^exactness + end + end + end + + def test_dump + cases = { + [Amocrm::Internal::Type::Unknown, B.new(a: "one", b: B.new(a: 1.0))] => {a: "one", b: {a: 1}}, + [A, B.new(a: "one", b: B.new(a: 1.0))] => {a: "one", b: {a: 1}}, + [H, B.new(a: "one", b: B.new(a: 1.0))] => {a: "one", b: {a: 1}}, + [E, B.new(a: "one", b: B.new(a: 1.0))] => {a: "one", b: {a: 1}}, + [U, B.new(a: "one", b: B.new(a: 1.0))] => {a: "one", b: {a: 1}}, + [B, B.new(a: "one", b: B.new(a: 1.0))] => {a: "one", b: {a: 1}}, + [String, B.new(a: "one", b: B.new(a: 1.0))] => {a: "one", b: {a: 1}}, + [:b, B.new(a: "one", b: B.new(a: 1.0))] => {a: "one", b: {a: 1}}, + [nil, B.new(a: "one", b: B.new(a: 1.0))] => {a: "one", b: {a: 1}}, + [Amocrm::Internal::Type::Boolean, true] => true, + [Amocrm::Internal::Type::Boolean, "true"] => "true", + [Integer, "1"] => "1", + [Float, 1] => 1, + [String, "one"] => "one", + [String, :one] => :one, + [:a, :b] => :b, + [:a, "a"] => "a", + [String, StringIO.new("one")] => "one", + [String, Pathname(__FILE__)] => Amocrm::FilePart + } + + cases.each do + target, input = _1 + expect = _2 + assert_pattern do + Amocrm::Internal::Type::Converter.dump(target, input) => ^expect + end + end + end + + def test_coerce_errors + cases = { + [Integer, "one"] => ArgumentError, + [Float, "one"] => ArgumentError, + [String, Time] => TypeError, + [Date, "one"] => ArgumentError, + [Time, "one"] => ArgumentError + } + + cases.each do |testcase, expect| + target, input = testcase + state = Amocrm::Internal::Type::Converter.new_coerce_state + Amocrm::Internal::Type::Converter.coerce(target, input, state: state) + assert_pattern do + state => {error: ^expect} + end + end + end + + def test_dump_retry + types = [ + Amocrm::Internal::Type::Unknown, + Amocrm::Internal::Type::Boolean, + A, + H, + E, + U, + B + ] + Pathname(__FILE__).open do |fd| + cases = [ + fd, + [fd], + {a: fd}, + {a: {b: fd}} + ] + types.product(cases).each do |target, input| + state = {can_retry: true} + Amocrm::Internal::Type::Converter.dump(target, input, state: state) + + assert_pattern do + state => {can_retry: false} + end + end + end + end +end + +class Amocrm::Test::EnumModelTest < Minitest::Test + class E0 + include Amocrm::Internal::Type::Enum + + attr_reader :values + + def initialize(*values) = (@values = values) + end + + module E1 + extend Amocrm::Internal::Type::Enum + + TRUE = true + end + + module E2 + extend Amocrm::Internal::Type::Enum + + ONE = 1 + TWO = 2 + end + + module E3 + extend Amocrm::Internal::Type::Enum + + ONE = 1.0 + TWO = 2.0 + end + + module E4 + extend Amocrm::Internal::Type::Enum + + ONE = :one + TWO = :two + end + + def test_coerce + cases = { + [E0.new, "one"] => [{no: 1}, "one"], + [E0.new(:one), "one"] => [{yes: 1}, :one], + [E0.new(:two), "one"] => [{maybe: 1}, "one"], + + [E1, true] => [{yes: 1}, true], + [E1, false] => [{no: 1}, false], + [E1, :true] => [{no: 1}, :true], + + [E2, 1] => [{yes: 1}, 1], + [E2, 1.0] => [{yes: 1}, 1], + [E2, 1.2] => [{no: 1}, 1.2], + [E2, "1"] => [{no: 1}, "1"], + + [E3, 1.0] => [{yes: 1}, 1.0], + [E3, 1] => [{yes: 1}, 1.0], + [E3, "one"] => [{no: 1}, "one"], + + [E4, :one] => [{yes: 1}, :one], + [E4, "one"] => [{yes: 1}, :one], + [E4, "1"] => [{maybe: 1}, "1"], + [E4, :"1"] => [{maybe: 1}, :"1"], + [E4, 1] => [{no: 1}, 1] + } + + cases.each do |lhs, rhs| + target, input = lhs + exactness, expect = rhs + state = Amocrm::Internal::Type::Converter.new_coerce_state + assert_pattern do + Amocrm::Internal::Type::Converter.coerce(target, input, state: state) => ^expect + state.fetch(:exactness).filter { _2.nonzero? }.to_h => ^exactness + end + end + end + + def test_dump + cases = { + [E1, true] => true, + [E1, "true"] => "true", + + [E2, 1.0] => 1.0, + [E2, 3] => 3, + [E2, "1.0"] => "1.0", + + [E3, 1.0] => 1.0, + [E3, 3] => 3, + [E3, "1.0"] => "1.0", + + [E4, :one] => :one, + [E4, "one"] => "one", + [E4, "1.0"] => "1.0" + } + + cases.each do + target, input = _1 + expect = _2 + assert_pattern do + Amocrm::Internal::Type::Converter.dump(target, input) => ^expect + end + end + end +end + +class Amocrm::Test::CollectionModelTest < Minitest::Test + A1 = Amocrm::Internal::Type::ArrayOf[-> { Integer }] + H1 = Amocrm::Internal::Type::HashOf[Integer] + + A2 = Amocrm::Internal::Type::ArrayOf[H1] + H2 = Amocrm::Internal::Type::HashOf[-> { A1 }] + + A3 = Amocrm::Internal::Type::ArrayOf[Integer, nil?: true] + H3 = Amocrm::Internal::Type::HashOf[Integer, nil?: true] + + def test_coerce + cases = { + [A1, []] => [{yes: 1}, []], + [A1, {}] => [{no: 1}, {}], + [A1, [1, 2.0]] => [{yes: 2, maybe: 1}, [1, 2]], + [A1, ["1", 2.0]] => [{yes: 1, maybe: 2}, [1, 2]], + [H1, {}] => [{yes: 1}, {}], + [H1, []] => [{no: 1}, []], + [H1, {a: 1, b: 2}] => [{yes: 3}, {a: 1, b: 2}], + [H1, {"a" => 1, "b" => 2}] => [{yes: 3}, {a: 1, b: 2}], + [H1, {[] => 1}] => [{yes: 2, no: 1}, {[] => 1}], + [H1, {a: 1.5}] => [{yes: 1, maybe: 1}, {a: 1}], + + [A2, [{}, {"a" => 1}]] => [{yes: 4}, [{}, {a: 1}]], + [A2, [{"a" => "1"}]] => [{yes: 2, maybe: 1}, [{a: 1}]], + [H2, {a: [1, 2]}] => [{yes: 4}, {a: [1, 2]}], + [H2, {"a" => ["1", 2]}] => [{yes: 3, maybe: 1}, {a: [1, 2]}], + [H2, {"a" => ["one", 2]}] => [{yes: 3, no: 1}, {a: ["one", 2]}], + + [A3, [nil, 1]] => [{yes: 3}, [nil, 1]], + [A3, [nil, "1"]] => [{yes: 2, maybe: 1}, [nil, 1]], + [H3, {a: nil, b: "1"}] => [{yes: 2, maybe: 1}, {a: nil, b: 1}], + [H3, {a: nil}] => [{yes: 2}, {a: nil}] + } + + cases.each do |lhs, rhs| + target, input = lhs + exactness, expect = rhs + state = Amocrm::Internal::Type::Converter.new_coerce_state + assert_pattern do + Amocrm::Internal::Type::Converter.coerce(target, input, state: state) => ^expect + state.fetch(:exactness).filter { _2.nonzero? }.to_h => ^exactness + end + end + end +end + +class Amocrm::Test::BaseModelTest < Minitest::Test + class M1 < Amocrm::Internal::Type::BaseModel + required :a, Integer + end + + class M2 < M1 + required :a, Time + required :b, Integer, nil?: true + optional :c, String + end + + class M3 < Amocrm::Internal::Type::BaseModel + optional :c, const: :c + required :d, const: :d + end + + class M4 < M1 + request_only do + required :a, Integer + optional :b, String + end + + response_only do + required :c, Integer + optional :d, String + end + end + + class M5 < Amocrm::Internal::Type::BaseModel + request_only do + required :c, const: :c + end + + response_only do + required :d, const: :d + end + end + + class M6 < M1 + required :a, Amocrm::Internal::Type::ArrayOf[M6] + optional :b, M6 + end + + def test_coerce + cases = { + [M1, {}] => [{yes: 1, no: 1}, {}], + [M1, :m1] => [{no: 1}, :m1], + + [M2, {}] => [{yes: 2, no: 1, maybe: 1}, {}], + [M2, {a: "1990-09-19", b: nil}] => [{yes: 4}, {a: "1990-09-19", b: nil}], + [M2, {a: "1990-09-19", b: "1"}] => [{yes: 3, maybe: 1}, {a: "1990-09-19", b: "1"}], + [M2, {a: "1990-09-19"}] => [{yes: 3, maybe: 1}, {a: "1990-09-19"}], + [M2, {a: "1990-09-19", c: nil}] => [{yes: 2, maybe: 2}, {a: "1990-09-19", c: nil}], + + [M3, {c: "c", d: "d"}] => [{yes: 3}, {c: :c, d: :d}], + [M3, {c: "d", d: "c"}] => [{yes: 1, maybe: 2}, {c: "d", d: "c"}], + + [M4, {c: 2}] => [{yes: 5}, {c: 2}], + [M4, {a: "1", c: 2}] => [{yes: 4, maybe: 1}, {a: "1", c: 2}], + [M4, {b: nil, c: 2}] => [{yes: 4, maybe: 1}, {b: nil, c: 2}], + + [M5, {}] => [{yes: 3}, {}], + [M5, {c: "c"}] => [{yes: 3}, {c: :c}], + [M5, {d: "d"}] => [{yes: 3}, {d: :d}], + [M5, {d: nil}] => [{yes: 2, no: 1}, {d: nil}], + + [M6, {a: [{a: []}]}] => [{yes: 6}, -> { _1 in {a: [M6]} }], + [M6, {b: {a: []}}] => [{yes: 4, no: 1}, -> { _1 in {b: M6} }] + } + + cases.each do |lhs, rhs| + target, input = lhs + exactness, expect = rhs + state = Amocrm::Internal::Type::Converter.new_coerce_state + assert_pattern do + coerced = Amocrm::Internal::Type::Converter.coerce(target, input, state: state) + assert_equal(coerced, coerced) + if coerced.is_a?(Amocrm::Internal::Type::BaseModel) + coerced.to_h => ^expect + else + coerced => ^expect + end + state.fetch(:exactness).filter { _2.nonzero? }.to_h => ^exactness + end + end + end + + def test_dump + cases = { + [M3, M3.new] => {d: :d}, + [M3, {}] => {d: :d}, + [M3, {d: 1}] => {d: 1}, + + [M4, M4.new(a: 1, b: "b", c: 2, d: "d")] => {a: 1, b: "b"}, + [M4, {a: 1, b: "b", c: 2, d: "d"}] => {a: 1, b: "b"}, + + [M5, M5.new] => {c: :c}, + [M5, {}] => {c: :c}, + [M5, {c: 1}] => {c: 1} + } + + cases.each do + target, input = _1 + expect = _2 + assert_pattern do + Amocrm::Internal::Type::Converter.dump(target, input) => ^expect + end + end + end + + def test_accessors + cases = { + M2.new({a: "1990-09-19", b: "1"}) => [{a: "1990-09-19", b: "1"}, {a: Time.new(1990, 9, 19), b: 1}], + M2.new(a: "one", b: "one") => [{a: "one", b: "one"}, {a: ArgumentError, b: ArgumentError}], + M2.new(a: nil, b: 2.0) => [{a: nil, b: 2.0}, {a: TypeError}], + M2.new(a: nil, b: 2.2) => [{a: nil, b: 2.2}, {a: TypeError, b: 2}], + + M3.new => [{}, {d: :d}], + M3.new(d: 1) => [{d: 1}, {d: ArgumentError}], + + M5.new => [{}, {c: :c, d: :d}] + } + + cases.each do + target = _1 + data, attributes = _2 + + assert_pattern do + target.to_h => ^data + end + + attributes.each do |accessor, expect| + case expect + in Class if expect <= StandardError + tap do + target.public_send(accessor) + flunk + rescue Amocrm::Errors::ConversionError => e + assert_kind_of(expect, e.cause) + end + else + assert_pattern { target.public_send(accessor) => ^expect } + end + end + end + end + + def test_inplace_modification + m1 = M6.new(a: []) + m1.a << M6.new(a: []) + + m2 = M6.new(b: M6.new(a: [])) + m2.b.a << M6.new(a: []) + + m3 = M6.new(a: []) + m4 = M6.new(b: m3) + m3.a << M6.new(a: []) + + assert_pattern do + m1 => {a: [{a: []}]} + m2 => {b: {a: [{a: []}]}} + m4 => {b: {a: [{a: []}]}} + end + end +end + +class Amocrm::Test::UnionTest < Minitest::Test + class U0 + include Amocrm::Internal::Type::Union + + def initialize(*variants) = variants.each { variant(_1) } + end + + module U1 + extend Amocrm::Internal::Type::Union + + variant const: :a + variant const: 2 + end + + class M1 < Amocrm::Internal::Type::BaseModel + required :t, const: :a, api_name: :type + optional :c, String + end + + class M2 < Amocrm::Internal::Type::BaseModel + required :type, const: :b + optional :c, String + end + + module U2 + extend Amocrm::Internal::Type::Union + + discriminator :type + + variant :a, M1 + variant :b, M2 + end + + module U3 + extend Amocrm::Internal::Type::Union + + discriminator :type + + variant :a, M1 + variant String + end + + module U4 + extend Amocrm::Internal::Type::Union + + discriminator :type + + variant String + variant :a, M1 + end + + class M3 < Amocrm::Internal::Type::BaseModel + optional :recur, -> { U5 } + required :a, Integer + end + + class M4 < Amocrm::Internal::Type::BaseModel + optional :recur, -> { U5 } + required :a, Amocrm::Internal::Type::ArrayOf[-> { U5 }] + end + + class M5 < Amocrm::Internal::Type::BaseModel + optional :recur, -> { U5 } + required :b, Amocrm::Internal::Type::ArrayOf[-> { U5 }] + end + + module U5 + extend Amocrm::Internal::Type::Union + + variant -> { M3 } + variant -> { M4 } + end + + module U6 + extend Amocrm::Internal::Type::Union + + variant -> { M3 } + variant -> { M5 } + end + + def test_accessors + model = M3.new(recur: []) + tap do + model.recur + flunk + rescue Amocrm::Errors::ConversionError => e + assert_kind_of(ArgumentError, e.cause) + end + end + + 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], + + [U2, {type: :a}] => [{yes: 3}, 0, {t: :a}], + [U2, {type: "b"}] => [{yes: 3}, 0, {type: :b}], + + [U3, "one"] => [{yes: 1}, 2, "one"], + [U4, "one"] => [{yes: 1}, 1, "one"], + + [U5, {a: []}] => [{yes: 3}, 2, {a: []}], + [U6, {b: []}] => [{yes: 3}, 2, {b: []}], + + [U5, {a: [{a: []}]}] => [{yes: 6}, 4, {a: [M4.new(a: [])]}], + [U5, {a: [{a: [{a: []}]}]}] => [{yes: 9}, 6, {a: [M4.new(a: [M4.new(a: [])])]}] + } + + cases.each do |lhs, rhs| + target, input = lhs + exactness, branched, expect = rhs + state = Amocrm::Internal::Type::Converter.new_coerce_state + assert_pattern do + coerced = Amocrm::Internal::Type::Converter.coerce(target, input, state: state) + assert_equal(coerced, coerced) + if coerced.is_a?(Amocrm::Internal::Type::BaseModel) + coerced.to_h => ^expect + else + coerced => ^expect + end + state.fetch(:exactness).filter { _2.nonzero? }.to_h => ^exactness + state => {branched: ^branched} + end + end + end +end + +class Amocrm::Test::BaseModelQoLTest < Minitest::Test + class E0 + include Amocrm::Internal::Type::Enum + + attr_reader :values + + def initialize(*values) = (@values = values) + end + + module E1 + extend Amocrm::Internal::Type::Enum + + A = 1 + end + + module E2 + extend Amocrm::Internal::Type::Enum + + A = 1 + end + + module E3 + extend Amocrm::Internal::Type::Enum + + A = 2 + B = 3 + end + + class U0 + include Amocrm::Internal::Type::Union + + def initialize(*variants) = variants.each { variant(_1) } + end + + module U1 + extend Amocrm::Internal::Type::Union + + variant String + variant Integer + end + + module U2 + extend Amocrm::Internal::Type::Union + + variant String + variant Integer + end + + class M1 < Amocrm::Internal::Type::BaseModel + required :a, Integer + end + + class M2 < Amocrm::Internal::Type::BaseModel + required :a, Integer, nil?: true + end + + class M3 < M2 + required :a, Integer + end + + def test_equality + cases = { + [Amocrm::Internal::Type::Unknown, Amocrm::Internal::Type::Unknown] => true, + [Amocrm::Internal::Type::Boolean, Amocrm::Internal::Type::Boolean] => true, + [Amocrm::Internal::Type::Unknown, Amocrm::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, + [M1.new(a: 1), M1.new(a: 1)] => true + } + + cases.each do + if _2 + assert_equal(*_1) + assert_equal(*_1.map(&:hash)) + else + refute_equal(*_1) + refute_equal(*_1.map(&:hash)) + end + end + end +end + +class Amocrm::Test::MetaInfoTest < Minitest::Test + A1 = Amocrm::Internal::Type::ArrayOf[Integer, nil?: true, doc: "dog"] + H1 = Amocrm::Internal::Type::HashOf[-> { String }, nil?: true, doc: "dawg"] + + class M1 < Amocrm::Internal::Type::BaseModel + required :a, Integer, doc: "dog" + optional :b, -> { String }, nil?: true, doc: "dawg" + end + + module U1 + extend Amocrm::Internal::Type::Union + + variant -> { Integer }, const: 2, doc: "dog" + variant -> { String }, doc: "dawg" + end + + def test_meta_retrieval + m1 = A1.instance_variable_get(:@meta) + m2 = H1.instance_variable_get(:@meta) + assert_equal({doc: "dog"}, m1) + assert_equal({doc: "dawg"}, m2) + + ma, mb = M1.fields.fetch_values(:a, :b) + assert_equal({doc: "dog"}, ma.fetch(:meta)) + assert_equal({doc: "dawg"}, mb.fetch(:meta)) + + ua, ub = U1.send(:known_variants).map(&:last) + assert_equal({doc: "dog"}, ua) + assert_equal({doc: "dawg"}, ub) + end +end diff --git a/test/amocrm/internal/util_test.rb b/test/amocrm/internal/util_test.rb new file mode 100644 index 0000000..41a6971 --- /dev/null +++ b/test/amocrm/internal/util_test.rb @@ -0,0 +1,673 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +class Amocrm::Test::UtilDataHandlingTest < Minitest::Test + def test_left_map + assert_pattern do + Amocrm::Internal::Util.deep_merge({a: 1}, nil) => nil + end + end + + def test_right_map + assert_pattern do + Amocrm::Internal::Util.deep_merge(nil, {a: 1}) => {a: 1} + end + end + + def test_disjoint_maps + assert_pattern do + Amocrm::Internal::Util.deep_merge({b: 2}, {a: 1}) => {a: 1, b: 2} + end + end + + def test_overlapping_maps + assert_pattern do + Amocrm::Internal::Util.deep_merge({b: 2, c: 3}, {a: 1, c: 4}) => {a: 1, b: 2, c: 4} + end + end + + def test_nested + assert_pattern do + Amocrm::Internal::Util.deep_merge({b: {b2: 1}}, {b: {b2: 2}}) => {b: {b2: 2}} + end + end + + def test_nested_left_map + assert_pattern do + Amocrm::Internal::Util.deep_merge({b: {b2: 1}}, {b: 6}) => {b: 6} + end + end + + def test_omission + merged = Amocrm::Internal::Util.deep_merge( + {b: {b2: 1, b3: {c: 4, d: 5}}}, + {b: {b2: 1, b3: {c: Amocrm::Internal::OMIT, d: 5}}} + ) + + assert_pattern do + merged => {b: {b2: 1, b3: {d: 5}}} + end + end + + def test_concat + merged = Amocrm::Internal::Util.deep_merge( + {a: {b: [1, 2]}}, + {a: {b: [3, 4]}}, + concat: true + ) + + assert_pattern do + merged => {a: {b: [1, 2, 3, 4]}} + end + end + + def test_concat_false + merged = Amocrm::Internal::Util.deep_merge( + {a: {b: [1, 2]}}, + {a: {b: [3, 4]}}, + concat: false + ) + + assert_pattern do + merged => {a: {b: [3, 4]}} + end + end + + def test_dig + assert_pattern do + Amocrm::Internal::Util.dig(1, nil) => 1 + Amocrm::Internal::Util.dig({a: 1}, :b) => nil + Amocrm::Internal::Util.dig({a: 1}, :a) => 1 + Amocrm::Internal::Util.dig({a: {b: 1}}, [:a, :b]) => 1 + + Amocrm::Internal::Util.dig([], 1) => nil + Amocrm::Internal::Util.dig([nil, [nil, 1]], [1, 1]) => 1 + Amocrm::Internal::Util.dig({a: [nil, 1]}, [:a, 1]) => 1 + Amocrm::Internal::Util.dig([], 1.0) => nil + + Amocrm::Internal::Util.dig(Object, 1) => nil + Amocrm::Internal::Util.dig([], 1.0) { 2 } => 2 + Amocrm::Internal::Util.dig([], ->(_) { 2 }) => 2 + Amocrm::Internal::Util.dig([1], -> { _1 in [1] }) => true + end + end +end + +class Amocrm::Test::UtilUriHandlingTest < Minitest::Test + def test_parsing + %w[ + http://example.com + https://example.com/ + https://example.com:443/example?e1=e1&e2=e2&e= + ].each do |url| + parsed = Amocrm::Internal::Util.parse_uri(url) + unparsed = Amocrm::Internal::Util.unparse_uri(parsed).to_s + + assert_equal(url, unparsed) + assert_equal(parsed, Amocrm::Internal::Util.parse_uri(unparsed)) + end + end + + def test_joining + cases = [ + [ + "h://a.b/c?d=e", + "h://nope/ignored", + Amocrm::Internal::Util.parse_uri("h://a.b/c?d=e") + ], + [ + "h://a.b/c?d=e", + "h://nope", + { + host: "a.b", + path: "/c", + query: {"d" => ["e"]} + } + ], + [ + "h://a.b/c?d=e", + "h://nope", + { + path: "h://a.b/c", + query: {"d" => ["e"]} + } + ] + ] + + cases.each do |expect, lhs, rhs| + assert_equal( + URI.parse(expect), + Amocrm::Internal::Util.join_parsed_uri( + Amocrm::Internal::Util.parse_uri(lhs), + rhs + ) + ) + end + end + + def test_joining_queries + base_url = "h://a.b/c?d=e" + cases = { + "c2" => "h://a.b/c/c2", + "/c2?f=g" => "h://a.b/c2?f=g", + "/c?f=g" => "h://a.b/c?d=e&f=g" + } + + cases.each do |path, expected| + assert_equal( + URI.parse(expected), + Amocrm::Internal::Util.join_parsed_uri( + Amocrm::Internal::Util.parse_uri(base_url), + {path: path} + ) + ) + end + end +end + +class Amocrm::Test::RegexMatchTest < Minitest::Test + def test_json_content + cases = { + "application/json" => true, + "application/jsonl" => false, + "application/vnd.github.v3+json" => true, + "application/vnd.api+json" => true + } + cases.each do |header, verdict| + assert_pattern do + Amocrm::Internal::Util::JSON_CONTENT.match?(header) => ^verdict + end + end + end + + def test_jsonl_content + cases = { + "application/x-ndjson" => true, + "application/x-ldjson" => true, + "application/jsonl" => true, + "application/x-jsonl" => true, + "application/json" => false, + "application/vnd.api+json" => false + } + cases.each do |header, verdict| + assert_pattern do + Amocrm::Internal::Util::JSONL_CONTENT.match?(header) => ^verdict + end + end + end +end + +class Amocrm::Test::UtilFormDataEncodingTest < Minitest::Test + class FakeCGI < CGI + def initialize(headers, io) + encoded = io.to_a + @ctype = headers["content-type"] + # rubocop:disable Lint/EmptyBlock + @io = Amocrm::Internal::Util::ReadIOAdapter.new(encoded.to_enum) {} + # rubocop:enable Lint/EmptyBlock + @c_len = encoded.join.bytesize.to_s + super() + end + + def stdinput = @io + + def env_table + { + "REQUEST_METHOD" => "POST", + "CONTENT_TYPE" => @ctype, + "CONTENT_LENGTH" => @c_len + } + end + end + + def test_encoding_length + headers, = Amocrm::Internal::Util.encode_content( + {"content-type" => "multipart/form-data"}, + Pathname(__FILE__) + ) + assert_pattern do + headers.fetch("content-type") => /boundary=(.+)$/ + end + field, = Regexp.last_match.captures + assert(field.length < 70 - 6) + end + + def test_file_encode + file = Pathname(__FILE__) + fileinput = Amocrm::Internal::Type::Converter.dump(Amocrm::Internal::Type::FileInput, "abc") + headers = {"content-type" => "multipart/form-data"} + cases = { + "abc" => ["", "abc"], + StringIO.new("abc") => ["", "abc"], + fileinput => %w[upload abc], + Amocrm::FilePart.new(StringIO.new("abc")) => ["", "abc"], + file => [file.basename.to_path, /^class Amocrm/], + Amocrm::FilePart.new(file, filename: "d o g") => ["d%20o%20g", /^class Amocrm/] + } + cases.each do |body, testcase| + filename, val = testcase + encoded = Amocrm::Internal::Util.encode_content(headers, body) + cgi = FakeCGI.new(*encoded) + io = cgi[""] + assert_pattern do + io.original_filename => ^filename + io.read => ^val + end + end + end + + def test_hash_encode + headers = {"content-type" => "multipart/form-data"} + cases = { + {a: 2, b: 3} => {"a" => "2", "b" => "3"}, + {a: 2, b: nil} => {"a" => "2", "b" => "null"}, + {a: 2, b: [1, 2, 3]} => {"a" => "2", "b" => "1"}, + {strio: StringIO.new("a")} => {"strio" => "a"}, + {strio: Amocrm::FilePart.new("a")} => {"strio" => "a"}, + {pathname: Pathname(__FILE__)} => {"pathname" => -> { _1.read in /^class Amocrm/ }}, + {pathname: Amocrm::FilePart.new(Pathname(__FILE__))} => {"pathname" => -> { _1.read in /^class Amocrm/ }} + } + cases.each do |body, testcase| + encoded = Amocrm::Internal::Util.encode_content(headers, body) + cgi = FakeCGI.new(*encoded) + testcase.each do |key, val| + assert_pattern do + parsed = + case (p = cgi[key]) + in StringIO + p.read + else + p + end + parsed => ^val + end + end + end + end +end + +class Amocrm::Test::UtilIOAdapterTest < Minitest::Test + def test_copy_read + cases = { + StringIO.new("abc") => "abc", + Enumerator.new { _1 << "abc" } => "abc" + } + cases.each do |input, expected| + io = StringIO.new + # rubocop:disable Lint/EmptyBlock + adapter = Amocrm::Internal::Util::ReadIOAdapter.new(input) {} + # rubocop:enable Lint/EmptyBlock + IO.copy_stream(adapter, io) + assert_equal(expected, io.string) + end + end + + def test_copy_write + cases = { + StringIO.new => "", + StringIO.new("abc") => "abc" + } + cases.each do |input, expected| + enum = Amocrm::Internal::Util.writable_enum do |y| + IO.copy_stream(input, y) + end + assert_equal(expected, enum.to_a.join) + end + end +end + +class Amocrm::Test::UtilFusedEnumTest < Minitest::Test + def test_rewind_closing + touched = false + once = 0 + steps = 0 + enum = Enumerator.new do |y| + next if touched + + 10.times do + steps = _1 + y << _1 + end + ensure + once = once.succ + end + + fused = Amocrm::Internal::Util.fused_enum(enum, external: true) do + touched = true + loop { enum.next } + end + Amocrm::Internal::Util.close_fused!(fused) + + assert_equal(1, once) + assert_equal(0, steps) + end + + def test_thread_interrupts + once = 0 + que = Queue.new + enum = Enumerator.new do |y| + 10.times { y << _1 } + ensure + once = once.succ + end + + fused_1 = Amocrm::Internal::Util.fused_enum(enum, external: true) { loop { enum.next } } + fused_2 = Amocrm::Internal::Util.chain_fused(fused_1) { fused_1.each(&_1) } + fused_3 = Amocrm::Internal::Util.chain_fused(fused_2) { fused_2.each(&_1) } + + th = ::Thread.new do + que << "🐶" + fused_3.each { sleep(10) } + end + + assert_equal("🐶", que.pop) + th.kill.join + assert_equal(1, once) + end + + def test_closing + arr = [1, 2, 3] + once = 0 + fused = Amocrm::Internal::Util.fused_enum(arr.to_enum) do + once = once.succ + end + + enumerated_1 = fused.to_a + assert_equal(arr, enumerated_1) + assert_equal(1, once) + + enumerated_2 = fused.to_a + assert_equal([], enumerated_2) + assert_equal(1, once) + end + + def test_rewind_chain + once = 0 + fused = Amocrm::Internal::Util.fused_enum([1, 2, 3].to_enum) do + once = once.succ + end + .lazy + .map(&:succ) + .filter(&:odd?) + first = fused.next + + assert_equal(3, first) + assert_equal(0, once) + assert_raises(StopIteration) { fused.rewind.next } + assert_equal(1, once) + end + + def test_external_iteration + iter = [1, 2, 3].to_enum + first = iter.next + fused = Amocrm::Internal::Util.fused_enum(iter, external: true) + + assert_equal(1, first) + assert_equal([2, 3], fused.to_a) + end + + def test_close_fused + once = 0 + fused = Amocrm::Internal::Util.fused_enum([1, 2, 3].to_enum) do + once = once.succ + end + + Amocrm::Internal::Util.close_fused!(fused) + + assert_equal(1, once) + assert_equal([], fused.to_a) + assert_equal(1, once) + end + + def test_closed_fused_extern_iteration + taken = 0 + enum = [1, 2, 3].to_enum.lazy.map do + taken = taken.succ + _1 + end + fused = Amocrm::Internal::Util.fused_enum(enum) + first = fused.next + + assert_equal(1, first) + Amocrm::Internal::Util.close_fused!(fused) + assert_equal(1, taken) + end + + def test_closed_fused_taken_count + taken = 0 + enum = [1, 2, 3].to_enum.lazy.map do + taken = taken.succ + _1 + end + .map(&:succ) + .filter(&:odd?) + fused = Amocrm::Internal::Util.fused_enum(enum) + + assert_equal(0, taken) + Amocrm::Internal::Util.close_fused!(fused) + assert_equal(0, taken) + end + + def test_closed_fused_extern_iter_taken_count + taken = 0 + enum = [1, 2, 3].to_enum.lazy.map do + taken = taken.succ + _1 + end + .map(&:succ) + .filter(&:itself) + first = enum.next + assert_equal(2, first) + assert_equal(1, taken) + + fused = Amocrm::Internal::Util.fused_enum(enum) + Amocrm::Internal::Util.close_fused!(fused) + assert_equal(1, taken) + end + + def test_close_fused_sse_chain + taken = 0 + enum = [1, 2, 3].to_enum.lazy.map do + taken = taken.succ + _1 + end + .map(&:succ) + .filter(&:odd?) + .map(&:to_s) + + fused_1 = Amocrm::Internal::Util.fused_enum(enum) + fused_2 = Amocrm::Internal::Util.decode_lines(fused_1) + fused_3 = Amocrm::Internal::Util.decode_sse(fused_2) + + assert_equal(0, taken) + Amocrm::Internal::Util.close_fused!(fused_3) + assert_equal(0, taken) + end +end + +class Amocrm::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| + Amocrm::Internal::Util.force_charset!(content_type, text: text) + assert_equal(encoding, text.encoding) + end + end +end + +class Amocrm::Test::UtilSseTest < Minitest::Test + def test_decode_lines + cases = { + %w[] => %w[], + %W[\n\n] => %W[\n \n], + %W[\n \n] => %W[\n \n], + %w[a] => %w[a], + %W[a\nb] => %W[a\n b], + %W[a\nb\n] => %W[a\n b\n], + %W[\na b\n] => %W[\n ab\n], + %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], + ["\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| + eols.each do |eol| + lines = Amocrm::Internal::Util.decode_lines(enum.map { _1.gsub("\n", eol) }) + assert_equal(expected.map { _1.gsub("\n", eol) }, lines.to_a, "eol=#{JSON.generate(eol)}") + end + end + end + + def test_mixed_decode_lines + cases = { + %w[] => %w[], + %W[\r\r] => %W[\r \r], + %W[\r \r] => %W[\r \r], + %W[\r\r\r] => %W[\r \r \r], + %W[\r\r \r] => %W[\r \r \r], + %W[\r \n] => %W[\r\n], + %W[\r\r\n] => %W[\r \r\n], + %W[\n\r] => %W[\n \r] + } + cases.each do |enum, expected| + lines = Amocrm::Internal::Util.decode_lines(enum) + assert_equal(expected, lines.to_a) + end + end + + def test_decode_sse + cases = { + "empty input" => { + [] => [] + }, + "single data event" => { + [ + "data: hello world\n", + "\n" + ] => [ + {data: "hello world\n"} + ] + }, + "multiple data lines" => { + [ + "data: line 1\n", + "data: line 2\n", + "\n" + ] => [ + {data: "line 1\nline 2\n"} + ] + }, + "complete event" => { + [ + "id: 123\n", + "event: update\n", + "data: hello world\n", + "retry: 5000\n", + "\n" + ] => [ + { + event: "update", + id: "123", + data: "hello world\n", + retry: 5000 + } + ] + }, + "multiple events" => { + [ + "event: update\n", + "data: first\n", + "\n", + "event: message\n", + "data: second\n", + "\n" + ] => [ + {event: "update", data: "first\n"}, + {event: "message", data: "second\n"} + ] + }, + "comments" => { + [ + ": this is a comment\n", + "data: actual data\n", + "\n" + ] => [ + {data: "actual data\n"} + ] + }, + "invalid retry" => { + [ + "retry: not a number\n", + "data: hello\n", + "\n" + ] => [ + {data: "hello\n"} + ] + }, + "invalid id with null" => { + [ + "id: bad\0id\n", + "data: hello\n", + "\n" + ] => [ + {data: "hello\n"} + ] + }, + "leading space in value" => { + [ + "data: hello world\n", + "data: leading space\n", + "\n" + ] => [ + {data: "hello world\n leading space\n"} + ] + }, + "no final newline" => { + [ + "data: hello\n", + "id: 1" + ] => [ + {data: "hello\n", id: "1"} + ] + }, + "multiple empty lines" => { + [ + "data: first\n", + "\n", + "\n", + "data: second\n", + "\n" + ] => [ + {data: "first\n"}, + {data: "second\n"} + ] + }, + "multibyte unicode" => { + [ + "data: \u1F62E\u200D\u1F4A8\n" + ] => [ + {data: "\u1F62E\u200D\u1F4A8\n"} + ] + } + } + + cases.each do |name, test_cases| + test_cases.each do |input, expected| + actual = Amocrm::Internal::Util.decode_sse(input).map(&:compact) + assert_equal(expected, actual, name) + end + end + end +end diff --git a/test/amocrm/resource_namespaces.rb b/test/amocrm/resource_namespaces.rb new file mode 100644 index 0000000..1400dfb --- /dev/null +++ b/test/amocrm/resource_namespaces.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Amocrm + module Test + module Resources + module Leads + end + + module V4 + module Leads + end + end + end + end +end diff --git a/test/amocrm/resources/v4/leads/unsorted_test.rb b/test/amocrm/resources/v4/leads/unsorted_test.rb new file mode 100644 index 0000000..d1dbeea --- /dev/null +++ b/test/amocrm/resources/v4/leads/unsorted_test.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative "../../../test_helper" + +class Amocrm::Test::Resources::V4::Leads::UnsortedTest < Amocrm::Test::ResourceTest + def test_accept + skip("Prism tests are disabled") + + response = @amocrm.v4.leads.unsorted.accept("uid") + + assert_pattern do + response => Amocrm::Models::V4::Leads::UnsortedAcceptResponse + end + + assert_pattern do + case response + in Amocrm::Models::V4::Leads::UnsortedAcceptResponse::UnsortedAcceptResponse + in Amocrm::Models::V4::Leads::UnsortedAcceptResponse::Problem + end + end + end + + def test_create_forms_required_params + skip("Prism tests are disabled") + + response = + @amocrm.v4.leads.unsorted.create_forms( + body: [{metadata: {}, source_name: "source_name", source_uid: "source_uid"}] + ) + + assert_pattern do + response => Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse + end + + assert_pattern do + case response + in Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::UnsortedCreateResponse + in Amocrm::Models::V4::Leads::UnsortedCreateFormsResponse::Problem + end + end + end + + def test_decline + skip("Prism tests are disabled") + + response = @amocrm.v4.leads.unsorted.decline("uid") + + assert_pattern do + response => Amocrm::Models::V4::Leads::UnsortedDeclineResponse + end + + assert_pattern do + case response + in Amocrm::Models::V4::Leads::UnsortedDeclineResponse::UnsortedAcceptResponse + in Amocrm::Models::V4::Leads::UnsortedDeclineResponse::Problem + end + end + end +end diff --git a/test/amocrm/resources/v4/leads_test.rb b/test/amocrm/resources/v4/leads_test.rb new file mode 100644 index 0000000..e4c4043 --- /dev/null +++ b/test/amocrm/resources/v4/leads_test.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +class Amocrm::Test::Resources::V4::LeadsTest < Amocrm::Test::ResourceTest +end diff --git a/test/amocrm/resources/v4_test.rb b/test/amocrm/resources/v4_test.rb new file mode 100644 index 0000000..ad34383 --- /dev/null +++ b/test/amocrm/resources/v4_test.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +class Amocrm::Test::Resources::V4Test < Amocrm::Test::ResourceTest +end diff --git a/test/amocrm/test_helper.rb b/test/amocrm/test_helper.rb new file mode 100644 index 0000000..ad140e4 --- /dev/null +++ b/test/amocrm/test_helper.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# Requiring this file from each test file ensures we always do the following, even +# when running a single-file test: +# - Load the whole gem (as one would in production) +# - Define shared testing namespace so that we don't need to indent test files as much +# - Setting up testing dependencies + +require "digest" +require "singleton" + +require "async" +require "minitest/autorun" +require "minitest/focus" +require "minitest/hooks/test" +require "minitest/proveit" +require "minitest/rg" +require "webmock" + +require_relative "../../lib/amocrm" +require_relative "resource_namespaces" + +module Kernel + alias_method :_sleep, :sleep + + def sleep(secs) + case Thread.current.thread_variable_get(:mock_sleep) + in Array => counter + counter << secs + secs + else + _sleep(secs) + end + end +end + +class Time + class << self + alias_method :_now, :now + end + + def self.now = Thread.current.thread_variable_get(:time_now) || _now +end + +class Amocrm::Test::SingletonClient < Amocrm::Client + include Singleton + + TEST_API_BASE_URL = ENV.fetch("TEST_API_BASE_URL", "http://localhost:4010") + + def initialize + super(base_url: Amocrm::Test::SingletonClient::TEST_API_BASE_URL, api_key: "My API Key") + end +end + +module Minitest::Serial + def test_order = :random + + def run_one_method(...) = Minitest::Runnable.run_one_method(...) +end + +class Minitest::Test + include Minitest::Hooks + + make_my_diffs_pretty! + parallelize_me! + prove_it! +end + +class Amocrm::Test::ResourceTest < Minitest::Test + def async? + return @async unless @async.nil? + @async = Digest::SHA256.hexdigest(self.class.name).to_i(16).odd? + end + + def before_all + super + @amocrm = Amocrm::Test::SingletonClient.instance + end + + def around_all = async? ? Sync { super } : super + + def around = async? ? Async { super }.wait : super +end + +module WebMock + AssertionFailure.error_class = Minitest::Assertion +end