diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 4686e5bc7..000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,4 +0,0 @@ -version: "2" -plugins: - rubocop: - enabled: false diff --git a/.document b/.document deleted file mode 100644 index 7bdcb2030..000000000 --- a/.document +++ /dev/null @@ -1,6 +0,0 @@ -LICENSE.md -README.md -bin/* -lib/**/*.rb -test/**/*.rb -.github/*.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c6c8b3621 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 006401ba5..027026a5a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,10 +4,27 @@ In Faraday we always welcome new ideas and features, however we also have to ens that the overall code quality stays on reasonable levels. For this reason, before adding any contribution to Faraday, we highly recommend reading this quick guide to ensure your PR can be reviewed and approved as quickly as possible. - -We are pushing towards a 1.0 release, when we will have to follow [Semantic -Versioning][semver]. If your patch includes changes to break compatibility, -note that so we can add it to the [Changelog][]. + +We are past our 1.0 release, and follow [Semantic Versioning][semver]. If your +patch includes changes that break compatibility, note that in the Pull Request, so we can add it to +the [Changelog][]. + + +### Policy on inclusive language + +You have read our [Code of Conduct][], which includes a note about **inclusive language**. This section tries to make that actionable. + +Faraday has a large and diverse userbase. To make Faraday a pleasant and effective experience for everyone, we use inclusive language. + +These resources can help: + +- Google's tutorial [Writing inclusive documentation](https://developers.google.com/style/inclusive-documentation) teaches by example, how to reword non-inclusive things. +- Linux kernel mailing list's [Coding Style: Inclusive Terminology](https://lkml.org/lkml/2020/7/4/229) said "Add no new instances of non-inclusive words, here is a list of words not include new ones of." +- Linguistic Society of America published [Guidelines for Inclusive Language](https://www.linguisticsociety.org/resource/guidelines-inclusive-language) which concluded: "We encourage all linguists to consider the possible reactions of their potential audience to their writing and, in so doing, to choose expository practices and content that is positive, inclusive, and respectful." + +This project attempts to improve in these areas. Join us in doing that important work. + +If you want to privately raise any breach to this policy with the Faraday team, feel free to reach out to [@iMacTia](https://twitter.com/iMacTia) and [@olleolleolle](https://twitter.com/olleolleolle) on Twitter. ### Required Checks @@ -19,7 +36,7 @@ our GitHub Actions Workflow to block your contribution. # Run unit tests and check code coverage $ bundle exec rspec -# Run Rubocop and check code style +# Check code style $ bundle exec rubocop ``` @@ -34,42 +51,29 @@ When adding a feature in Faraday: the green light by the core team start working on the PR. -### New Middleware - -We will accept middleware that: +### New Middleware & Adapters -1. is useful to a broader audience, but can be implemented relatively simple; and -2. which isn't already present in [faraday_middleware][] project. +We prefer new adapters and middlewares to be added **as separate gems**. We can link to such gems from this project. +This goes for the [faraday_middleware][] project as well. -### New Adapters - -We will accept adapters that: +We encourage adapters that: 1. support SSL & streaming; 1. are proven and may have better performance than existing ones; or -2. if they have features not present in included adapters. - +1. have features not present in included adapters. -### Changes to Faraday Website -The [Faraday Website][website] is included in the Faraday repository, under the `/docs` folder. -If you want to apply changes to it, please test it locally using `Jekyll`. +### Changes to the Faraday Docs -```bash -# Navigate into the /docs folder -$ cd docs - -# Install Jekyll dependencies, this bundle is different from Faraday's one. -$ bundle install +The Faraday Docs are included in the Faraday repository, under the `/docs` folder and deployed to [GitHub Pages][website]. +If you want to apply changes to it, please test it locally before opening your PR. +You can find more information in the [Faraday Docs README][docs], including how to preview changes locally. -# Run the Jekyll server with the Faraday website -$ bundle exec jekyll serve - -# The site will now be reachable at http://127.0.0.1:4000/faraday/ -``` -[semver]: http://semver.org/ +[semver]: https://semver.org/ [changelog]: https://github.com/lostisland/faraday/releases [faraday_middleware]: https://github.com/lostisland/faraday_middleware [website]: https://lostisland.github.io/faraday +[docs]: ../docs/README.md +[Code of Conduct]: ./CODE_OF_CONDUCT.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..075134ebf --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "npm" + directory: / + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c567eb623..fbe375628 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,79 +4,66 @@ on: pull_request: push: - branches: [master, 0.1x] + branches: [ main, 1.x, 0.1x ] env: GIT_COMMIT_SHA: ${{ github.sha }} GIT_BRANCH: ${{ github.ref }} - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + +permissions: + contents: read # to fetch code (actions/checkout) jobs: linting: runs-on: ubuntu-latest + env: + BUNDLE_WITH: lint + BUNDLE_WITHOUT: development:test steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - - name: Set up Ruby 2.6 - uses: actions/setup-ruby@v1 - with: - ruby-version: 2.6.x + - name: Setup Ruby 3.x + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3 + bundler-cache: true - - name: Rubocop - run: | - gem install rubocop rubocop-performance --no-document - rubocop --require rubocop-performance --format progress + - name: Rubocop + run: bundle exec rubocop --format progress - - name: Yard-Junk - run: | - gem install yard-junk --no-document - yard-junk --path lib + - name: Yard-Junk + run: bundle exec yard-junk --path lib build: - needs: [linting] + needs: [ linting ] runs-on: ubuntu-latest + name: build ${{ matrix.ruby }} strategy: + fail-fast: false matrix: - ruby: ['2.4', '2.5', '2.6', '2.7'] + ruby: [ '3.0', '3.1', '3.2', '3.3' ] + experimental: [false] + include: + - ruby: head + experimental: true + - ruby: truffleruby-head + experimental: true steps: - - uses: actions/checkout@v1 - - - name: Install dependencies - run: | - sudo apt-get install libcurl4-openssl-dev - - - name: Set up RVM - run: | - curl -sSL https://get.rvm.io | bash - - - name: Set up Ruby - run: | - source $HOME/.rvm/scripts/rvm - rvm install ${{ matrix.ruby }} --disable-binary - rvm --default use ${{ matrix.ruby }} + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true - - name: Build - run: | - source $HOME/.rvm/scripts/rvm - sudo apt-get install libcurl4-openssl-dev - gem install bundler -v '<2' - bundle install --jobs 4 --retry 3 + - name: RSpec + continue-on-error: ${{ matrix.experimental }} + run: bundle exec rake - - name: Setup Code Climate - if: matrix.ruby == '2.6' - run: | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build + - name: Test External Adapters + continue-on-error: ${{ matrix.experimental }} + run: bundle exec bake test:external - - name: Test - run: | - source $HOME/.rvm/scripts/rvm - bundle exec rake - - name: Run Code Climate Test Reporter - if: success() && matrix.ruby == '2.6' - run: ./cc-test-reporter after-build --coverage-input-type simplecov --exit-code $? - continue-on-error: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fc001b6a1..c667d8f48 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,21 +9,18 @@ jobs: name: Publish to Rubygems runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - - name: Set up Ruby 2.6 - uses: actions/setup-ruby@v1 + - name: Setup Ruby 3.x + uses: ruby/setup-ruby@v1 with: - version: 2.6.x + bundler-cache: true + ruby-version: 3 - name: Publish to RubyGems - run: | - mkdir -p $HOME/.gem - touch $HOME/.gem/credentials - chmod 0600 $HOME/.gem/credentials - printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials - gem build faraday.gemspec - gem push faraday-*.gem - env: - GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_AUTH_TOKEN }} + uses: rubygems/release-gem@v1 diff --git a/.github/workflows/refresh_team_page.yml b/.github/workflows/refresh_team_page.yml new file mode 100644 index 000000000..4b721ba71 --- /dev/null +++ b/.github/workflows/refresh_team_page.yml @@ -0,0 +1,15 @@ +name: Refresh Team Page + +on: + push: + branches: [ main ] + +permissions: {} +jobs: + build: + name: Refresh Contributors Stats + runs-on: ubuntu-latest + steps: + - name: Call GitHub API + run: | + curl "https://api.github.com/repos/${{ github.repository }}/stats/contributors" diff --git a/.gitignore b/.gitignore index eea05d17b..6e75dace2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,12 +8,17 @@ tmp .rvmrc .ruby-version .yardoc +.DS_Store ## BUNDLER *.gem .bundle Gemfile.lock vendor/bundle +external + +## NPM +node_modules ## PROJECT::SPECIFIC .rbx diff --git a/.rspec b/.rspec index bb697427e..7a2cc1a6e 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,3 @@ --require spec_helper --format documentation ---color \ No newline at end of file +--color diff --git a/.rubocop.yml b/.rubocop.yml index 3845b51f2..c359f4bf6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,27 +1,200 @@ inherit_from: .rubocop_todo.yml require: + - rubocop-packaging - rubocop-performance AllCops: DisplayCopNames: true DisplayStyleGuide: true - TargetRubyVersion: 2.3 + TargetRubyVersion: 3.0 -Metrics/BlockLength: +# Custom config +Gemspec/RequireMFA: # we don't know if this works with auto-deployments yet + Enabled: false +Layout/LineLength: Exclude: - spec/**/*.rb - examples/**/*.rb - -Layout/LineLength: +Metrics/BlockLength: Exclude: + - lib/faraday/options/env.rb - spec/**/*.rb - examples/**/*.rb - -Style/DoubleNegation: - Enabled: false - +Metrics/ModuleLength: + Exclude: + - lib/faraday/options/env.rb Style/Documentation: Exclude: - 'spec/**/*' - 'examples/**/*' +Style/DoubleNegation: + Enabled: false +Style/IfUnlessModifier: + Enabled: false + +# New cops +Gemspec/DeprecatedAttributeAssignment: # new in 1.30 + Enabled: true +Layout/LineContinuationLeadingSpace: # new in 1.31 + Enabled: true +Layout/LineContinuationSpacing: # new in 1.31 + Enabled: true +Layout/LineEndStringConcatenationIndentation: # new in 1.18 + Enabled: true +Layout/SpaceBeforeBrackets: # new in 1.7 + Enabled: true +Lint/AmbiguousAssignment: # new in 1.7 + Enabled: true +Lint/AmbiguousOperatorPrecedence: # new in 1.21 + Enabled: true +Lint/AmbiguousRange: # new in 1.19 + Enabled: true +Lint/ConstantOverwrittenInRescue: # new in 1.31 + Enabled: true +Lint/DeprecatedConstants: # new in 1.8 + Enabled: true +Lint/DuplicateBranch: # new in 1.3 + Enabled: true +Lint/DuplicateRegexpCharacterClassElement: # new in 1.1 + Enabled: true +Lint/EmptyBlock: # new in 1.1 + Enabled: true +Lint/EmptyClass: # new in 1.3 + Enabled: true +Lint/EmptyInPattern: # new in 1.16 + Enabled: true +Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 + Enabled: true +Lint/LambdaWithoutLiteralBlock: # new in 1.8 + Enabled: true +Lint/NoReturnInBeginEndBlocks: # new in 1.2 + Enabled: true +Lint/NonAtomicFileOperation: # new in 1.31 + Enabled: true +Lint/NumberedParameterAssignment: # new in 1.9 + Enabled: true +Lint/OrAssignmentToConstant: # new in 1.9 + Enabled: true +Lint/RedundantDirGlobSort: # new in 1.8 + Enabled: true +Lint/RefinementImportMethods: # new in 1.27 + Enabled: true +Lint/RequireRangeParentheses: # new in 1.32 + Enabled: true +Lint/RequireRelativeSelfPath: # new in 1.22 + Enabled: true +Lint/SymbolConversion: # new in 1.9 + Enabled: true +Lint/ToEnumArguments: # new in 1.1 + Enabled: true +Lint/TripleQuotes: # new in 1.9 + Enabled: true +Lint/UnexpectedBlockArity: # new in 1.5 + Enabled: true +Lint/UnmodifiedReduceAccumulator: # new in 1.1 + Enabled: true +Lint/UselessRuby2Keywords: # new in 1.23 + Enabled: true +Naming/BlockForwarding: # new in 1.24 + Enabled: true +Security/CompoundHash: # new in 1.28 + Enabled: true +Security/IoMethods: # new in 1.22 + Enabled: true +Style/ArgumentsForwarding: # new in 1.1 + Enabled: true +Style/CollectionCompact: # new in 1.2 + Enabled: true +Style/DocumentDynamicEvalDefinition: # new in 1.1 + Enabled: true +Style/EmptyHeredoc: # new in 1.32 + Enabled: true +Style/EndlessMethod: # new in 1.8 + Enabled: true +Style/EnvHome: # new in 1.29 + Enabled: true +Style/FetchEnvVar: # new in 1.28 + Enabled: true +Style/FileRead: # new in 1.24 + Enabled: true +Style/FileWrite: # new in 1.24 + Enabled: true +Style/HashConversion: # new in 1.10 + Enabled: true +Style/HashExcept: # new in 1.7 + Enabled: true +Style/IfWithBooleanLiteralBranches: # new in 1.9 + Enabled: true +Style/InPatternThen: # new in 1.16 + Enabled: true +Style/MapCompactWithConditionalBlock: # new in 1.30 + Enabled: true +Style/MapToHash: # new in 1.24 + Enabled: true +Style/MultilineInPatternThen: # new in 1.16 + Enabled: true +Style/NegatedIfElseCondition: # new in 1.2 + Enabled: true +Style/NestedFileDirname: # new in 1.26 + Enabled: true +Style/NilLambda: # new in 1.3 + Enabled: true +Style/NumberedParameters: # new in 1.22 + Enabled: true +Style/NumberedParametersLimit: # new in 1.22 + Enabled: true +Style/ObjectThen: # new in 1.28 + Enabled: true +Style/OpenStructUse: # new in 1.23 + Enabled: true +Style/QuotedSymbols: # new in 1.16 + Enabled: true +Style/RedundantArgument: # new in 1.4 + Enabled: true +Style/RedundantInitialize: # new in 1.27 + Enabled: true +Style/RedundantSelfAssignmentBranch: # new in 1.19 + Enabled: true +Style/SelectByRegexp: # new in 1.22 + Enabled: true +Style/StringChars: # new in 1.12 + Enabled: true +Style/SwapValues: # new in 1.1 + Enabled: true +Performance/AncestorsInclude: # new in 1.7 + Enabled: true +Performance/BigDecimalWithNumericArgument: # new in 1.7 + Enabled: true +Performance/BlockGivenWithExplicitBlock: # new in 1.9 + Enabled: true +Performance/CollectionLiteralInLoop: # new in 1.8 + Enabled: true +Performance/ConcurrentMonotonicTime: # new in 1.12 + Enabled: true +Performance/ConstantRegexp: # new in 1.9 + Enabled: true +Performance/MapCompact: # new in 1.11 + Enabled: true +Performance/MethodObjectAsBlock: # new in 1.9 + Enabled: true +Performance/RedundantEqualityComparisonBlock: # new in 1.10 + Enabled: true +Performance/RedundantSortBlock: # new in 1.7 + Enabled: true +Performance/RedundantSplitRegexpArgument: # new in 1.10 + Enabled: true +Performance/RedundantStringChars: # new in 1.7 + Enabled: true +Performance/ReverseFirst: # new in 1.7 + Enabled: true +Performance/SortReverse: # new in 1.7 + Enabled: true +Performance/Squeeze: # new in 1.7 + Enabled: true +Performance/StringIdentifierArgument: # new in 1.13 + Enabled: true +Performance/StringInclude: # new in 1.7 + Enabled: true +Performance/Sum: # new in 1.8 + Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6bf427c24..9b7e255db 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,29 +1,71 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2019-04-08 21:22:38 +0200 using RuboCop version 0.67.2. +# on 2023-12-27 11:12:52 UTC using RuboCop version 1.59.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 37 +# Offense count: 6 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Exclude: + - 'spec/faraday/options/options_spec.rb' + - 'spec/faraday/rack_builder_spec.rb' + - 'spec/faraday/request/instrumentation_spec.rb' + +# Offense count: 11 +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: + Exclude: + - 'spec/faraday/connection_spec.rb' + - 'spec/faraday/rack_builder_spec.rb' + - 'spec/faraday/response_spec.rb' + +# Offense count: 13 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Max: 67 + Max: 42 -# Offense count: 4 -# Configuration parameters: CountComments. +# Offense count: 3 +# Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 234 + Max: 225 -# Offense count: 17 +# Offense count: 9 +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Max: 16 + Max: 13 -# Offense count: 48 -# Configuration parameters: CountComments, ExcludedMethods. +# Offense count: 27 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Max: 48 + Max: 33 -# Offense count: 13 +# Offense count: 1 +# Configuration parameters: CountKeywordArgs, MaxOptionalParameters. +Metrics/ParameterLists: + Max: 6 + +# Offense count: 7 +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 16 + Max: 14 + +# Offense count: 19 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowOnlyRestArgument, UseAnonymousForwarding, RedundantRestArgumentNames, RedundantKeywordRestArgumentNames, RedundantBlockArgumentNames. +# RedundantRestArgumentNames: args, arguments +# RedundantKeywordRestArgumentNames: kwargs, options, opts +# RedundantBlockArgumentNames: blk, block, proc +Style/ArgumentsForwarding: + Exclude: + - 'lib/faraday.rb' + - 'lib/faraday/rack_builder.rb' + +# Offense count: 3 +Style/DocumentDynamicEvalDefinition: + Exclude: + - 'lib/faraday/connection.rb' + - 'lib/faraday/options.rb' diff --git a/.yardopts b/.yardopts index e1ba7b869..87bde0f1e 100644 --- a/.yardopts +++ b/.yardopts @@ -9,4 +9,4 @@ lib/**/*.rb - -CHANGELOG.md \ No newline at end of file +CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 13dcda6f2..c9bc56f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,297 @@ # Faraday Changelog -## v1.0 +## The changelog has moved! + +This file is not being updated anymore. Instead, please check the [Releases](https://github.com/lostisland/faraday/releases) page. + +## [2.2.0](https://github.com/lostisland/faraday/compare/v2.1.0...v2.2.0) (2022-02-03) + +* Reintroduce the possibility to register middleware with symbols, strings or procs in [#1391](https://github.com/lostisland/faraday/pull/1391) + +## [2.1.0](https://github.com/lostisland/faraday/compare/v2.0.1...v2.1.0) (2022-01-15) + +* Fix test adapter thread safety by @iMacTia in [#1380](https://github.com/lostisland/faraday/pull/1380) +* Add default adapter options by @hirasawayuki in [#1382](https://github.com/lostisland/faraday/pull/1382) +* CI: Add Ruby 3.1 to matrix by @petergoldstein in [#1374](https://github.com/lostisland/faraday/pull/1374) +* docs: fix regex pattern in logger.md examples by @hirasawayuki in [#1378](https://github.com/lostisland/faraday/pull/1378) + +## [2.0.1](https://github.com/lostisland/faraday/compare/v2.0.0...v2.0.1) (2022-01-05) + +* Re-add `faraday-net_http` as default adapter by @iMacTia in [#1366](https://github.com/lostisland/faraday/pull/1366) +* Updated sample format in UPGRADING.md by @vimutter in [#1361](https://github.com/lostisland/faraday/pull/1361) +* docs: Make UPGRADING examples more copyable by @olleolleolle in [#1363](https://github.com/lostisland/faraday/pull/1363) + +## [2.0.0](https://github.com/lostisland/faraday/compare/v1.8.0...v2.0.0) (2022-01-04) + +The next major release is here, and it comes almost 2 years after the release of v1.0! + +This release changes the way you use Faraday and embraces a new paradigm of Faraday as an ecosystem, rather than a library. + +What does that mean? It means that Faraday is less of a bundled tool and more of a framework for the community to build on top of. + +As a result, all adapters and some middleware have moved out and are now shipped as standalone gems 🙌! + +But this doesn't mean that upgrading from Faraday 1.x to Faraday 2.0 should be hard, in fact we've listed everything you need to do in the [UPGRADING.md](https://github.com/lostisland/faraday/blob/main/UPGRADING.md) doc. + +Moreover, we've setup a new [awesome-faraday](https://github.com/lostisland/awesome-faraday) repository that will showcase a curated list of adapters and middleware 😎. + +This release was the result of the efforts of the core team and all the contributors, new and old, that have helped achieve this milestone 👏. + +## What's Changed + +* Autoloading, dependency loading and middleware registry cleanup by @iMacTia in [#1301](https://github.com/lostisland/faraday/pull/1301) +* Move JSON middleware (request and response) from faraday_middleware by @iMacTia in [#1300](https://github.com/lostisland/faraday/pull/1300) +* Remove deprecated `Faraday::Request#method` by @olleolleolle in [#1303](https://github.com/lostisland/faraday/pull/1303) +* Remove deprecated `Faraday::UploadIO` by @iMacTia in [#1307](https://github.com/lostisland/faraday/pull/1307) +* [1.x] Deprecate Authorization helpers in `Faraday::Connection` by @iMacTia in [#1306](https://github.com/lostisland/faraday/pull/1306) +* Drop deprecated auth helpers from Connection and refactor auth middleware by @iMacTia in [#1308](https://github.com/lostisland/faraday/pull/1308) +* Add Faraday 1.x examples in authentication.md docs by @iMacTia in [#1320](https://github.com/lostisland/faraday/pull/1320) +* Fix passing a URL with embedded basic auth by @iMacTia in [#1324](https://github.com/lostisland/faraday/pull/1324) +* Register JSON middleware by @mollerhoj in [#1331](https://github.com/lostisland/faraday/pull/1331) +* Retry middleware should handle string exception class name consistently by @jrochkind in [#1334](https://github.com/lostisland/faraday/pull/1334) +* Improve request info in exceptions raised by RaiseError Middleware by @willianzocolau in [#1335](https://github.com/lostisland/faraday/pull/1335) +* Remove net-http adapter and update docs by @iMacTia in [#1336](https://github.com/lostisland/faraday/pull/1336) +* Explain plan for faraday_middleware in UPGRADING.md by @iMacTia in [#1339](https://github.com/lostisland/faraday/pull/1339) +* Scripts folder cleanup by @iMacTia in [#1340](https://github.com/lostisland/faraday/pull/1340) +* Replace `Hash#merge` with `Utils#deep_merge` for connection options by @xkwd in [#1343](https://github.com/lostisland/faraday/pull/1343) +* Callable authorizers by @sled in [#1345](https://github.com/lostisland/faraday/pull/1345) +* Default value for exc error by @DariuszMusielak in [#1351](https://github.com/lostisland/faraday/pull/1351) +* Don't call `retry_block` unless a retry is going to happen by @jrochkind in [#1350](https://github.com/lostisland/faraday/pull/1350) +* Improve documentation for v2 by @iMacTia in [#1353](https://github.com/lostisland/faraday/pull/1353) +* Remove default `default_adapter` (yes, you read that right) by @iMacTia in [#1354](https://github.com/lostisland/faraday/pull/1354) +* Remove retry middleware by @iMacTia in [#1356](https://github.com/lostisland/faraday/pull/1356) +* Remove multipart middleware and all its documentation and tests by @iMacTia in [#1357](https://github.com/lostisland/faraday/pull/1357) + +## [1.9.3](https://github.com/lostisland/faraday/compare/v1.9.2...v1.9.3) (2022-01-06) + +* Re-add support for Ruby 2.4+ by @iMacTia in [#1371](https://github.com/lostisland/faraday/pull/1371) + +## [1.9.2](https://github.com/lostisland/faraday/compare/v1.9.1...v1.9.2) (2022-01-06) + +* Add alias with legacy name to gemified middleware by @iMacTia in [#1372](https://github.com/lostisland/faraday/pull/1372) + +## [1.9.1](https://github.com/lostisland/faraday/compare/v1.9.0...v1.9.1) (2022-01-06) + +* Update adapter dependencies in Gemspec by @iMacTia in [#1370](https://github.com/lostisland/faraday/pull/1370) + +## [1.9.0](https://github.com/lostisland/faraday/compare/v1.8.0...v1.9.0) (2022-01-06) + +* Use external multipart and retry middleware by @iMacTia in [#1367](https://github.com/lostisland/faraday/pull/1367) + +## [1.8.0](https://github.com/lostisland/faraday/releases/tag/v1.8.0) (2021-09-18) + +### Features + +* Backport authorization procs (#1322, @jarl-dk) + +## [v1.7.0](https://github.com/lostisland/faraday/releases/tag/v1.7.0) (2021-08-09) + +### Features + +* Add strict_mode to Test::Stubs (#1298, @yykamei) + +## [v1.6.0](https://github.com/lostisland/faraday/releases/tag/v1.6.0) (2021-08-01) + +### Misc + +* Use external Rack adapter (#1296, @iMacTia) + +## [v1.5.1](https://github.com/lostisland/faraday/releases/tag/v1.5.1) (2021-07-11) + +### Fixes + +* Fix JRuby incompatibility after moving out EM adapters (#1294, @ahorek) + +### Documentation + +* Update YARD to follow RackBuilder (#1292, @kachick) + +## [v1.5.0](https://github.com/lostisland/faraday/releases/tag/v1.5.0) (2021-07-04) + +### Misc + +* Use external httpclient adapter (#1289, @iMacTia) +* Use external patron adapter (#1290, @iMacTia) + +## [v1.4.3](https://github.com/lostisland/faraday/releases/tag/v1.4.3) (2021-06-24) + +### Fixes + +* Silence warning (#1286, @gurgeous) +* Always dup url_prefix in Connection#build_exclusive_url (#1288, @alexeyds) + +## [v1.4.2](https://github.com/lostisland/faraday/releases/tag/v1.4.2) (2021-05-22) + +### Fixes +* Add proxy setting when url_prefix is changed (#1276, @ci) +* Default proxy scheme to http:// if necessary, fixes #1282 (#1283, @gurgeous) + +### Documentation +* Improve introduction page (#1273, @gurgeous) +* Docs: add more middleware examples (#1277, @gurgeous) + +### Misc +* Use external `em_http` and `em_synchrony` adapters (#1274, @iMacTia) + +## [v1.4.1](https://github.com/lostisland/faraday/releases/tag/v1.4.1) (2021-04-18) + +### Fixes + +* Fix dependencies from external adapter gems (#1269, @iMacTia) + +## [v1.4.0](https://github.com/lostisland/faraday/releases/tag/v1.4.0) (2021-04-16) + +### Highlights + +With this release, we continue the work of gradually moving out adapters into their own gems 🎉 +Thanks to @MikeRogers0 for helping the Faraday team in progressing with this quest 👏 + +And thanks to @olleolleolle efforts, Faraday is becoming more inclusive than ever 🤗 +Faraday's `master` branch has been renamed into `main`, we have an official policy on inclusive language and even a rubocop plugin to check for non-inclusive words ❤️! +Checkout the "Misc" section below for more details 🙌 ! + +### Fixes + +* Fix NoMethodError undefined method 'coverage' (#1255, @Maroo-b) + +### Documentation + +* Some docs on EventMachine adapters. (#1232, @damau) +* CONTRIBUTING: Fix grammar and layout (#1261, @olleolleolle) + +### Misc + +* Replacing Net::HTTP::Persistent with faraday-net_http_persistent (#1250, @MikeRogers0) +* CI: Configure the regenerated Coveralls token (#1256, @olleolleolle) +* Replace Excon adapter with Faraday::Excon gem, and fix autoloading issue with Faraday::NetHttpPersistent (#1257, @iMacTia) +* Drop CodeClimate (#1259, @olleolleolle) +* CI: Rename default branch to main (#1263, @olleolleolle) +* Drop RDoc support file .document (#1264, @olleolleolle, @iMacTia) +* CONTRIBUTING: add a policy on inclusive language (#1262, @olleolleolle) +* Add rubocop-inclusivity (#1267, @olleolleolle, @iMacTia) + +## [v1.3.1](https://github.com/lostisland/faraday/releases/tag/v1.3.1) (2021-04-16) + +### Fixes + +* Escape colon in path segment (#1237, @yarafan) +* Handle IPv6 address String on Faraday::Connection#proxy_from_env (#1252, @cosmo0920) + +### Documentation + +* Fix broken Rubydoc.info links (#1236, @nickcampbell18) +* Add httpx to list of external adapters (#1246, @HoneyryderChuck) + +### Misc + +* Refactor CI to remove duplicated line (#1230, @tricknotes) +* Gemspec: Pick a good ruby2_keywords release (#1241, @olleolleolle) + +## [v1.3.0](https://github.com/lostisland/faraday/releases/tag/v1.3.0) (2020-12-31) + +### Highlights +Faraday v1.3.0 is the first release to officially support Ruby 3.0 in the CI pipeline 🎉 🍾! + +This is also the first release with a previously "included" adapter (Net::HTTP) being isolated into a [separate gem](https://github.com/lostisland/faraday-net_http) 🎊! +The new adapter is added to Faraday as a dependency for now, so that means full backwards-compatibility, but just to be safe be careful when upgrading! + +This is a huge step towards are Faraday v2.0 objective of pushing adapters and middleware into separate gems. +Many thanks to the Faraday Team, @JanDintel and everyone who attended the [ROSS Conf remote event](https://www.rossconf.io/event/remote/) + +### Features + +* Improves consistency with Faraday::Error and Faraday::RaiseError (#1229, @qsona, @iMacTia) + +### Fixes + +* Don't assign to global ::Timer (#1227, @bpo) + +### Documentation + +* CHANGELOG: add releases after 1.0 (#1225, @olleolleolle) +* Improves retry middleware documentation. (#1228, @iMacTia) + +### Misc + +* Move out Net::HTTP adapter (#1222, @JanDintel, @iMacTia) +* Adds Ruby 3.0 to CI Matrix (#1226, @iMacTia) + + +## [v1.2.0](https://github.com/lostisland/faraday/releases/tag/v1.2.0) (2020-12-23) + +### Features + +* Introduces `on_request` and `on_complete` methods in `Faraday::Middleware`. (#1194, @iMacTia) + +### Fixes + +* Require 'date' to avoid retry exception (#1206, @rustygeldmacher) +* Fix rdebug recursion issue (#1205, @native-api) +* Update call to `em_http_ssl_patch` (#1202, @kylekeesling) +* `EmHttp` adapter: drop superfluous loaded? check (#1213, @olleolleolle) +* Avoid 1 use of keyword hackery (#1211, @grosser) +* Fix #1219 `Net::HTTP` still uses env proxy (#1221, @iMacTia) + +### Documentation + +* Add comment in gemspec to explain exposure of `examples` and `spec` folders. (#1192, @iMacTia) +* Adapters, how to create them (#1193, @olleolleolle) +* Update documentation on using the logger (#1196, @tijmenb) +* Adjust the retry documentation and spec to align with implementation (#1198, @nbeyer) + +### Misc + +* Test against ruby head (#1208, @grosser) + +## [v1.1.0](https://github.com/lostisland/faraday/releases/tag/v1.1.0) (2020-10-17) + +### Features + +* Makes parameters sorting configurable (#1162 @wishdev) +* Introduces `flat_encode` option for multipart adapter. (#1163 @iMacTia) +* Include request info in exceptions raised by RaiseError Middleware (#1181 @SandroDamilano) + +### Fixes + +* Avoid `last arg as keyword param` warning when building user middleware on Ruby 2.7 (#1153 @dgholz) +* Limits net-http-persistent version to < 4.0 (#1156 @iMacTia) +* Update `typhoeus` to new stable version (`1.4`) (#1159 @AlexWayfer) +* Properly fix test failure with Rack 2.1+. (#1171 @voxik) + +### Documentation + +* Improves documentation on how to contribute to the site by using Docker. (#1175 @iMacTia) +* Remove retry_change_requests from documentation (#1185 @stim371) + +### Misc + +* Link from GitHub Actions badge to CI workflow (#1141 @olleolleolle) +* Return tests of `Test` adapter (#1147 @AlexWayfer) +* Add 1.0 release to wording in CONTRIBUTING (#1155 @olleolleolle) +* Fix linting bumping Rubocop to 0.90.0 (#1182 @iMacTia) +* Drop `git ls-files` in gemspec (#1183 @utkarsh2102) +* Upgrade CI to ruby/setup-ruby (#1187 @gogainda) + +## [v1.0.1](https://github.com/lostisland/faraday/releases/tag/v1.0.1) (2020-03-29) + +### Fixes + +* Use Net::HTTP#start(&block) to ensure closed TCP connections (#1117) +* Fully qualify constants to be checked (#1122) +* Allows `parse` method to be private/protected in response middleware (#1123) +* Encode Spaces in Query Strings as '%20' Instead of '+' (#1125) +* Limits rack to v2.0.x (#1127) +* Adapter Registry reads also use mutex (#1136) + +### Documentation + +* Retry middleware documentation fix (#1109) +* Docs(retry): precise usage of retry-after (#1111) +* README: Link the logo to the website (#1112) +* Website: add search bar (#1116) +* Fix request/response mix-up in docs text (#1132) + +## [v1.0](https://github.com/lostisland/faraday/releases/tag/v1.0.0) (2020-01-22) Features: @@ -44,6 +335,13 @@ Misc: * Describe clearing cached stubs #1045 (@viraptor) * Add project metadata to the gemspec #1046 (@orien) +## v0.17.4 + +Fixes: + +* NetHttp adapter: wrap Errno::EADDRNOTAVAIL (#1114, @embs) +* Fix === for subclasses of deprecated classes (#1243, @mervync) + ## v0.17.3 Fixes: diff --git a/Gemfile b/Gemfile index 9bce7bf74..a053bb8ee 100644 --- a/Gemfile +++ b/Gemfile @@ -2,32 +2,32 @@ source 'https://rubygems.org' -ruby RUBY_VERSION - -gem 'jruby-openssl', '~> 0.8.8', platforms: :jruby -gem 'rake' +# Even though we don't officially support JRuby, this dependency makes Faraday +# compatible with it, so we're leaving it in for jruby users to use it. +gem 'jruby-openssl', '~> 0.11.0', platforms: :jruby group :development, :test do + gem 'bake-test-external' + gem 'coveralls_reborn', require: false gem 'pry' -end - -group :test do - gem 'coveralls', require: false - gem 'em-http-request', '>= 1.1', require: 'em-http' - gem 'em-synchrony', '>= 1.0.3', require: %w[em-synchrony em-synchrony/em-http] - gem 'excon', '>= 0.27.4' - gem 'httpclient', '>= 2.2' - gem 'multipart-parser' - gem 'net-http-persistent' - gem 'patron', '>= 0.4.2', platforms: :ruby - gem 'rack-test', '>= 0.6', require: 'rack/test' + gem 'rack', '~> 3.0' + gem 'rake' gem 'rspec', '~> 3.7' gem 'rspec_junit_formatter', '~> 0.4' - gem 'rubocop-performance', '~> 1.0' gem 'simplecov' - gem 'typhoeus', '~> 1.3', git: 'https://github.com/typhoeus/typhoeus.git', - require: 'typhoeus' gem 'webmock', '~> 3.4' end +group :development, :lint do + gem 'racc', '~> 1.7' # for RuboCop, on Ruby 3.3 + gem 'rubocop' + gem 'rubocop-packaging', '~> 0.5' + gem 'rubocop-performance', '~> 1.0' + gem 'yard-junk' +end + +group :deployment do + gem 'rubygems-await', github: 'segiddins/rubygems-await', ref: 'f5e2b0413ec6f17e35d9bb7902dcb28b31804701' +end + gemspec diff --git a/LICENSE.md b/LICENSE.md index 2e3a1b55a..38776159a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2009-2019 Rick Olson, Zack Hobson +Copyright (c) 2009-2023 Rick Olson, Zack Hobson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 3c8d5b757..0cca6db87 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,41 @@ -# ![Faraday](./docs/assets/img/repo-card-slim.png) +# [![Faraday](./docs/_media/home-logo.svg)][website] [![Gem Version](https://badge.fury.io/rb/faraday.svg)](https://rubygems.org/gems/faraday) -![GitHub Actions CI](https://github.com/lostisland/faraday/workflows/CI/badge.svg) -[![Test Coverage](https://api.codeclimate.com/v1/badges/f869daab091ceef1da73/test_coverage)](https://codeclimate.com/github/lostisland/faraday/test_coverage) -[![Maintainability](https://api.codeclimate.com/v1/badges/f869daab091ceef1da73/maintainability)](https://codeclimate.com/github/lostisland/faraday/maintainability) -[![Gitter](https://badges.gitter.im/lostisland/faraday.svg)](https://gitter.im/lostisland/faraday?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![GitHub Actions CI](https://github.com/lostisland/faraday/workflows/CI/badge.svg)](https://github.com/lostisland/faraday/actions?query=workflow%3ACI) +[![GitHub Discussions](https://img.shields.io/github/discussions/lostisland/faraday?logo=github)](https://github.com/lostisland/faraday/discussions) +Faraday is an HTTP client library abstraction layer that provides a common interface over many +adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle. +Take a look at [Awesome Faraday][awesome] for a list of available adapters and middleware. -Faraday is an HTTP client library that provides a common interface over many -adapters (such as Net::HTTP) and embraces the concept of Rack middleware when -processing the request/response cycle. +## Why use Faraday? + +Faraday gives you the power of Rack middleware for manipulating HTTP requests and responses, +making it easier to build sophisticated API clients or web service libraries that abstract away +the details of how HTTP requests are made. + +Faraday comes with a lot of features out of the box, such as: +* Support for multiple adapters (Net::HTTP, Typhoeus, Patron, Excon, HTTPClient, and more) +* Persistent connections (keep-alive) +* Parallel requests +* Automatic response parsing (JSON, XML, YAML) +* Customization of the request/response cycle with middleware +* Support for streaming responses +* Support for uploading files +* And much more! ## Getting Started The best starting point is the [Faraday Website][website], with its introduction and explanation. -Need more details? See the [Faraday API Documentation][apidoc] to see how it works internally. -## Supported Ruby versions +Need more details? See the [Faraday API Documentation][apidoc] to see how it works internally, or take a look at [Advanced techniques for calling HTTP APIs in Ruby](https://mattbrictson.com/blog/advanced-http-techniques-in-ruby) blog post from [@mattbrictson](https://github.com/mattbrictson) 🚀 -This library aims to support and is [tested against][actions] the following Ruby -implementations: +## Supported Ruby versions -* Ruby 2.4+ +This library aims to support and is [tested against][actions] the currently officially supported Ruby +implementations. This means that, even without a major release, we could add or drop support for Ruby versions, +following their [EOL](https://endoflife.date/ruby). +Currently that means we support Ruby 3.0+ If something doesn't work on one of these Ruby versions, it's a bug. @@ -43,13 +57,11 @@ Open the issues page and check for the `help wanted` label! But before you start coding, please read our [Contributing Guide][contributing] ## Copyright -© 2009 - 2019, the [Faraday Team][faraday_team]. Website and branding design by [Elena Lo Piccolo](https://elelopic.design). - -[website]: https://lostisland.github.io/faraday -[faraday_team]: https://lostisland.github.io/faraday/team -[contributing]: https://github.com/lostisland/faraday/blob/master/.github/CONTRIBUTING.md -[apidoc]: http://www.rubydoc.info/gems/faraday -[actions]: https://github.com/lostisland/faraday/actions -[jruby]: http://jruby.org/ -[rubinius]: http://rubini.us/ -[license]: LICENSE.md + +© 2009 - 2023, the Faraday Team. Website and branding design by [Elena Lo Piccolo](https://elelopic.design). + +[awesome]: https://github.com/lostisland/awesome-faraday/#adapters +[website]: https://lostisland.github.io/faraday +[contributing]: https://github.com/lostisland/faraday/blob/main/.github/CONTRIBUTING.md +[apidoc]: https://www.rubydoc.info/github/lostisland/faraday +[actions]: https://github.com/lostisland/faraday/actions diff --git a/Rakefile b/Rakefile index cffdd09be..a98c5113f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,12 @@ # frozen_string_literal: true require 'rspec/core/rake_task' +require 'bundler' -RSpec::Core::RakeTask.new(:spec) +Bundler::GemHelper.install_tasks + +RSpec::Core::RakeTask.new(:spec) do |task| + task.ruby_opts = %w[-W] +end task default: :spec diff --git a/UPGRADING.md b/UPGRADING.md index b2acbf303..dd0228901 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,3 +1,134 @@ +## Faraday 2.0 + +### Adapters have moved! + +With this release, we've officially moved all adapters, except for the `net_http` one, out of Faraday. +What that means, is that they won't be available out-of-the-box anymore, +and you'll instead need to add them to your Gemfile. + +**NOTE: the `net_http` adapter was originally removed as well in version `2.0`, but quickly reintroduced in version `2.0.1`. +We strongly suggest you to skip version `2.0` and instead use version `2.0.1` or greater.** + +#### Why was this decision made? + +We've taken this decision for the following technical reasons: + +* We wanted the Faraday gem to focus on providing a clean, standardized, middleware-stack-based API. +* We wanted to free the core team from maintaining all the different adapters, relying more on the community to + maintain them based on the broad interest. This will allow the core team to focus on implementing features + focused on the Faraday API more quickly, without having to push it on all adapters immediately. +* With the community creating more and more adapters, we wanted to avoid having first and second-class adapters + by having some of them included with the gem and others available externally. +* Moving adapters into separate gems allow to solve the dependency issues once and for all. + Faraday will remain a dependency-free gem, while adapter gems will be able to automatically pull + any necessary dependency, without having to rely on the developer to do so. + +#### So what will this mean for me? + +We did our best to make this transition as painless as possible for you, so here is what we did. + +* All adapters, except for the `net_http` one, have already been moved out and released as separate gems. + They've then been re-added into Faraday's v1 dependencies so that you wouldn't notice. + This means that immediately after v2.0 will be released, all the adapters that were previously available will be + already compatible with Faraday 2.0! +* We've setup an [Awesome Faraday](https://github.com/lostisland/awesome-faraday) repository, where you can find and discover adapters. + We also highlighted their unique features and level of compliance with Faraday's features. + +#### That's great! What should I change in my code immediately after upgrading? + +* If you just use the default `net_http` adapter, then you don't need to do anything! +* Otherwise, add the corresponding adapter gem to your Gemfile (e.g. `faraday-net_http_persistent`). Then, simply require them after you require `faraday`. + ```ruby + # Gemfile + gem 'faraday' + gem 'faraday-net_http_persistent' + + # Code + require 'faraday' + require 'faraday/net_http_persistent' + ``` + +### Faraday Middleware Deprecation + +In case you never used it, [Faraday Middleware](https://github.com/lostisland/faraday_middleware) is a handy collection of middleware that have so far been maintained by the Faraday core team. +With Faraday 2.0 focusing on becoming an ecosystem, rather than an out-of-the-box solution, it only made sense to take the same approach for middleware as we did for adapters. For this reason, `faraday_middleware` will not be updated to support Faraday 2.0. +Instead, we'll support the transition from centralised-repo collection of middleware to individual middleware gems, effectively replicating what we did with adapters. Each middleware will have its own repository and gem, and it will allow developers to only add the ones they require to their Gemfile. + +So don't fret yet! We're doing our best to support our `faraday_middleware` users out there, so here are the steps we've taken to make this work: +* We've promoted the highly popular JSON middleware (both request encoding and response parsing) to a core middleware and it will now be shipped together with Faraday. We expect many `faraday_middleware` users will be able to just stop using the extra dependency thanks to this. +* We've created a [faraday-middleware-template](https://github.com/lostisland/faraday-middleware-template) repository that will make creating a new middleware gem simple and straightforward. +* We've added a middleware section to the [awesome-faraday](https://github.com/lostisland/awesome-faraday) repo, so you can easily find available middleware when you need it. + +It will obviously take time for some of the middleware in `faraday_middleware` to make its way into a separate gem, so we appreciate this might be an upgrade obstacle for some. However this is part of an effort to focus the core team resources tackling the most requested features. +We'll be listening to the community and prioritize middleware that are most used, and will be welcoming contributors who want to become owners of the middleware when these become separate from the `faraday_middleware` repo. + +### Bundled middleware moved out + +Moving middleware into its own gem makes sense not only for `faraday_middleware`, but also for middleware bundled with Faraday. +As of v2.0, the `retry` and `multipart` middleware have been moved to separate `faraday-retry` and `faraday-multipart` gems. +These have been identified as good candidates due to their complexity and external dependencies. +Thanks to this change, we were able to make Faraday 2.0 completely dependency free 🎉 (the only exception being `ruby2_keywords`, which will be necessary only while we keep supporting Ruby 2.6). + +#### So what should I do if I currently use the `retry` or `multipart` middleware? + +Upgrading is pretty simple, because the middleware was simply moved out to external gems. +All you need to do is to add them to your gemfile (either `faraday-retry` or `faraday-multipart` and require them before usage: + +```ruby +# Gemfile +gem 'faraday-multipart' +gem 'faraday-retry' + +# Connection initializer +require 'faraday/retry' +require 'faraday/multipart' + +conn = Faraday.new(url) do |f| + f.request :multipart + f.request :retry + # ... +end +``` + +### Autoloading and dependencies + +Faraday has until now provided and relied on a complex dynamic dependencies system. +This would allow to reference classes and require dependencies only when needed (e.g. when running the first request) based +on the middleware/adapters used. +As part of Faraday v2.0, we've removed all external dependencies, which means we don't really need this anymore. +This change should not affect you directly, but if you're registering middleware then be aware of the new syntax: + +```ruby +# `name` here can be anything you want. +# `klass` is your custom middleware class. +# This method can also be called on `Faraday::Adapter`, `Faraday::Request` and `Faraday::Response` +Faraday::Middleware.register_middleware(name: klass) +``` + +The `register_middleware` method also previously allowed to provide a symbol, string, proc, or array +but this has been removed from the v2.0 release to simplify the interface. +(EDIT: symbol/string/proc have subsequently been reintroduced in v2.2, but not the array). + +### Authentication helper methods in Connection have been removed + +You were previously able to call `authorization`, `basic_auth` and `token_auth` against the `Connection` object, but this helper methods have now been dropped. +Instead, you should be using the equivalent middleware, as explained in [this page](https://lostisland.github.io/faraday/#/middleware/included/authentication). + +For more details, see https://github.com/lostisland/faraday/pull/1306 + +### The `dependency` method in middlewares has been removed + +In Faraday 1, a deferred require was used with the `dependency` method. + +In Faraday 2, that method has been removed. In your middleware gems, use a regular `require` at the top of the file, + +### Others + +* Rename `Faraday::Request#method` to `#http_method`. +* Remove `Faraday::Response::Middleware`. You can now use the new `on_complete` callback provided by `Faraday::Middleware`. +* `Faraday.default_connection_options` will now be deep-merged into new connections to avoid overriding them (e.g. headers). +* `Faraday::Builder#build` method is not exposed through `Faraday::Connection` anymore and does not reset the handlers if called multiple times. This method should be used internally only. + ## Faraday 1.0 ### Errors @@ -52,4 +183,3 @@ conn = Faraday.new(...) do |f| f.adapter AnyAdapter end ``` - diff --git a/bin/console b/bin/console new file mode 100755 index 000000000..40446e2f1 --- /dev/null +++ b/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'faraday' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require 'irb' +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 000000000..ff8b4096c --- /dev/null +++ b/bin/setup @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +gem install bundler +bundle install --jobs 4 diff --git a/bin/test b/bin/test new file mode 100755 index 000000000..92920beec --- /dev/null +++ b/bin/test @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle exec rubocop -a --format progress +bundle exec rspec diff --git a/config/external.yaml b/config/external.yaml new file mode 100644 index 000000000..fbc232921 --- /dev/null +++ b/config/external.yaml @@ -0,0 +1,3 @@ +faraday-net_http: + url: https://github.com/lostisland/faraday-net_http.git + command: bundle exec rspec diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 45c150536..000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -_site -.sass-cache -.jekyll-metadata diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/docs/404.html b/docs/404.html deleted file mode 100644 index c472b4ea0..000000000 --- a/docs/404.html +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: default ---- - - - -
-

404

- -

Page not found :(

-

The requested page could not be found.

-
diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index 14d91b3d4..000000000 --- a/docs/Gemfile +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -source 'https://rubygems.org' - -# Hello! This is where you manage which Jekyll version is used to run. -# When you want to use a different version, change it below, save the -# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: -# -# bundle exec jekyll serve -# -# This will help ensure the proper Jekyll version is running. -# Happy Jekylling! -# gem "jekyll", "~> 3.7.4" - -# This is the default theme for new Jekyll sites. -# You may change this to anything you like. -# gem "minima", "~> 2.0" -# gem "jekyll-theme-type" -gem 'jekyll-remote-theme' - -# If you want to use GitHub Pages, remove the "gem "jekyll"" above and -# uncomment the line below. To upgrade, run `bundle update github-pages`. -gem 'github-pages', group: :jekyll_plugins - -# If you have any plugins, put them here! -group :jekyll_plugins do - gem 'jekyll-feed', '~> 0.6' -end - -# Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] - -# Performance-booster for watching directories on Windows -gem 'wdm', '~> 0.1.0' if Gem.win_platform? diff --git a/docs/README.md b/docs/README.md index 26127d74a..99aa42eb0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,19 +1,21 @@ -# Faraday Website +# Faraday Docs -This is the root directory of the [Faraday Website][website]. -If you want to apply changes to it, please test it locally using `Jekyll`. +Faraday Docs are powered by [Docsify](https://docsify.js.org/#/). -```bash -# Navigate into the /docs folder -$ cd docs +## Development -# Install Jekyll dependencies, this bundle is different from Faraday's one. -$ bundle install +### Setup -# Run the Jekyll server with the Faraday website -$ bundle exec jekyll serve +From the Faraday project root, run the following: -# The site will now be reachable at http://127.0.0.1:4000/faraday/ +```bash +npm install # this will install the necessary dependencies ``` -[website]: https://lostisland.github.io/faraday \ No newline at end of file +### Running the Docs Locally + +To preview your changes locally, run the following: + +```bash +npm run docs +``` diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 391c821ce..000000000 --- a/docs/_config.yml +++ /dev/null @@ -1,54 +0,0 @@ -# SITE CONFIGURATION -url: 'https://lostisland.github.io' -baseurl: '/faraday' - -# THEME-SPECIFIC CONFIGURATION -theme_settings: - title: Faraday - avatar: assets/img/logo.png - favicon: assets/img/favicon.png - # email: your-email@example.com - description: >- - Simple, but flexible HTTP client library, with support for multiple backends - footer_text: "© 2009 - 2019, the Faraday Team. Website and branding design by Elena Lo Piccolo." - - # Icons - github: 'lostisland/faraday' - gitter: 'lostisland/faraday' - - # Post navigation - post_navigation: true - site_navigation_sort: 'order' - -# BUILD SETTINGS -markdown: kramdown -remote_theme: rohanchandra/type-theme - -plugins: - - jekyll-feed - - jekyll-remote-theme - -# GitHub settings -lsi: false -safe: true -#source: [your repo's top level directory] -incremental: false -highlighter: rouge -gist: - noscript: false -kramdown: - math_engine: mathjax - syntax_highlighter: rouge - -# Exclude from processing. -# The following items will not be processed, by default. Create a custom list -# to override the default setting. -exclude: - - Gemfile - - Gemfile.lock - - README.md - - node_modules - - vendor/bundle/ - - vendor/cache/ - - vendor/gems/ - - vendor/ruby/ diff --git a/docs/_includes/docs_nav.md b/docs/_includes/docs_nav.md deleted file mode 100644 index 098d23627..000000000 --- a/docs/_includes/docs_nav.md +++ /dev/null @@ -1,17 +0,0 @@ -
-

- {% if page.prev_link %} - {{ page.prev_name }} - {% endif %} -

-

- {% if page.top_link %} - {{ page.top_name }} - {% endif %} -

-

- {% if page.next_link %} - {{ page.next_name }} - {% endif %} -

-
\ No newline at end of file diff --git a/docs/_layouts/documentation.md b/docs/_layouts/documentation.md deleted file mode 100644 index 21ead50da..000000000 --- a/docs/_layouts/documentation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -layout: page ---- - -{{ content }} - -{% include docs_nav.md %} diff --git a/docs/assets/img/favicon.png b/docs/_media/favicon.png similarity index 100% rename from docs/assets/img/favicon.png rename to docs/_media/favicon.png diff --git a/docs/assets/img/home-logo.svg b/docs/_media/home-logo.svg similarity index 100% rename from docs/assets/img/home-logo.svg rename to docs/_media/home-logo.svg diff --git a/docs/_media/logo.png b/docs/_media/logo.png new file mode 100644 index 000000000..5a2a95a2a Binary files /dev/null and b/docs/_media/logo.png differ diff --git a/docs/assets/img/middleware.png b/docs/_media/middleware.png similarity index 100% rename from docs/assets/img/middleware.png rename to docs/_media/middleware.png diff --git a/docs/_media/overview.png b/docs/_media/overview.png new file mode 100644 index 000000000..4958f0202 Binary files /dev/null and b/docs/_media/overview.png differ diff --git a/docs/assets/img/repo-card-slim.png b/docs/_media/repo-card-slim.png similarity index 100% rename from docs/assets/img/repo-card-slim.png rename to docs/_media/repo-card-slim.png diff --git a/docs/assets/img/repo-card.png b/docs/_media/repo-card.png similarity index 100% rename from docs/assets/img/repo-card.png rename to docs/_media/repo-card.png diff --git a/docs/_posts/2019-03-12-welcome-to-jekyll.markdown b/docs/_posts/2019-03-12-welcome-to-jekyll.markdown deleted file mode 100644 index 386d4f0a8..000000000 --- a/docs/_posts/2019-03-12-welcome-to-jekyll.markdown +++ /dev/null @@ -1,25 +0,0 @@ ---- -layout: post -title: "Welcome to Jekyll!" -date: 2019-03-12 10:25:23 +0000 -categories: jekyll update ---- -You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. - -To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works. - -Jekyll also offers powerful support for code snippets: - -{% highlight ruby %} -def print_hi(name) - puts "Hi, #{name}" -end -print_hi('Tom') -#=> prints 'Hi, Tom' to STDOUT. -{% endhighlight %} - -Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. - -[jekyll-docs]: https://jekyllrb.com/docs/home -[jekyll-gh]: https://github.com/jekyll/jekyll -[jekyll-talk]: https://talk.jekyllrb.com/ diff --git a/docs/_sass/_variables.scss b/docs/_sass/_variables.scss deleted file mode 100644 index c1de38999..000000000 --- a/docs/_sass/_variables.scss +++ /dev/null @@ -1,8 +0,0 @@ -// Override theme variables. - -@import url('https://fonts.googleapis.com/css?family=Raleway:700'); - -$link-color: #EE4266; -$text-color: #3C3C3C; -$font-family-main: 'KohinoorTelugu-Regular', Helvetica, Arial, sans-serif; -$font-family-headings: 'Raleway', Helvetica, Arial, sans-serif; \ No newline at end of file diff --git a/docs/_sass/faraday.sass b/docs/_sass/faraday.sass deleted file mode 100644 index 61f0143f2..000000000 --- a/docs/_sass/faraday.sass +++ /dev/null @@ -1,122 +0,0 @@ -// Custom Styles added on top of the theme. - -.btn - display: inline-block - background-color: $link-color - padding: 5px 10px - box-shadow: 0 4px 10px 5px rgba(238, 66, 102, 0.30) - border-radius: 20px - width: 200px - color: #FFFFFF - letter-spacing: -0.41px - text-align: center - margin: 0 10px - - &:hover - background-color: darken($link-color, 10%) - color: white - text-decoration: none - -.text-center - text-align: center - -.mt-60 - margin-top: 60px - -.hidden - display: none - -.docs-nav - display: flex - margin-top: 40px - -.docs-nav-item - flex: 1 1 0 - text-align: center - -pre.highlight - padding: 20px - background-color: #F6F6F6 - border-radius: 4px - - code - word-wrap: normal - overflow: scroll - -code.highlighter-rouge - background-color: #EEE - padding: 0 5px - border-radius: 3px - -.site-header .site-nav li - margin-right: 1.2em - -h1, h2, h3, h4, h5, h6 - font-weight: bold - -.feature-image header - @media (max-width: 1000px) - padding: 7% 12.5% - @media (max-width: 576px) - padding: 4% 5% 1% 5% - -#team-content - h3 - margin: 30px 0 - -#contributors-list - text-align: justify - -.team-tile - width: 200px - display: inline-block - margin: 0 20px - - img - width: 100% - border-radius: 50% - -footer - background-color: #f1f3f4 - -#active-maintainers-list, #historical-team-list - text-align: center - -#loader - margin-top: 20% - margin-bottom: 20% - text-align: center - -.lds-ring - display: inline-block - position: relative - width: 200px - height: 200px - -.lds-ring div - box-sizing: border-box - display: block - position: absolute - width: 187px - height: 187px - margin: 6px - border: 12px solid $link-color - border-radius: 50% - animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite - border-color: $link-color transparent transparent transparent - -.lds-ring div:nth-child(1) - animation-delay: -0.45s - -.lds-ring div:nth-child(2) - animation-delay: -0.3s - -.lds-ring div:nth-child(3) - animation-delay: -0.15s - -@keyframes lds-ring - 0% - transform: rotate(0deg) - - 100% - transform: rotate(360deg) diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 000000000..fdbcb5a57 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,32 @@ +* Getting Started + * [Quick Start](getting-started/quick-start.md) + * [The Env Object](getting-started/env-object.md) + * [Dealing with Errors](getting-started/errors.md) +* Customization + * [Configuration](customization/index.md) + * [Connection Options](customization/connection-options.md) + * [Request Options](customization/request-options.md) + * [SSL Options](customization/ssl-options.md) + * [Proxy Options](customization/proxy-options.md) +* Middleware + * [Overview](middleware/index.md) + * [Included](middleware/included/index.md) + * [Authentication](middleware/included/authentication.md) + * [URL Encoding](middleware/included/url-encoding.md) + * [JSON Encoding/Decoding](middleware/included/json.md) + * [Instrumentation](middleware/included/instrumentation.md) + * [Logging](middleware/included/logging.md) + * [Raising Errors](middleware/included/raising-errors.md) + * [Writing custom middleware](middleware/custom-middleware.md) +* Adapters + * [Overview](adapters/index.md) + * [Net::HTTP](adapters/net-http.md) + * [Test Adapter](adapters/test-adapter.md) + * Writing custom adapters + * [Overview](adapters/custom/index.md) + * [Parallel Requests](adapters/custom/parallel-requests.md) + * [Streaming Responses](adapters/custom/streaming.md) + * [Test your adapter](adapters/custom/testing.md) +* Advanced Features + * [Parallel Requests](advanced/parallel-requests.md) + * [Streaming Responses](advanced/streaming-responses.md) diff --git a/docs/adapters/custom/index.md b/docs/adapters/custom/index.md new file mode 100644 index 000000000..179412281 --- /dev/null +++ b/docs/adapters/custom/index.md @@ -0,0 +1,161 @@ +# Writing custom adapters + +!> A template for writing your own middleware is available in the [faraday-adapter-template](https://github.com/lostisland/faraday-adapter-template) repository. + +Adapters have methods that can help you implement support for a new backend. + +This example will use a fictional HTTP backend gem called `FlorpHttp`. It doesn't +exist. Its only function is to make this example more concrete. + +## An Adapter _is_ a Middleware + +When you subclass `Faraday::Adapter`, you get helpful methods defined and all you need to do is to +extend the `call` method (remember to call `super` inside it!): + +```ruby +module Faraday + class Adapter + class FlorpHttp < Faraday::Adapter + def call(env) + super + # Perform the request and call `save_response` + end + end + end +end +``` + +Now, there are only two things which are actually mandatory for an adapter middleware to function: + +- a `#call` implementation +- a call to `#save_response` inside `#call`, which will keep the Response around. + +These are the only two things, the rest of this text is about methods which make the authoring easier. + +Like any other middleware, the `env` parameter passed to `#call` is an instance of [Faraday::Env][env-object]. +This object will contain all the information about the request, as well as the configuration of the connection. +Your adapter is expected to deal with SSL and Proxy settings, as well as any other configuration options. + +## Connection options and configuration block + +Users of your adapter have two main ways of configuring it: +* connection options: these can be passed to your adapter initializer and are automatically stored into an instance variable `@connection_options`. +* configuration block: this can also be provided to your adapter initializer and it's stored into an instance variable `@config_block`. + +Both of these are automatically managed by `Faraday::Adapter#initialize`, so remember to call it with `super` if you create an `initialize` method in your adapter. +You can then use them in your adapter code as you wish, since they're pretty flexible. + +Below is an example of how they can be used: + +```ruby +# You can use @connection_options and @config_block in your adapter code +class FlorpHttp < Faraday::Adapter + def call(env) + # `connection` internally calls `build_connection` and yields the result + connection do |conn| + # perform the request using configured `conn` + end + end + + def build_connection(env) + conn = FlorpHttp::Client.new(pool_size: @connection_options[:pool_size] || 10) + @config_block&.call(conn) + conn + end +end + +# Then your users can provide them when initializing the connection +Faraday.new(...) do |f| + # ... + # in this example, { pool_size: 5 } will be provided as `connection_options` + f.adapter :florp_http, pool_size: 5 do |client| + # this block is useful to set properties unique to HTTP clients that are not + # manageable through the Faraday API + client.some_fancy_florp_http_property = 10 + end +end +``` + +## Implementing `#close` + +Just like middleware, adapters can implement a `#close` method. It will be called when the connection is closed. +In this method, you should close any resources that you opened in `#initialize` or `#call`, like sockets or files. + +```ruby +class FlorpHttp < Faraday::Adapter + def close + @socket.close if @socket + end +end +``` + +## Helper methods + +`Faraday::Adapter` provides some helper methods to make it easier to implement adapters. + +### `#save_response` + +The most important helper method and the only one you're expected to call from your `#call` method. +This method is responsible for, among other things, the following: +* Take the `env` object and save the response into it. +* Set the `:response` key in the `env` object. +* Parse headers using `Utils::Headers` and set the `:response_headers` key in the `env` object. +* Call `#finish` on the `Response` object, triggering the `#on_complete` callbacks in the middleware stack. + +```ruby +class FlorpHttp < Faraday::Adapter + def call(env) + super + # Perform the request using FlorpHttp. + # Returns a FlorpHttp::Response object. + response = FlorpHttp.perform_request(...) + + save_response(env, response.status, response.body, response.headers, response.reason_phrase) + end +end +``` + +`#save_response` also accepts a `finished` keyword argument, which defaults to `true`, but that you can set to false +if you don't want it to call `#finish` on the `Response` object. + +### `#request_timeout` + +Most HTTP libraries support different types of timeouts, like `:open_timeout`, `:read_timeout` and `:write_timeout`. +Faraday let you set individual values for each of these, as well as a more generic `:timeout` value on the request options. + +This helper method knows about supported timeout types, and falls back to `:timeout` if they are not set. +You can use those when building the options you need for your backend's instantiation. + +```ruby +class FlorpHttp < Faraday::Adapter + def call(env) + super + # Perform the request using FlorpHttp. + # Returns a FlorpHttp::Response object. + response = FlorpHttp.perform_request( + # ... other options ..., + open_timeout: request_timeout(:open, env[:request]), + read_timeout: request_timeout(:read, env[:request]), + write_timeout: request_timeout(:write, env[:request]) + ) + + # Call save_response + end +end +``` + +## Register your adapter + +Like middleware, you may register a nickname for your adapter. +People can then refer to your adapter with that name when initializing their connection. +You do that using `Faraday::Adapter.register_middleware`, like this: + +```ruby +class FlorpHttp < Faraday::Adapter + # ... +end + +Faraday::Adapter.register_middleware(florp_http: FlorpHttp) +``` + +[env-object]: /getting-started/env-object.md diff --git a/docs/adapters/custom/parallel-requests.md b/docs/adapters/custom/parallel-requests.md new file mode 100644 index 000000000..8431dc3f0 --- /dev/null +++ b/docs/adapters/custom/parallel-requests.md @@ -0,0 +1,59 @@ +# Adding support for parallel requests + +!> This is slightly more involved, and this section is not fully formed. + +Vague example, excerpted from [the test suite about parallel requests](https://github.com/lostisland/faraday/blob/main/spec/support/shared_examples/request_method.rb#L179) + +```ruby +response_1 = nil +response_2 = nil + +conn.in_parallel do + response_1 = conn.get('/about') + response_2 = conn.get('/products') +end + +puts response_1.status +puts response_2.status +``` + +First, in your class definition, you can tell Faraday that your backend supports parallel operation: + +```ruby +class FlorpHttp < ::Faraday::Adapter + dependency do + require 'florp_http' + end + + self.supports_parallel = true +end +``` + +Then, implement a method which returns a ParallelManager: + +```ruby +class FlorpHttp < ::Faraday::Adapter + dependency do + require 'florp_http' + end + + self.supports_parallel = true + + def self.setup_parallel_manager(_options = nil) + FlorpParallelManager.new # NB: we will need to define this + end +end + +class FlorpParallelManager + def add(request, method, *args, &block) + # Collect the requests + end + + def run + # Process the requests + end +end +``` + +Compare to the finished example [em-synchrony](https://github.com/lostisland/faraday-em_synchrony/blob/main/lib/faraday/adapter/em_synchrony.rb) +and its [ParallelManager implementation](https://github.com/lostisland/faraday-em_synchrony/blob/main/lib/faraday/adapter/em_synchrony/parallel_manager.rb). diff --git a/docs/adapters/custom/streaming.md b/docs/adapters/custom/streaming.md new file mode 100644 index 000000000..6feba81af --- /dev/null +++ b/docs/adapters/custom/streaming.md @@ -0,0 +1,79 @@ +# Adding support for streaming + +Faraday supports streaming responses, which means that the response body is not loaded into memory all at once, +but instead it is read in chunks. This can be particularly useful when dealing with large responses. +Not all HTTP clients support this, so it is not required for adapters to support it. + +However, if you do want to support it in your adapter, you can do so by leveraging helpers provided by the env object. +Let's see an example implementation first with some comments, and then we'll explain it in more detail: + +```ruby +module Faraday + class Adapter + class FlorpHttp < Faraday::Adapter + def call(env) + super + if env.stream_response? # check if the user wants to stream the response + # start a streaming response. + # on_data is a block that will let users consume the response body + http_response = env.stream_response do |&on_data| + # perform the request using FlorpHttp + # the block will be called for each chunk of data + FlorpHttp.perform_request(...) do |chunk| + on_data.call(chunk) + end + end + # the body is already consumed by the block + # so it's good practice to set it to nil + http_response.body = nil + else + # perform the request normally, no streaming. + http_response = FlorpHttp.perform_request(...) + end + save_response(env, http_response.status, http_response.body, http_response.headers, http_response.reason_phrase) + end + end + end +end +``` + +## How it works + +### `#stream_response?` + +The first helper method we use is `#stream_response?`. This method is provided by the env object and it returns true +if the user wants to stream the response. This is controlled by the presence of an `on_data` callback in the request options. + +### `#stream_response` + +The second helper is `#stream_response`. This method is also provided by the env object and it takes a block. +The block will be called with a single argument, which is a callback that the user can use to consume the response body. +All your adapter needs to do, is to call this callback with each chunk of data that you receive from the server. + +The `on_data` callback will internally call the callback provided by the user, so you don't need to worry about that. +It will also keep track of the number of bytes that have been read, and pass that information to the user's callback. + +To see how this all works together, let's see an example of how a user would use this feature: + +```ruby +# A buffer to store the streamed data +streamed = [] + +conn = Faraday.new('http://httpbingo.org') +conn.get('/stream/100') do |req| + # Set a callback which will receive tuples of chunk Strings, + # the sum of characters received so far, and the response environment. + # The latter will allow access to the response status, headers and reason, as well as the request info. + req.options.on_data = proc do |chunk, overall_received_bytes, env| + puts "Received #{overall_received_bytes} characters" + streamed << chunk + end +end + +# Joins all response chunks together +streamed.join +``` + +For more details on the user experience, check the [Streaming Responses] page. + +[Streaming Responses]: /advanced/streaming-responses.md diff --git a/docs/adapters/custom/testing.md b/docs/adapters/custom/testing.md new file mode 100644 index 000000000..032ed3002 --- /dev/null +++ b/docs/adapters/custom/testing.md @@ -0,0 +1,60 @@ +# Test your custom adapter + +Faraday puts a lot of expectations on adapters, but it also provides you with useful tools to test your adapter +against those expectations. This guide will walk you through the process of testing your adapter. + +## The adapter test suite + +Faraday provides a test suite that you can use to test your adapter. +The test suite is located in the `spec/external_adapters/faraday_specs_setup.rb`. + +All you need to do is to `require 'faraday_specs_setup'` in your adapter's `spec_helper.rb` file. +This will load the `an adapter` shared example group that you can use to test your adapter. + +```ruby +require 'faraday_specs_setup' + +RSpec.describe Faraday::Adapter::FlorpHttp do + it_behaves_like 'an adapter' + + # You can then add any other test specific to this adapter here... +end +``` + +## Testing optional features + +By default, `an adapter` will test your adapter against the required behaviour for an adapter. +However, there are some optional "features" that your adapter can implement, like parallel requests or streaming. + +If your adapter implements any of those optional features, you can test it against those extra expectations +by calling the `features` method: + +```ruby +RSpec.describe Faraday::Adapter::MyAdapter do + # Since not all adapters support all the features Faraday has to offer, you can use + # the `features` method to turn on only the ones you know you can support. + features :request_body_on_query_methods, + :compression, + :streaming + + # Runs the tests provide by Faraday, according to the features specified above. + it_behaves_like 'an adapter' + + # You can then add any other test specific to this adapter here... +end +``` + +### Available features + +| Feature | Description | +|----------------------------------|----------------------------------------------------------------------------------------------------------| +| `:compression` | Tests that your adapter can handle `gzip` and `deflate` compressions. | +| `:local_socket_binding` | Tests that your adapter supports binding to a local socket via the `:bind` request option. | +| `:parallel` | Tests that your adapter supports parallel requests. See [Parallel requests][parallel] for more details. | +| `:reason_phrase_parse` | Tests that your adapter supports parsing the `reason_phrase` from the response. | +| `:request_body_on_query_methods` | Tests that your adapter supports sending a request body on `GET`, `HEAD`, `DELETE` and `TRACE` requests. | +| `:streaming` | Tests that your adapter supports streaming responses. See [Streaming][streaming] for more details. | +| `:trace_method` | Tests your adapter against the `TRACE` HTTP method. | + +[streaming]: /adapters/custom/streaming.md +[parallel]: /adapters/custom/parallel-requests.md diff --git a/docs/adapters/em-http.md b/docs/adapters/em-http.md deleted file mode 100644 index ba0d861cc..000000000 --- a/docs/adapters/em-http.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -layout: documentation -title: "EM-HTTP Adapter" -permalink: /adapters/em-http -hide: true -top_name: Adapters -top_link: ./ ---- - -This Adapter uses the [em-http-request][rdoc] gem to make HTTP requests. - -```ruby -conn = Faraday.new(...) do |f| - # no custom options available - f.adapter :em_http -end -``` - -## Links - -* [Gem RDoc][rdoc] -* [Gem source][src] -* [Adapter RDoc][adapter_rdoc] - -[rdoc]: https://www.rubydoc.info/gems/em-http-request -[src]: https://github.com/igrigorik/em-http-request#readme -[adapter_rdoc]: https://www.rubydoc.info/gems/faraday/Faraday/Adapter/EMHttp diff --git a/docs/adapters/em-synchrony.md b/docs/adapters/em-synchrony.md deleted file mode 100644 index a362c8a6b..000000000 --- a/docs/adapters/em-synchrony.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -layout: documentation -title: "EM-Synchrony Adapter" -permalink: /adapters/em-synchrony -hide: true -top_name: Adapters -top_link: ./ ---- - -This Adapter uses the [em-synchrony][rdoc] gem to make HTTP requests. - -```ruby -conn = Faraday.new(...) do |f| - # no custom options available - f.adapter :em_synchrony -end -``` - -## Links - -* [Gem RDoc][rdoc] -* [Gem source][src] -* [Adapter RDoc][adapter_rdoc] -* [EM-HTTP Adapter](./em-http.md) - -[rdoc]: https://www.rubydoc.info/gems/em-synchrony -[src]: https://github.com/igrigorik/em-synchrony -[adapter_rdoc]: https://www.rubydoc.info/gems/faraday/Faraday/Adapter/EMSynchrony diff --git a/docs/adapters/excon.md b/docs/adapters/excon.md deleted file mode 100644 index a526327f8..000000000 --- a/docs/adapters/excon.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -layout: documentation -title: "Excon Adapter" -permalink: /adapters/excon -hide: true -top_name: Adapters -top_link: ./ ---- - -This Adapter uses the [excon][rdoc] gem to make HTTP requests. - -```ruby -conn = Faraday.new(...) do |f| - # no custom options available - f.adapter :excon -end -``` - -## Links - -* [Gem RDoc][rdoc] -* [Gem source][src] -* [Adapter RDoc][adapter_rdoc] - -[rdoc]: https://www.rubydoc.info/gems/excon -[src]: https://github.com/excon/excon -[adapter_rdoc]: https://www.rubydoc.info/gems/faraday/Faraday/Adapter/Excon diff --git a/docs/adapters/httpclient.md b/docs/adapters/httpclient.md deleted file mode 100644 index fe52fc7ab..000000000 --- a/docs/adapters/httpclient.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -layout: documentation -title: "HTTPClient Adapter" -permalink: /adapters/httpclient -hide: true -top_name: Adapters -top_link: ./ ---- - -This Adapter uses the [httpclient][rdoc] gem to make HTTP requests. - -```ruby -conn = Faraday.new(...) do |f| - f.adapter :httpclient do |client| - # yields HTTPClient - client.keep_alive_timeout = 20 - client.ssl_config.timeout = 25 - end -end -``` - -## Links - -* [Gem RDoc][rdoc] -* [Gem source][src] -* [Adapter RDoc][adapter_rdoc] - -[rdoc]: https://www.rubydoc.info/gems/httpclient -[src]: https://github.com/nahi/httpclient -[adapter_rdoc]: https://www.rubydoc.info/gems/faraday/Faraday/Adapter/HTTPClient diff --git a/docs/adapters/index.md b/docs/adapters/index.md index 3a44bc1d0..f3d475560 100644 --- a/docs/adapters/index.md +++ b/docs/adapters/index.md @@ -1,54 +1,46 @@ ---- -layout: documentation -title: "Adapters" -permalink: /adapters/ -order: 2 ---- +# Adapters The Faraday Adapter interface determines how a Faraday request is turned into a Faraday response object. Adapters are typically implemented with common Ruby HTTP clients, but can have custom implementations. Adapters can be configured either globally or per Faraday Connection through the configuration block. -{: .mt-60} -## Built-in adapters +For example, consider using `httpclient` as an adapter. Note that [faraday-httpclient](https://github.com/lostisland/faraday-httpclient) must be installed beforehand. -Faraday includes these adapters (but not the HTTP client libraries): +If you want to configure it globally, do the following: -* [Net::HTTP][net_http] _(this is the default adapter)_ -* [Net::HTTP::Persistent][persistent] -* [Excon][excon] -* [Patron][patron] -* [EM-Synchrony][em-synchrony] -* [HTTPClient][httpclient] +```ruby +require 'faraday' +require 'faraday/httpclient' -While most adapters use a common Ruby HTTP client library, adapters can also -have completely custom implementations. +Faraday.default_adapter = :httpclient +``` + +If you want to configure it per Faraday Connection, do the following: -* [Test Adapter][testing] -* Rack Adapter (link TBD) +```ruby +require 'faraday' +require 'faraday/httpclient' -## External adapters +conn = Faraday.new do |f| + f.adapter :httpclient +end +``` -Adapters are slowly being moved into their own gems, or bundled with HTTP clients. -Please refer to their documentation for usage examples. +## Fantastic adapters and where to find them -* [Typhoeus][typhoeus] -* [HTTP.rb][faraday-http] +Except for the default [Net::HTTP][net_http] adapter and the [Test Adapter][testing] adapter, which is for _test purposes only_, +adapters are distributed separately from Faraday and need to be manually installed. +They are usually available as gems, or bundled with HTTP clients. + +While most adapters use a common Ruby HTTP client library, adapters can also +have completely custom implementations. -## Ad-hoc adapters customization +If you're just getting started you can find a list of featured adapters in [Awesome Faraday][awesome]. +Anyone can create a Faraday adapter and distribute it. If you're interested learning more, check how to [build your own][build_adapters]! -Faraday is intended to be a generic interface between your code and the adapter. -However, sometimes you need to access a feature specific to one of the adapters that is not covered in Faraday's interface. -When that happens, you can pass a block when specifying the adapter to customize it. -The block parameter will change based on the adapter you're using. See each adapter page for more details. -[net_http]: ./net-http -[persistent]: ./net-http-persistent -[excon]: ./excon -[patron]: ./patron -[em-synchrony]: ./em-synchrony -[httpclient]: ./httpclient -[typhoeus]: https://github.com/typhoeus/typhoeus/blob/master/lib/typhoeus/adapters/faraday.rb -[faraday-http]: https://github.com/lostisland/faraday-http -[testing]: ./testing +[testing]: /adapters/test-adapter.md +[net_http]: /adapters/net-http.md +[awesome]: https://github.com/lostisland/awesome-faraday/#adapters +[build_adapters]: /adapters/custom/index.md diff --git a/docs/adapters/net-http.md b/docs/adapters/net-http.md new file mode 100644 index 000000000..8c1dbaea0 --- /dev/null +++ b/docs/adapters/net-http.md @@ -0,0 +1,12 @@ +# Net::HTTP Adapter + +Faraday's Net::HTTP adapter is the default adapter. It uses the `Net::HTTP` +library that ships with Ruby's standard library. +Unless you have a specific reason to use a different adapter, this is probably +the adapter you want to use. + +With the release of Faraday 2.0, the Net::HTTP adapter has been moved into a [separate gem][faraday-net_http], +but it has also been added as a dependency of Faraday. +That means you can use it without having to install it or require it explicitly. + +[faraday-net_http]: https://github.com/lostisland/faraday-net_http diff --git a/docs/adapters/net_http.md b/docs/adapters/net_http.md deleted file mode 100644 index 7a0411574..000000000 --- a/docs/adapters/net_http.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -layout: documentation -title: "Net::HTTP Adapter" -permalink: /adapters/net-http -hide: true -top_name: Adapters -top_link: ./ ---- - -This Adapter uses the [`Net::HTTP`][rdoc] client from the Ruby standard library to make -HTTP requests. - -```ruby -conn = Faraday.new(...) do |f| - f.adapter :net_http do |http| - # yields Net::HTTP - http.idle_timeout = 100 - http.verify_callback = lambda do |preverify, cert_store| - # do something here... - end - end -end -``` - -## Links - -* [Net::HTTP RDoc][rdoc] -* [Adapter RDoc][adapter_rdoc] - -[rdoc]: http://ruby-doc.org/stdlib/libdoc/net/http/rdoc/Net/HTTP.html -[adapter_rdoc]: https://www.rubydoc.info/gems/faraday/Faraday/Adapter/NetHttp diff --git a/docs/adapters/net_http_persistent.md b/docs/adapters/net_http_persistent.md deleted file mode 100644 index 503443c08..000000000 --- a/docs/adapters/net_http_persistent.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -layout: documentation -title: "Net::HTTP::Persistent Adapter" -permalink: /adapters/net-http-persistent -hide: true -top_name: Adapters -top_link: ./ ---- - -This Adapter uses the [net-http-persistent][rdoc] gem to make HTTP requests. - -```ruby -conn = Faraday.new(...) do |f| - f.adapter :net_http_persistent, pool_size: 5 do |http| - # yields Net::HTTP::Persistent - http.idle_timeout = 100 - http.retry_change_requests = true - end -end -``` - -## Links - -* [Gem RDoc][rdoc] -* [Gem source][src] -* [Adapter RDoc][adapter_rdoc] - -[rdoc]: https://www.rubydoc.info/gems/net-http-persistent -[src]: https://github.com/drbrain/net-http-persistent -[adapter_rdoc]: https://www.rubydoc.info/gems/faraday/Faraday/Adapter/NetHttpPersistent diff --git a/docs/adapters/patron.md b/docs/adapters/patron.md deleted file mode 100644 index 97a22d6d9..000000000 --- a/docs/adapters/patron.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -layout: documentation -title: "Patron Adapter" -permalink: /adapters/patron -hide: true -top_name: Adapters -top_link: ./ ---- - -This Adapter uses the [patron][rdoc] gem to make HTTP requests. - -```ruby -conn = Faraday.new(...) do |f| - f.adapter :patron do |session| - # yields Patron::Session - session.max_redirects = 10 - end -end -``` - -## Links - -* [Gem RDoc][rdoc] -* [Gem source][src] -* [Adapter RDoc][adapter_rdoc] - -[rdoc]: https://www.rubydoc.info/gems/patron -[src]: https://github.com/toland/patron -[adapter_rdoc]: https://www.rubydoc.info/gems/faraday/Faraday/Adapter/Patron diff --git a/docs/adapters/testing.md b/docs/adapters/test-adapter.md similarity index 57% rename from docs/adapters/testing.md rename to docs/adapters/test-adapter.md index fb19f68e8..3a647946b 100644 --- a/docs/adapters/testing.md +++ b/docs/adapters/test-adapter.md @@ -1,11 +1,4 @@ ---- -layout: documentation -title: "Testing" -permalink: /adapters/testing -hide: true -top_name: Adapters -top_link: ./ ---- +# Test Adapter The built-in Faraday Test adapter lets you define stubbed HTTP requests. This can be used to mock out network services in an application's unit tests. @@ -33,7 +26,7 @@ conn = Faraday.new do |builder| # test exceptions too stub.get('/boom') do - raise Faraday::ConnectionFailed, nil + raise Faraday::ConnectionFailed end end end @@ -64,6 +57,41 @@ initialized. This is useful for testing. stubs.get('/uni') { |env| [ 200, {}, 'urchin' ]} ``` +You can also stub the request body with a string or a proc. +It would be useful to pass a proc if it's OK only to check the parts of the request body are passed. + +```ruby +stubs.post('/kohada', 'where=sea&temperature=24') { |env| [ 200, {}, 'spotted gizzard shad' ]} +stubs.post('/anago', -> (request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'Wakamoto' } }) { |env| [200, {}, 'conger eel'] } +``` + +If you want to stub requests that exactly match a path, parameters, and headers, +`strict_mode` would be useful. + +```ruby +stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true) do |stub| + stub.get('/ikura?nori=true', 'X-Soy-Sauce' => '5ml' ) { |env| [200, {}, 'ikura gunkan maki'] } +end +``` + +This stub expects the connection will be called like this: + +```ruby +conn.get('/ikura', { nori: 'true' }, { 'X-Soy-Sauce' => '5ml' } ) +``` + +If there are other parameters or headers included, the Faraday Test adapter +will raise `Faraday::Test::Stubs::NotFound`. It also raises the error +if the specified parameters (`nori`) or headers (`X-Soy-Sauce`) are omitted. + +You can also enable `strict_mode` after initializing the connection. +In this case, all requests, including ones that have been already stubbed, +will be handled in a strict way. + +```ruby +stubs.strict_mode = true +``` + Finally, you can treat your stubs as mocks by verifying that all of the stubbed calls were made. NOTE: this feature is still fairly experimental. It will not verify the order or count of any stub. @@ -85,5 +113,5 @@ Faraday.default_connection = nil Working [RSpec] and [test/unit] examples for a fictional JSON API client are available. -[RSpec]: https://github.com/lostisland/faraday/blob/master/examples/client_spec.rb -[test/unit]: https://github.com/lostisland/faraday/blob/master/examples/client_test.rb +[RSpec]: https://github.com/lostisland/faraday/blob/main/examples/client_spec.rb +[test/unit]: https://github.com/lostisland/faraday/blob/main/examples/client_test.rb diff --git a/docs/advanced/parallel-requests.md b/docs/advanced/parallel-requests.md new file mode 100644 index 000000000..a2634f955 --- /dev/null +++ b/docs/advanced/parallel-requests.md @@ -0,0 +1,58 @@ +# Parallel Requests + +Some adapters support running requests in parallel. +This can be achieved using the `#in_parallel` method on the connection object. + +```ruby +# Install the Typhoeus adapter with `gem install faraday-typhoeus` first. +require 'faraday/typhoeus' + +conn = Faraday.new('http://httpbingo.org') do |faraday| + faraday.adapter :typhoeus +end + +now = Time.now + +conn.in_parallel do + conn.get('/delay/3') + conn.get('/delay/3') +end + +# This should take about 3 seconds, not 6. +puts "Time taken: #{Time.now - now}" +``` + +## A note on Async + +You might have heard about [Async] and its native integration with Ruby 3.0. +The good news is that you can already use Async with Faraday (thanks to the [async-http-faraday] gem) +and this does not require the use of `#in_parallel` to run parallel requests. +Instead, you only need to wrap your Faraday code into an Async block: + +```ruby +# Install the Async adapter with `gem install async-http-faraday` first. +require 'async/http/faraday' + +conn = Faraday.new('http://httpbingo.org') do |faraday| + faraday.adapter :async_http +end + +now = Time.now + +# NOTE: This is not limited to a single connection anymore! +# You can run parallel requests spanning multiple connections. +Async do + Async { conn.get('/delay/3') } + Async { conn.get('/delay/3') } +end + +# This should take about 3 seconds, not 6. +puts "Time taken: #{Time.now - now}" + +``` + +The big advantage of using Async is that you can now run parallel requests *spanning multiple connections*, +whereas the `#in_parallel` method only works for requests that are made through the same connection. + +[Async]: https://github.com/socketry/async +[async-http-faraday]: https://github.com/socketry/async-http-faraday diff --git a/docs/advanced/streaming-responses.md b/docs/advanced/streaming-responses.md new file mode 100644 index 000000000..cd688c368 --- /dev/null +++ b/docs/advanced/streaming-responses.md @@ -0,0 +1,35 @@ +# Streaming Responses + +Sometimes you might need to receive a streaming response. +You can do this with the `on_data` request option. + +The `on_data` callback is a receives tuples of chunk Strings, and the total +of received bytes so far. + +This example implements such a callback: + +```ruby +# A buffer to store the streamed data +streamed = [] + +conn = Faraday.new('http://httpbingo.org') +conn.get('/stream/100') do |req| + # Set a callback which will receive tuples of chunk Strings, + # the sum of characters received so far, and the response environment. + # The latter will allow access to the response status, headers and reason, as well as the request info. + req.options.on_data = Proc.new do |chunk, overall_received_bytes, env| + puts "Received #{overall_received_bytes} characters" + streamed << chunk + end +end + +# Joins all response chunks together +streamed.join +``` + +The `on_data` streaming is currently only supported by some adapters. +To see which ones, please refer to [Awesome Faraday][awesome] comparative table or check the adapter documentation. +Moreover, the `env` parameter was only recently added, which means some adapters may only have partial support +(i.e. only `chunk` and `overall_received_bytes` will be passed to your block). + +[awesome]: https://github.com/lostisland/awesome-faraday/#adapters diff --git a/docs/assets/css/main.scss b/docs/assets/css/main.scss deleted file mode 100644 index 601041bc0..000000000 --- a/docs/assets/css/main.scss +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -@import "variables"; -@import "type-theme"; -@import "faraday"; \ No newline at end of file diff --git a/docs/assets/img/featured-bg.svg b/docs/assets/img/featured-bg.svg deleted file mode 100644 index 97a190228..000000000 --- a/docs/assets/img/featured-bg.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - Background and Stripes - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/assets/img/home-banner.jpg b/docs/assets/img/home-banner.jpg deleted file mode 100644 index 3e71d8b70..000000000 Binary files a/docs/assets/img/home-banner.jpg and /dev/null differ diff --git a/docs/assets/img/logo.png b/docs/assets/img/logo.png deleted file mode 100644 index 3db015a71..000000000 Binary files a/docs/assets/img/logo.png and /dev/null differ diff --git a/docs/assets/js/team.js b/docs/assets/js/team.js deleted file mode 100644 index 5c61fcbdc..000000000 --- a/docs/assets/js/team.js +++ /dev/null @@ -1,49 +0,0 @@ -function teamTile(member) { - console.log(member); - return '
' + - '' + - '' + - '' + - '' + - '
'; -} - -function fetchTeam(json, team, div) { - let el = document.querySelector(div); - el.innerHTML = team.map(function (m) { - let index = json.findIndex(function(e) { - return e.author.login === m - }); - return teamTile(json.splice(index, 1)[0]); - }).join(''); -} - -function fetchContributors(json) { - let el = document.querySelector('#contributors-list'); - el.innerHTML = json.reverse().map(function (c) { - return '' + c.author.login + ''; - }).join(' · '); -} - -function hideLoader() { - let el = document.querySelector('#loader'); - el.classList.add('hidden'); -} - -function showTeam() { - let el = document.querySelector('#team-content'); - el.classList.remove('hidden'); -} - -fetch('https://api.github.com/repos/lostisland/faraday/stats/contributors') - .then(function (response) { - response.json().then(function (json) { - fetchTeam(json, ['technoweenie', 'iMacTia', 'olleolleolle'], '#active-maintainers-list'); - fetchTeam(json, ['mislav', 'sferik'], '#historical-team-list'); - fetchContributors(json); - hideLoader(); - showTeam(); - }); - }); \ No newline at end of file diff --git a/docs/customization/connection-options.md b/docs/customization/connection-options.md new file mode 100644 index 000000000..b6bb8e461 --- /dev/null +++ b/docs/customization/connection-options.md @@ -0,0 +1,48 @@ +# Connection Options + +When initializing a new Faraday connection with `Faraday.new`, you can pass a hash of options to customize the connection. +All these options are optional. + +| Option | Type | Default | Description | +|---------------------|-------------------|-----------------|---------------------------------------------------------------------------------------------------------------| +| `:request` | Hash | nil | Hash of request options. Will be use to build [RequestOptions]. | +| `:proxy` | URI, String, Hash | nil | Proxy options, either as a URL or as a Hash of [ProxyOptions]. | +| `:ssl` | Hash | nil | Hash of SSL options. Will be use to build [SSLOptions]. | +| `:url` | URI, String | nil | URI or String base URL. This can also be passed as positional argument. | +| `:parallel_manager` | | nil | Default parallel manager to use. This is normally set by the adapter, but you have the option to override it. | +| `:params` | Hash | nil | URI query unencoded key/value pairs. | +| `:headers` | Hash | nil | Hash of unencoded HTTP header key/value pairs. | +| `:builder_class` | Class | RackBuilder | A custom class to use as the middleware stack builder. | +| `:builder` | Object | Rackbuilder.new | An instance of a custom class to use as the middleware stack builder. | + +## Example + +```ruby +options = { + request: { + open_timeout: 5, + timeout: 5 + }, + proxy: { + uri: 'https://proxy.com', + user: 'proxy_user', + password: 'proxy_password' + }, + ssl: { + ca_file: '/path/to/ca_file', + ca_path: '/path/to/ca_path', + verify: true + }, + url: 'https://example.com', + params: { foo: 'bar' }, + headers: { 'X-Api-Key' => 'secret', 'X-Api-Version' => '2' } +} + +conn = Faraday.new(options) do |faraday| + # ... +end +``` + +[RequestOptions]: /customization/request-options.md +[ProxyOptions]: /customization/proxy-options.md +[SSLOptions]: /customization/ssl-options.md diff --git a/docs/usage/customize.md b/docs/customization/index.md similarity index 66% rename from docs/usage/customize.md rename to docs/customization/index.md index adf40031a..eea4e7948 100644 --- a/docs/usage/customize.md +++ b/docs/customization/index.md @@ -1,28 +1,23 @@ ---- -layout: documentation -title: "Customizing the Request" -permalink: /usage/customize -hide: true -top_name: Usage -top_link: ./ -next_name: Streaming Responses -next_link: ./streaming ---- +# Configuration +Faraday is highly configurable and allows you to customize the way requests are made. +This applies to both the connection and the request, but can also cover things like SSL and proxy settings. + +Below are some examples of how to customize Faraday requests. Configuration can be set up with the connection and/or adjusted per request. As connection options: ```ruby -conn = Faraday.new('http://sushi.com', request: { timeout: 5 }) -conn.get('/search') +conn = Faraday.new('http://httpbingo.org', request: { timeout: 5 }) +conn.get('/ip') ``` Or as per-request options: ```ruby conn.get do |req| - req.url '/search' + req.url '/ip' req.options.timeout = 5 end ``` @@ -32,7 +27,7 @@ This will be available in the `env` on all middleware. ```ruby conn.get do |req| - req.url '/search' + req.url '/get' req.options.context = { foo: 'foo', bar: 'bar' @@ -77,6 +72,20 @@ serializes POST bodies. The default encoder is `Faraday::NestedParamsEncoder`. +### Order of parameters + +By default, parameters are sorted by name while being serialized. +Since this is really useful to provide better cache management and most servers don't really care about parameters order, this is the default behaviour. +However you might find yourself dealing with a server that requires parameters to be in a specific order. +When that happens, you can configure the encoder to skip sorting them. +This configuration is supported by both the default `Faraday::NestedParamsEncoder` and `Faraday::FlatParamsEncoder`: + +```ruby +Faraday::NestedParamsEncoder.sort_params = false +# or +Faraday::FlatParamsEncoder.sort_params = false +``` + ## Proxy Faraday will try to automatically infer the proxy settings from your system using [`URI#find_proxy`][ruby-find-proxy]. diff --git a/docs/customization/proxy-options.md b/docs/customization/proxy-options.md new file mode 100644 index 000000000..971fc0aad --- /dev/null +++ b/docs/customization/proxy-options.md @@ -0,0 +1,30 @@ +# Proxy Options + +Proxy options can be provided to the connection constructor or set on a per-request basis via [RequestOptions](/customization/request-options.md). +All these options are optional. + +| Option | Type | Default | Description | +|-------------|-------------|---------|-----------------| +| `:uri` | URI, String | nil | Proxy URL. | +| `:user` | String | nil | Proxy user. | +| `:password` | String | nil | Proxy password. | + +## Example + +```ruby +# Proxy options can be passed to the connection constructor and will be applied to all requests. +proxy_options = { + uri: 'http://proxy.example.com:8080', + user: 'username', + password: 'password' +} + +conn = Faraday.new(proxy: proxy_options) do |faraday| + # ... +end + +# You can then override them on a per-request basis. +conn.get('/foo') do |req| + req.options.proxy.update(uri: 'http://proxy2.example.com:8080') +end +``` diff --git a/docs/customization/request-options.md b/docs/customization/request-options.md new file mode 100644 index 000000000..319c8fd74 --- /dev/null +++ b/docs/customization/request-options.md @@ -0,0 +1,39 @@ +# Request Options + +Request options can be provided to the connection constructor or set on a per-request basis. +All these options are optional. + +| Option | Type | Default | Description | +|-------------------|-------------------|----------------------------------------------------------------|-------------------------------------------------------------------------| +| `:params_encoder` | Class | `Faraday::Utils.nested_params_encoder` (`NestedParamsEncoder`) | A custom class to use as the params encoder. | +| `:proxy` | URI, String, Hash | nil | Proxy options, either as a URL or as a Hash of [ProxyOptions]. | +| `:bind` | Hash | nil | Hash of bind options. Requires the `:host` and `:port` keys. | +| `:timeout` | Integer, Float | nil (adapter default) | The max number of seconds to wait for the request to complete. | +| `:open_timeout` | Integer, Float | nil (adapter default) | The max number of seconds to wait for the connection to be established. | +| `:read_timeout` | Integer, Float | nil (adapter default) | The max number of seconds to wait for one block to be read. | +| `:write_timeout` | Integer, Float | nil (adapter default) | The max number of seconds to wait for one block to be written. | +| `:boundary` | String | nil | The boundary string for multipart requests. | +| `:context` | Hash | nil | Arbitrary data that you can pass to your request. | +| `:on_data` | Proc | nil | A callback that will be called when data is received. See [Streaming] | + +## Example + +```ruby +# Request options can be passed to the connection constructor and will be applied to all requests. +request_options = { + params_encoder: Faraday::FlatParamsEncoder, + timeout: 5 +} + +conn = Faraday.new(request: request_options) do |faraday| + # ... +end + +# You can then override them on a per-request basis. +conn.get('/foo') do |req| + req.options.timeout = 10 +end +``` + +[ProxyOptions]: /customization/proxy-options.md +[SSLOptions]: /advanced/streaming-responses.md diff --git a/docs/customization/ssl-options.md b/docs/customization/ssl-options.md new file mode 100644 index 000000000..02290a4f3 --- /dev/null +++ b/docs/customization/ssl-options.md @@ -0,0 +1,33 @@ +# SSL Options + +Faraday supports a number of SSL options, which can be provided while initializing the connection. + +| Option | Type | Default | Description | +|--------------------|----------------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| `:verify` | Boolean | true | Verify SSL certificate. Defaults to `true`. | +| `:verify_hostname` | Boolean | true | Verify SSL certificate hostname. Defaults to `true`. | +| `:ca_file` | String | nil | Path to a CA file in PEM format. | +| `:ca_path` | String | nil | Path to a CA directory. | +| `:verify_mode` | Integer | nil | Any `OpenSSL::SSL::` constant (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL.html)). | +| `:cert_store` | OpenSSL::X509::Store | nil | OpenSSL certificate store. | +| `:client_cert` | OpenSSL::X509::Certificate | nil | Client certificate. | +| `:client_key` | OpenSSL::PKey::RSA, OpenSSL::PKey::DSA | nil | Client private key. | +| `:certificate` | OpenSSL::X509::Certificate | nil | Certificate (Excon only). | +| `:private_key` | OpenSSL::PKey::RSA | nil | Private key (Excon only). | +| `:verify_depth` | Integer | nil | Maximum depth for the certificate chain verification. | +| `:version` | Integer | nil | SSL version (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL/SSLContext.html#method-i-ssl_version-3D)). | +| `:min_version` | Integer | nil | Minimum SSL version (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL/SSLContext.html#method-i-min_version-3D)). | +| `:max_version` | Integer | nil | Maximum SSL version (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL/SSLContext.html#method-i-max_version-3D)). | + +## Example + +```ruby +ssl_options = { + ca_file: '/path/to/ca_file', + min_version: :TLS1_2 +} + +conn = Faraday.new(ssl: options) do |faraday| + # ... +end +``` diff --git a/docs/getting-started/env-object.md b/docs/getting-started/env-object.md new file mode 100644 index 000000000..8b8cd05f7 --- /dev/null +++ b/docs/getting-started/env-object.md @@ -0,0 +1,51 @@ +# The Env Object + +Inspired by Rack, Faraday uses an `env` object to pass data between middleware. +This object is initialized at the beginning of the request and passed down the middleware stack. +The adapter is then responsible to run the HTTP request and set the `response` property on the `env` object, +which is then passed back up the middleware stack. + +You can read more about how the `env` object is used in the [Middleware - How it works](/middleware/index?id=how-it-works) section. + +Because of its nature, the `env` object is a complex structure that holds a lot of information and can +therefore be a bit intimidating at first. This page will try to explain the different properties of the `env` object. + +## Properties + +Please also note that these properties are not all available at the same time: while configuration +and request properties are available at the beginning of the request, response properties are only +available after the request has been performed (i.e. in the `on_complete` callback of middleware). + + +| Property | Type | Request | Response | Description | +|---------------------|----------------------------|:------------------:|:------------------:|-----------------------------| +| `:method` | `Symbol` | :heavy_check_mark: | :heavy_check_mark: | The HTTP method to use. | +| `:request_body` | `String` | :heavy_check_mark: | :heavy_check_mark: | The request body. | +| `:url` | `URI` | :heavy_check_mark: | :heavy_check_mark: | The request URL. | +| `:request` | `Faraday::RequestOptions` | :heavy_check_mark: | :heavy_check_mark: | The request options. | +| `:request_headers` | `Faraday::Utils::Headers` | :heavy_check_mark: | :heavy_check_mark: | The request headers. | +| `:ssl` | `Faraday::SSLOptions` | :heavy_check_mark: | :heavy_check_mark: | The SSL options. | +| `:parallel_manager` | `Faraday::ParallelManager` | :heavy_check_mark: | :heavy_check_mark: | The parallel manager. | +| `:params` | `Hash` | :heavy_check_mark: | :heavy_check_mark: | The request params. | +| `:response` | `Faraday::Response` | :x: | :heavy_check_mark: | The response. | +| `:response_headers` | `Faraday::Utils::Headers` | :x: | :heavy_check_mark: | The response headers. | +| `:status` | `Integer` | :x: | :heavy_check_mark: | The response status code. | +| `:reason_phrase` | `String` | :x: | :heavy_check_mark: | The response reason phrase. | +| `:response_body` | `String` | :x: | :heavy_check_mark: | The response body. | + +## Helpers + +The `env` object also provides some helper methods to make it easier to work with the properties. + +| Method | Description | +|-------------------------|--------------------------------------------------------------------------------------------------| +| `#body`/`#current_body` | Returns the request or response body, based on the presence of `#status`. | +| `#success?` | Returns `true` if the response status is in the 2xx range. | +| `#needs_body?` | Returns `true` if there's no body yet, and the method is in the set of `Env::MethodsWithBodies`. | +| `#clear_body` | Clears the body, if it's present. That includes resetting the `Content-Length` header. | +| `#parse_body?` | Returns `true` unless the status indicates otherwise (e.g. 204, 304). | +| `#parallel?` | Returns `true` if a parallel manager is available. | +| `#stream_response?` | Returns `true` if the `on_data` streaming callback has been provided. | +| `#stream_response` | Helper method to implement streaming in adapters. See [Support streaming in your adapter]. | + +[Support streaming in your adapter]: /adapters/custom/streaming.md diff --git a/docs/getting-started/errors.md b/docs/getting-started/errors.md new file mode 100644 index 000000000..1c4e10fe0 --- /dev/null +++ b/docs/getting-started/errors.md @@ -0,0 +1,17 @@ +# Dealing with Errors + +As an abstraction layer between the user and the underlying HTTP library, +it's important that Faraday provides a consistent interface for dealing with errors. +This is especially important when dealing with multiple adapters, as each adapter may raise different errors. + +Below is a list of errors that Faraday may raise, and that you should be prepared to handle. + +| Error | Description | +|-----------------------------|--------------------------------------------------------------------------------| +| `Faraday::Error` | Base class for all Faraday errors, also used for generic or unexpected errors. | +| `Faraday::ConnectionFailed` | Raised when the connection to the remote server failed. | +| `Faraday::TimeoutError` | Raised when the connection to the remote server timed out. | +| `Faraday::SSLError` | Raised when the connection to the remote server failed due to an SSL error. | + +If you add the `raise_error` middleware, Faraday will also raise additional errors for 4xx and 5xx responses. +You can find the full list of errors in the [raise_error middleware](/middleware/included/raising-errors) page. diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md new file mode 100644 index 000000000..50794bd9e --- /dev/null +++ b/docs/getting-started/quick-start.md @@ -0,0 +1,266 @@ +# Quick Start + +## Installation + +Add this line to your application’s `Gemfile`: + +```ruby +gem 'faraday' +``` + +And then execute: + +```bash +$ bundle +``` + +Or install it yourself as: + +```bash +$ gem install faraday +``` + +## Usage + +### Quick requests + +Let's fetch the home page for the wonderful [httpbingo.org](https://httpbingo.org) service. + +You can make a simple `GET` request using `Faraday.get`: + +```ruby +response = Faraday.get('http://httpbingo.org') +``` + +This returns a `Faraday::Response` object with the response status, headers, and body. + +```ruby +response.status +# => 200 + +response.headers +# => {"server"=>"Fly/c375678 (2021-04-23)", "content-type"=> ... + +response.body +# => " ... +``` + +### Faraday Connection + +The recommended way to use Faraday, especially when integrating to 3rd party services and APIs, is to create +a `Faraday::Connection`. The connection initializer allows you to set: + +- default request headers & query parameters +- network settings like proxy or timeout +- common URL base path +- Faraday adapter & middleware (see below) + +Create a `Faraday::Connection` by calling `Faraday.new`. You can then call each HTTP verb +(`get`, `post`, ...) on your `Faraday::Connection` to perform a request: + +```ruby +conn = Faraday.new( + url: 'http://httpbingo.org', + params: {param: '1'}, + headers: {'Content-Type' => 'application/json'} +) + +response = conn.post('/post') do |req| + req.params['limit'] = 100 + req.body = {query: 'chunky bacon'}.to_json +end +# => POST http://httpbingo.org/post?param=1&limit=100 +``` + +### GET, HEAD, DELETE, TRACE + +Faraday supports the following HTTP verbs that typically don't include a request body: + +- `get(url, params = nil, headers = nil)` +- `head(url, params = nil, headers = nil)` +- `delete(url, params = nil, headers = nil)` +- `trace(url, params = nil, headers = nil)` + +You can specify URI query parameters and HTTP headers when making a request. + +```ruby +response = conn.get('get', { boom: 'zap' }, { 'User-Agent' => 'myapp' }) +# => GET http://httpbingo.org/get?boom=zap +``` + +### POST, PUT, PATCH + +Faraday also supports HTTP verbs with bodies. Instead of query parameters, these +accept a request body: + +- `post(url, body = nil, headers = nil)` +- `put(url, body = nil, headers = nil)` +- `patch(url, body = nil, headers = nil)` + +```ruby +# POST 'application/x-www-form-urlencoded' content +response = conn.post('post', 'boom=zap') + +# POST JSON content +response = conn.post('post', '{"boom": "zap"}', + "Content-Type" => "application/json") +``` + +#### Posting Forms + +Faraday will automatically convert key/value hashes into proper form bodies +thanks to the `url_encoded` middleware included in the default connection. + +```ruby +# POST 'application/x-www-form-urlencoded' content +response = conn.post('post', boom: 'zap') +# => POST 'boom=zap' to http://httpbingo.org/post +``` + +### Detailed HTTP Requests + +Faraday supports a longer style for making requests. This is handy if you need +to change many of the defaults, or if the details of the HTTP request change +according to method arguments. Each of the HTTP verb helpers can yield a +`Faraday::Request` that can be modified before being sent. + +This example shows a hypothetical search endpoint that accepts a JSON request +body as the actual search query. + +```ruby +response = conn.post('post') do |req| + req.params['limit'] = 100 + req.headers['Content-Type'] = 'application/json' + req.body = {query: 'chunky bacon'}.to_json +end +# => POST http://httpbingo.org/post?limit=100 +``` + +### Using Middleware + +Configuring your connection or request with predefined headers and parameters is a good start, +but the real power of Faraday comes from its middleware stack. +Middleware are classes that allow you to hook into the request/response cycle and modify the request. +They can help you with things like: +* adding authentication headers +* parsing JSON responses +* logging requests and responses +* raise errors on 4xx and 5xx responses +* and much more! + +For example, let's say you want to call an API that: +* requires an authentication token in the `Authorization` header +* expects JSON request bodies +* returns JSON responses + +and on top of that, you want to automatically raise errors on 4xx and 5xx responses, +as well as log all requests and responses. + +You can easily achieve all of the above by adding the necessary middleware to your connection: + +```ruby +conn = Faraday.new(url: 'http://httpbingo.org') do |builder| + # Calls MyAuthStorage.get_auth_token on each request to get the auth token + # and sets it in the Authorization header with Bearer scheme. + builder.request :authorization, 'Bearer', -> { MyAuthStorage.get_auth_token } + + # Sets the Content-Type header to application/json on each request. + # Also, if the request body is a Hash, it will automatically be encoded as JSON. + builder.request :json + + # Parses JSON response bodies. + # If the response body is not valid JSON, it will raise a Faraday::ParsingError. + builder.response :json + + # Raises an error on 4xx and 5xx responses. + builder.response :raise_error + + # Logs requests and responses. + # By default, it only logs the request method and URL, and the request/response headers. + builder.response :logger +end + +# A simple example implementation for MyAuthStorage +class MyAuthStorage + def self.get_auth_token + rand(36 ** 8).to_s(36) + end +end +``` + +The connection can now be used to make requests. + +```ruby +begin + response = conn.post('post', { payload: 'this ruby hash will become JSON' }) +rescue Faraday::Error => e + # You can handle errors here (4xx/5xx responses, timeouts, etc.) + puts e.response[:status] + puts e.response[:body] +end + +# At this point, you can assume the request was successful +puts response.body + +# I, [2023-06-30T14:27:11.776511 #35368] INFO -- request: POST http://httpbingo.org/post +# I, [2023-06-30T14:27:11.776646 #35368] INFO -- request: User-Agent: "Faraday v2.7.8" +# Authorization: "Bearer wibzjgyh" +# Content-Type: "application/json" +# I, [2023-06-30T14:27:12.063897 #35368] INFO -- response: Status 200 +# I, [2023-06-30T14:27:12.064260 #35368] INFO -- response: access-control-allow-credentials: "true" +# access-control-allow-origin: "*" +# content-type: "application/json; encoding=utf-8" +# date: "Fri, 30 Jun 2023 13:27:12 GMT" +# content-encoding: "gzip" +# transfer-encoding: "chunked" +# server: "Fly/a0b91024 (2023-06-13)" +# via: "1.1 fly.io" +# fly-request-id: "01H467RYRHA0YK4TQSZ7HS8ZFT-lhr" +# cf-team: "19ae1592b8000003bbaedcf400000001" +``` + +Faraday ships with a number of useful middleware, and you can also write your own. +To learn more about middleware, please check the [Middleware] section. + +### Swapping Adapters + +Faraday does not make HTTP requests itself, but instead relies on a Faraday adapter to do so. +By default, it will use the `Net::HTTP` adapter, which is part of the Ruby standard library. +Although `Net::HTTP` is the only adapter that ships with Faraday, there are [many other adapters +available as separate gems](https://github.com/lostisland/awesome-faraday#adapters). + +Once you have installed an adapter, you can use it by passing the `adapter` option to `Faraday.new`: + +```ruby +conn = Faraday.new(url: 'http://httpbingo.org') do |builder| + builder.adapter :async_http +end +``` + +To learn more about adapters, including how to write your own, please check the [Adapters] section. + +### Default Connection, Default Adapter + +Remember how we said that Faraday will automatically encode key/value hash +bodies into form bodies? Internally, the top level shortcut methods +`Faraday.get`, `post`, etc. use a simple default `Faraday::Connection`. The only +middleware used for the default connection is `:url_encoded`, which encodes +those form hashes, and the `default_adapter`. + +You can change the default adapter or connection. Be careful because they're set globally. + +```ruby +Faraday.default_adapter = :async_http # defaults to :net_http + +# The default connection has only `:url_encoded` middleware. +# Note that if you create your own connection with middleware, it won't encode +# form bodies unless you too include the :url_encoded middleware! +Faraday.default_connection = Faraday.new do |conn| + conn.request :url_encoded + conn.response :logger + conn.adapter Faraday.default_adapter +end +``` + +[Adapters]: /adapters/index.md +[Middleware]: /middleware/index.md diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..7428fd2ef --- /dev/null +++ b/docs/index.html @@ -0,0 +1,121 @@ + + + + + + + + + Faraday Docs + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md index 31dbb8ccd..3c668eede 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,51 +1,28 @@ ---- -# You don't need to edit this file, it's empty on purpose. -# Edit theme's home layout instead if you wanna make some changes -# See: https://jekyllrb.com/docs/themes/#overriding-theme-defaults -layout: page -title: Homepage -feature-title: -feature-img: "assets/img/featured-bg.svg" -hide: true ---- - -Faraday is an HTTP client library that provides a common interface over many adapters (such as Net::HTTP) -and embraces the concept of Rack middleware when processing the request/response cycle. - -{: .text-center} -[ Fork on GitHub][github]{: .btn} -[ Chat with us][gitter]{: .btn} - -{: .mt-60} -## Installation - -Add this line to your application's Gemfile: - -```ruby -gem 'faraday' -``` - -And then execute: - -```bash -$ bundle -``` - -Or install it yourself as: - -```bash -$ gem install faraday -``` - -You can also install the [`faraday_middleware`][faraday_middleware] -extension gem to access a collection of useful Faraday middleware. - -{: .mt-60} - -{: .text-center} -[ Read the docs][usage]{: .btn} - -[github]: https://github.com/lostisland/faraday -[gitter]: https://gitter.im/lostisland/faraday -[faraday_middleware]: https://github.com/lostisland/faraday_middleware -[usage]: ./usage +# ![Faraday](_media/home-logo.svg) + +Faraday is an HTTP client library abstraction layer that provides a common interface over many +adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle. + +## Why use Faraday? + +Faraday gives you the power of Rack middleware for manipulating HTTP requests and responses, +making it easier to build sophisticated API clients or web service libraries that abstract away +the details of how HTTP requests are made. + +Faraday comes with a lot of features out of the box, such as: +* Support for multiple adapters (Net::HTTP, Typhoeus, Patron, Excon, HTTPClient, and more) +* Persistent connections (keep-alive) +* Parallel requests +* Automatic response parsing (JSON, XML, YAML) +* Customization of the request/response cycle with middleware +* Support for streaming responses +* Support for uploading files +* And much more! + +## Who uses Faraday? + +Faraday is used by many popular Ruby libraries, such as: +* [Signet](https://github.com/googleapis/signet) +* [Octokit](https://github.com/octokit/octokit.rb) +* [Oauth2](https://bestgems.org/gems/oauth2) +* [Elasticsearch](https://github.com/elastic/elasticsearch-ruby) diff --git a/docs/middleware/custom-middleware.md b/docs/middleware/custom-middleware.md new file mode 100644 index 000000000..df15b80f0 --- /dev/null +++ b/docs/middleware/custom-middleware.md @@ -0,0 +1,84 @@ +# Writing custom middleware + +!> A template for writing your own middleware is available in the [faraday-middleware-template](https://github.com/lostisland/faraday-middleware-template) repository. + +The recommended way to write middleware is to make your middleware subclass `Faraday::Middleware`. +`Faraday::Middleware` simply expects your subclass to implement two methods: `#on_request(env)` and `#on_complete(env)`. +* `#on_request` is called when the request is being built and is given the `env` representing the request. +* `#on_complete` is called after the response has been received (that's right, it already supports parallel mode!) and receives the `env` of the response. + +For both `env` parameters, please refer to the [Env Object](getting-started/env-object.md) page. + +```ruby +class MyMiddleware < Faraday::Middleware + def on_request(env) + # do something with the request + # env[:request_headers].merge!(...) + end + + def on_complete(env) + # do something with the response + # env[:response_headers].merge!(...) + end +end +``` + +## Having more control + +For the majority of middleware, it's not necessary to override the `#call` method. You can instead use `#on_request` and `#on_complete`. + +However, in some cases you may need to wrap the call in a block, or work around it somehow (think of a begin-rescue, for example). +When that happens, then you can override `#call`. When you do so, remember to call either `app.call(env)` or `super` to avoid breaking the middleware stack call! + +```ruby +def call(request_env) + # do something with the request + # request_env[:request_headers].merge!(...) + + @app.call(request_env).on_complete do |response_env| + # do something with the response + # response_env[:response_headers].merge!(...) + end +end +``` + +It's important to do all processing of the response only in the `#on_complete` +block. This enables middleware to work in parallel mode where requests are +asynchronous. + +The `request_env` and `response_env` are both [Env Objects](getting-started/env-object.md) but note the amount of +information available in each one will differ based on the request/response lifecycle. + +## Accepting configuration options + +`Faraday::Middleware` also allows your middleware to accept configuration options. +These are passed in when the middleware is added to the stack, and can be accessed via the `options` getter. + +```ruby +class MyMiddleware < Faraday::Middleware + def on_request(_env) + # access the foo option + puts options[:foo] + end +end + +conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| + faraday.use MyMiddleware, foo: 'bar' +end +``` + +## Registering your middleware + +Users can use your middleware using the class directly, but you can also register it with Faraday so that +it can be used with the `use`, `request` or `response` methods as well. + +```ruby +# Register for `use` +Faraday::Middleware.register_middleware(my_middleware: MyMiddleware) + +# Register for `request` +Faraday::Request.register_middleware(my_middleware: MyMiddleware) + +# Register for `response` +Faraday::Response.register_middleware(my_middleware: MyMiddleware) +``` diff --git a/docs/middleware/custom.md b/docs/middleware/custom.md deleted file mode 100644 index 5fa82b78c..000000000 --- a/docs/middleware/custom.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -layout: documentation -title: "Writing Middleware" -permalink: /middleware/custom -hide: true -top_name: Middleware -top_link: ./ -prev_name: Available Middleware -prev_link: ./list ---- - -Middleware are classes that implement a `call` instance method. They hook into -the request/response cycle. - -```ruby -def call(request_env) - # do something with the request - # request_env[:request_headers].merge!(...) - - @app.call(request_env).on_complete do |response_env| - # do something with the response - # response_env[:response_headers].merge!(...) - end -end -``` - -It's important to do all processing of the response only in the `on_complete` -block. This enables middleware to work in parallel mode where requests are -asynchronous. - -The `env` is a hash with symbol keys that contains info about the request and, -later, response. Some keys are: - -``` -# request phase -:method - :get, :post, ... -:url - URI for the current request; also contains GET parameters -:body - POST parameters for :post/:put requests -:request_headers - -# response phase -:status - HTTP response status code, such as 200 -:body - the response body -:response_headers -``` diff --git a/docs/middleware/included/authentication.md b/docs/middleware/included/authentication.md new file mode 100644 index 000000000..b0e5cc1fc --- /dev/null +++ b/docs/middleware/included/authentication.md @@ -0,0 +1,65 @@ +# Authentication + +The `Faraday::Request::Authorization` middleware allows you to automatically add an `Authorization` header +to your requests. It also features a handy helper to manage Basic authentication. +**Please note the way you use this middleware in Faraday 1.x is different**, +examples are available at the bottom of this page. + +```ruby +Faraday.new(...) do |conn| + conn.request :authorization, 'Bearer', 'authentication-token' +end +``` + +### With a proc + +You can also provide a proc, which will be evaluated on each request: + +```ruby +Faraday.new(...) do |conn| + conn.request :authorization, 'Bearer', -> { MyAuthStorage.get_auth_token } +end +``` + +If the proc takes an argument, it will receive the forwarded `env` (see [The Env Object](getting-started/env-object.md)): + +```ruby +Faraday.new(...) do |conn| + conn.request :authorization, 'Bearer', ->(env) { MyAuthStorage.get_auth_token(env) } +end +``` + +### Basic Authentication + +The middleware will automatically Base64 encode your Basic username and password: + +```ruby +Faraday.new(...) do |conn| + conn.request :authorization, :basic, 'username', 'password' +end +``` + +### Faraday 1.x usage + +In Faraday 1.x, the way you use this middleware is slightly different: + +```ruby +# Basic Auth request +# Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= +Faraday.new(...) do |conn| + conn.request :basic_auth, 'username', 'password' +end + +# Token Auth request +# `options` are automatically converted into `key=value` format +# Authorization: Token authentication-token +Faraday.new(...) do |conn| + conn.request :token_auth, 'authentication-token', **options +end + +# Generic Auth Request +# Authorization: Bearer authentication-token +Faraday.new(...) do |conn| + conn.request :authorization, 'Bearer', 'authentication-token' +end +``` diff --git a/docs/middleware/list.md b/docs/middleware/included/index.md similarity index 56% rename from docs/middleware/list.md rename to docs/middleware/included/index.md index e912c195c..128ee30c5 100644 --- a/docs/middleware/list.md +++ b/docs/middleware/included/index.md @@ -1,19 +1,10 @@ ---- -layout: documentation -title: "Available Middleware" -permalink: /middleware/list -hide: true -top_name: Middleware -top_link: ./ -next_name: Writing Middleware -next_link: ./custom ---- +# Included middleware Faraday ships with some useful middleware that you can use to customize your request/response lifecycle. Middleware are separated into two macro-categories: **Request Middleware** and **Response Middleware**. The former usually deal with the request, encoding the parameters or setting headers. The latter instead activate after the request is completed and a response has been received, like -parsing the response body, logging useful info or checking the response status. +parsing the response body, logging useful info or checking the response status. ### Request Middleware @@ -21,14 +12,9 @@ parsing the response body, logging useful info or checking the response status. middleware set Header values or transform the request body based on the content type. -* [`BasicAuthentication`][authentication] sets the `Authorization` header to the `user:password` -base64 representation. -* [`TokenAuthentication`][authentication] sets the `Authorization` header to the specified token. -* [`Multipart`][multipart] converts a `Faraday::Request#body` hash of key/value pairs into a -multipart form request. +* [`Authorization`][authentication] allows you to automatically add an Authorization header to your requests. * [`UrlEncoded`][url_encoded] converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body. -* [`Retry`][retry] automatically retries requests that fail due to intermittent client -or server errors (such as network hiccups). +* [`Json Request`][json-request] converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body. * [`Instrumentation`][instrumentation] allows to instrument requests using different tools. @@ -37,14 +23,15 @@ or server errors (such as network hiccups). **Response middleware** receives the response from the adapter and can modify its details before returning it. +* [`Json Response`][json-response] parses response body into a hash of key/value pairs. * [`Logger`][logger] logs both the request and the response body and headers. * [`RaiseError`][raise_error] checks the response HTTP code and raises an exception if it is a 4xx or 5xx code. -[authentication]: ./authentication -[multipart]: ./multipart -[url_encoded]: ./url-encoded -[retry]: ./retry -[instrumentation]: ./instrumentation -[logger]: ./logger -[raise_error]: ./raise-error +[authentication]: middleware/included/authentication.md +[url_encoded]: middleware/included/url-encoding +[json-request]: middleware/included/json#json-requests +[instrumentation]: middleware/included/instrumentation +[json-response]: middleware/included/json#json-responses +[logger]: middleware/included/logging +[raise_error]: middleware/included/raising-errors diff --git a/docs/middleware/request/instrumentation.md b/docs/middleware/included/instrumentation.md similarity index 80% rename from docs/middleware/request/instrumentation.md rename to docs/middleware/included/instrumentation.md index 33fcaae08..c562e0031 100644 --- a/docs/middleware/request/instrumentation.md +++ b/docs/middleware/included/instrumentation.md @@ -1,15 +1,4 @@ ---- -layout: documentation -title: "Instrumentation Middleware" -permalink: /middleware/instrumentation -hide: true -prev_name: Retry Middleware -prev_link: ./retry -next_name: Logger Middleware -next_link: ./logger -top_name: Back to Middleware -top_link: ./list ---- +# Instrumentation The `Instrumentation` middleware allows to instrument requests using different tools. Options for this middleware include the instrumentation `name` and the `instrumenter` you want to use. diff --git a/docs/middleware/included/json.md b/docs/middleware/included/json.md new file mode 100644 index 000000000..26d987af9 --- /dev/null +++ b/docs/middleware/included/json.md @@ -0,0 +1,81 @@ +# JSON Encoding/Decoding + +## JSON Requests + +The `JSON` request middleware converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body. +The middleware also automatically sets the `Content-Type` header to `application/json`, +processes only requests with matching Content-Type or those without a type and +doesn't try to encode bodies that already are in string form. + +### Example Usage + +```ruby +conn = Faraday.new(...) do |f| + f.request :json + ... +end + +conn.post('/', { a: 1, b: 2 }) +# POST with +# Content-Type: application/json +# Body: {"a":1,"b":2} +``` + +### Using custom JSON encoders + +By default, middleware utilizes Ruby's `json` to generate JSON strings. + +Other encoders can be used by specifying `encoder` option for the middleware: +* a module/class which implements `dump` +* a module/class-method pair to be used + +```ruby +require 'oj' + +Faraday.new(...) do |f| + f.request :json, encoder: Oj +end + +Faraday.new(...) do |f| + f.request :json, encoder: [Oj, :dump] +end +``` + +## JSON Responses + +The `JSON` response middleware parses response body into a hash of key/value pairs. +The behaviour can be customized with the following options: +* **parser_options:** options that will be sent to the JSON.parse method. Defaults to {}. +* **content_type:** Single value or Array of response content-types that should be processed. Can be either strings or Regex. Defaults to `/\bjson$/`. +* **preserve_raw:** If set to true, the original un-parsed response will be stored in the `response.env[:raw_body]` property. Defaults to `false`. + +### Example Usage + +```ruby +conn = Faraday.new('http://httpbingo.org') do |f| + f.response :json, **options +end + +conn.get('json').body +# => {"slideshow"=>{"author"=>"Yours Truly", "date"=>"date of publication", "slides"=>[{"title"=>"Wake up to WonderWidgets!", "type"=>"all"}, {"items"=>["Why WonderWidgets are great", "Who buys WonderWidgets"], "title"=>"Overview", "type"=>"all"}], "title"=>"Sample Slide Show"}} +``` + +### Using custom JSON decoders + +By default, middleware utilizes Ruby's `json` to parse JSON strings. + +Other decoders can be used by specifying `decoder` parser option for the middleware: +* a module/class which implements `load` +* a module/class-method pair to be used + +```ruby +require 'oj' + +Faraday.new(...) do |f| + f.response :json, parser_options: { decoder: Oj } +end + +Faraday.new(...) do |f| + f.response :json, parser_options: { decoder: [Oj, :load] } +end +``` \ No newline at end of file diff --git a/docs/middleware/response/logger.md b/docs/middleware/included/logging.md similarity index 56% rename from docs/middleware/response/logger.md rename to docs/middleware/included/logging.md index 069382a3f..521b2c9a4 100644 --- a/docs/middleware/response/logger.md +++ b/docs/middleware/included/logging.md @@ -1,15 +1,4 @@ ---- -layout: documentation -title: "Logger Middleware" -permalink: /middleware/logger -hide: true -prev_name: Instrumentation Middleware -prev_link: ./instrumentation -next_name: RaiseError Middleware -next_link: ./raise-error -top_name: Back to Middleware -top_link: ./list ---- +# Logging The `Logger` middleware logs both the request and the response body and headers. It is highly customizable and allows to mask confidential information if necessary. @@ -17,12 +6,12 @@ It is highly customizable and allows to mask confidential information if necessa ### Basic Usage ```ruby -conn = Faraday.new(url: 'http://sushi.com') do |faraday| +conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| faraday.response :logger # log requests and responses to $stdout end conn.get -# => INFO -- request: GET http://sushi.com/ +# => INFO -- request: GET http://httpbingo.org/ # => DEBUG -- request: User-Agent: "Faraday v1.0.0" # => INFO -- response: Status 301 # => DEBUG -- response: date: "Sun, 19 May 2019 16:05:40 GMT" @@ -34,7 +23,7 @@ By default, the `Logger` middleware uses the Ruby `Logger.new($stdout)`. You can customize it to use any logger you want by providing it when you add the middleware to the stack: ```ruby -conn = Faraday.new(url: 'http://sushi.com') do |faraday| +conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| faraday.response :logger, MyLogger.new($stdout) end ``` @@ -42,12 +31,21 @@ end ### Include and exclude headers/bodies By default, the `logger` middleware logs only headers for security reasons, however, you can configure it -to log bodies as well, or disable headers logging if you need to. To do so, simply provide a configuration hash -when you add the middleware to the stack: +to log bodies and errors as well, or disable headers logging if you need to. +To do so, simply provide a configuration hash when you add the middleware to the stack: ```ruby -conn = Faraday.new(url: 'http://sushi.com') do |faraday| - faraday.response :logger, nil, { headers: true, bodies: true } +conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| + faraday.response :logger, nil, { headers: true, bodies: true, errors: true } +end +``` + +You can also configure the `logger` middleware with a little more complex settings +like "do not log the request bodies, but log the response bodies". + +```ruby +conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| + faraday.response :logger, nil, { bodies: { request: false, response: true } } end ``` @@ -58,14 +56,14 @@ Please note this only works with the default formatter. You can filter sensitive information from Faraday logs using a regex matcher: ```ruby -conn = Faraday.new(url: 'http://sushi.com') do |faraday| +conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| faraday.response :logger do | logger | - logger.filter(/(api_key=)(\w+)/, '\1[REMOVED]') + logger.filter(/(api_key=)([^&]+)/, '\1[REMOVED]') end end conn.get('/', api_key: 'secret') -# => INFO -- request: GET http://sushi.com/?api_key=[REMOVED] +# => INFO -- request: GET http://httpbingo.org/?api_key=[REMOVED] # => DEBUG -- request: User-Agent: "Faraday v1.0.0" # => INFO -- response: Status 301 # => DEBUG -- response: date: "Sun, 19 May 2019 16:12:36 GMT" @@ -77,17 +75,19 @@ By default, the `logger` middleware logs on the `info` log level. It is possible the severity by providing the `log_level` option: ```ruby -conn = Faraday.new(url: 'http://sushi.com') do |faraday| +conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| faraday.response :logger, nil, { bodies: true, log_level: :debug } end ``` ### Customize the formatter -You can also provide a custom formatter to control how requests and responses are logged. +You can also provide a custom formatter to control how requests, responses and errors are logged. Any custom formatter MUST implement the `request` and `response` method, with one argument which -will be passed being the Faraday environment. -If you make your formatter inheriting from `Faraday::Response::Logger::Formatter`, +will be passed being the Faraday environment. +Any custom formatter CAN implement the `exception` method, +with one argument which will be passed being the exception (StandardError). +If you make your formatter inheriting from `Faraday::Logging::Formatter`, then the methods `debug`, `info`, `warn`, `error` and `fatal` are automatically delegated to the logger. ```ruby @@ -98,12 +98,17 @@ class MyFormatter < Faraday::Logging::Formatter end def response(env) - # Build a custom message using `env` + # Build a custom message using `env` info('Response') { 'Response Received' } end + + def exception(exc) + # Build a custom message using `exc` + info('Error') { 'Error Raised' } + end end -conn = Faraday.new(url: 'http://sushi.com/api_key=s3cr3t') do |faraday| +conn = Faraday.new(url: 'http://httpbingo.org/api_key=s3cr3t') do |faraday| faraday.response :logger, nil, formatter: MyFormatter end -``` \ No newline at end of file +``` diff --git a/docs/middleware/included/raising-errors.md b/docs/middleware/included/raising-errors.md new file mode 100644 index 000000000..3b82130c8 --- /dev/null +++ b/docs/middleware/included/raising-errors.md @@ -0,0 +1,85 @@ +# Raising Errors + +The `RaiseError` middleware raises a `Faraday::Error` exception if an HTTP +response returns with a 4xx or 5xx status code. +This greatly increases the ease of use of Faraday, as you don't have to check +the response status code manually. +These errors add to the list of default errors [raised by Faraday](getting-started/errors.md). + +All exceptions are initialized with a hash containing the response `status`, `headers`, and `body`. + +```ruby +conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| + faraday.response :raise_error # raise Faraday::Error on status code 4xx or 5xx +end + +begin + conn.get('/wrong-url') # => Assume this raises a 404 response +rescue Faraday::ResourceNotFound => e + e.response_status #=> 404 + e.response_headers #=> { ... } + e.response_body #=> "..." +end +``` + +Specific exceptions are raised based on the HTTP Status code of the response. + +## 4xx Errors + +An HTTP status in the 400-499 range typically represents an error +by the client. They raise error classes inheriting from `Faraday::ClientError`. + +| Status Code | Exception Class | +|---------------------------------------------------------------------|-------------------------------------| +| [400](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400) | `Faraday::BadRequestError` | +| [401](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) | `Faraday::UnauthorizedError` | +| [403](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403) | `Faraday::ForbiddenError` | +| [404](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404) | `Faraday::ResourceNotFound` | +| [407](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407) | `Faraday::ProxyAuthError` | +| [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) | `Faraday::RequestTimeoutError` | +| [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) | `Faraday::ConflictError` | +| [422](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422) | `Faraday::UnprocessableEntityError` | +| [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) | `Faraday::TooManyRequestsError` | +| 4xx (any other) | `Faraday::ClientError` | + +## 5xx Errors + +An HTTP status in the 500-599 range represents a server error, and raises a +`Faraday::ServerError` exception. + +It's important to note that this exception is only returned if we receive a response and the +HTTP status in such response is in the 500-599 range. +Other kind of errors normally attributed to errors in the 5xx range (such as timeouts, failure to connect, etc...) +are raised as specific exceptions inheriting from `Faraday::Error`. +See [Faraday Errors](getting-started/errors.md) for more information on these. + +### Missing HTTP status + +The HTTP response status may be nil due to a malformed HTTP response from the +server, or a bug in the underlying HTTP library. This is considered a server error +and raised as `Faraday::NilStatusError`, which inherits from `Faraday::ServerError`. + +## Middleware Options + +The behavior of this middleware can be customized with the following options: + +| Option | Default | Description | +|---------------------|---------|-------------| +| **include_request** | true | When true, exceptions are initialized with request information including `method`, `url`, `url_path`, `params`, `headers`, and `body`. | + +### Example Usage + +```ruby +conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| + faraday.response :raise_error, include_request: true +end + +begin + conn.get('/wrong-url') # => Assume this raises a 404 response +rescue Faraday::ResourceNotFound => e + e.response[:status] #=> 404 + e.response[:headers] #=> { ... } + e.response[:body] #=> "..." + e.response[:request][:url_path] #=> "/wrong-url" +end +``` diff --git a/docs/middleware/request/url_encoded.md b/docs/middleware/included/url-encoding.md similarity index 62% rename from docs/middleware/request/url_encoded.md rename to docs/middleware/included/url-encoding.md index 95cdf528c..8677c0911 100644 --- a/docs/middleware/request/url_encoded.md +++ b/docs/middleware/included/url-encoding.md @@ -1,19 +1,8 @@ ---- -layout: documentation -title: "UrlEncoded Middleware" -permalink: /middleware/url-encoded -hide: true -prev_name: Multipart Middleware -prev_link: ./multipart -next_name: Retry Middleware -next_link: ./retry -top_name: Back to Middleware -top_link: ./list ---- +# URL Encoding The `UrlEncoded` middleware converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body. The middleware also automatically sets the `Content-Type` header to `application/x-www-form-urlencoded`. -The way parameters are serialized can be [customized][customize]. +The way parameters are serialized can be customized in the [Request Options](customization/request-options.md). ### Example Usage @@ -39,4 +28,4 @@ conn.post('/', { a: [1, 3], b: { c: 2, d: 4} }) # Body: a%5B%5D=1&a%5B%5D=3&b%5Bc%5D=2&b%5Bd%5D=4 ``` -[customize]: ../usage/customize/#changing-how-parameters-are-serialized +[customize]: ../usage/customize#changing-how-parameters-are-serialized diff --git a/docs/middleware/index.md b/docs/middleware/index.md index dc100eb34..ff88c014a 100644 --- a/docs/middleware/index.md +++ b/docs/middleware/index.md @@ -1,51 +1,200 @@ ---- -layout: documentation -title: "Middleware" -permalink: /middleware/ -next_name: Available Middleware -next_link: ./list -order: 3 ---- +# Middleware + +Under the hood, Faraday uses a Rack-inspired middleware stack for making +requests. Much of Faraday's power is unlocked with custom middleware. Some +middleware is included with Faraday, and others are in external gems. + +Here are some of the features that middleware can provide: + +- authentication +- caching responses on disk or in memory +- cookies +- following redirects +- JSON encoding/decoding +- logging + +To use these great features, create a `Faraday::Connection` with `Faraday.new` +and add the correct middleware in a block. For example: + +```ruby +require 'faraday' + +conn = Faraday.new do |f| + f.request :json # encode req bodies as JSON + f.response :logger # logs request and responses + f.response :json # decode response bodies as JSON + f.adapter :net_http # Use the Net::HTTP adapter +end +response = conn.get("http://httpbingo.org/get") +``` + +### How it Works A `Faraday::Connection` uses a `Faraday::RackBuilder` to assemble a Rack-inspired middleware stack for making HTTP requests. Each middleware runs and passes an Env object around to the next one. After the final middleware has run, Faraday will return a `Faraday::Response` to the end user. -![Middleware](../assets/img/middleware.png) +The order in which middleware is stacked is important. Like with Rack, the first +middleware on the list wraps all others, while the last middleware is the +innermost one. If you want to use a custom [adapter](adapters/index.md), it must +therefore be last. -The order in which middleware is stacked is important. Like with Rack, the -first middleware on the list wraps all others, while the last middleware is the -innermost one, so that must be the adapter. +![Middleware](../_media/middleware.png) + +This is what makes things like the "retry middleware" possible. +It doesn't really matter if the middleware was registered as a request or a response one, the only thing that matter is how they're added to the stack. + +Say you have the following: ```ruby Faraday.new(...) do |conn| - # POST/PUT params encoders: - conn.request :multipart + conn.request :authorization + conn.response :json + conn.response :parse_dates +end +``` + +This will result into a middleware stack like this: + +```ruby +authorization do + # authorization request hook + json do + # json request hook + parse_dates do + # parse_dates request hook + response = adapter.perform(request) + # parse_dates response hook + end + # json response hook + end + # authorization response hook +end +``` + +In this example, you can see that `parse_dates` is the LAST middleware processing the request, and the FIRST middleware processing the response. +This is why it's important for the adapter to always be at the end of the middleware list. + +### Using Middleware + +Calling `use` is the most basic way to add middleware to your stack, but most +middleware is conveniently registered in the `request`, `response` or `adapter` +namespaces. All four methods are equivalent apart from the namespacing. + +For example, the `Faraday::Request::UrlEncoded` middleware registers itself in +`Faraday::Request` so it can be added with `request`. These two are equivalent: + +```ruby +# add by symbol, lookup from Faraday::Request, +# Faraday::Response and Faraday::Adapter registries +conn = Faraday.new do |f| + f.request :url_encoded + f.response :logger + f.adapter :net_http +end +``` + +or: + +```ruby +# identical, but add the class directly instead of using lookups +conn = Faraday.new do |f| + f.use Faraday::Request::UrlEncoded + f.use Faraday::Response::Logger + f.use Faraday::Adapter::NetHttp +end +``` + +This is also the place to pass options. For example: + +```ruby +conn = Faraday.new do |f| + f.request :logger, bodies: true +end +``` + +### DEFAULT_OPTIONS + +`DEFAULT_OPTIONS` improve the flexibility and customizability of new and existing middleware. Class-level `DEFAULT_OPTIONS` and the ability to set these defaults at the application level compliment existing functionality in which options can be passed into middleware on a per-instance basis. + +#### Using DEFAULT_OPTIONS + +Using `RaiseError` as an example, you can see that `DEFAULT_OPTIONS` have been defined at the top of the class: + +```ruby + DEFAULT_OPTIONS = { include_request: true }.freeze +``` + +These options will be set at the class level upon instantiation and referenced as needed within the class. From our same example: + +```ruby + def response_values(env) + ... + return response unless options[:include_request] + ... +``` + +If the default value provides the desired functionality, no further consideration is needed. + +#### Setting Alternative Options per Application + +In the case where it is desirable to change the default option for all instances within an application, it can be done by configuring the options in a `/config/initializers` file. For example: + +```ruby +# config/initializers/faraday_config.rb + +Faraday::Response::RaiseError.default_options = { include_request: false } +``` + +After app initialization, all instances of the middleware will have the newly configured option(s). They can still be overriden on a per-instance bases (if handled in the middleware), like this: + +```ruby + Faraday.new do |f| + ... + f.response :raise_error, include_request: true + ... + end +``` + +### Available Middleware + +The following pages provide detailed configuration for the middleware that ships with Faraday: +* [Authentication](middleware/included/authentication.md) +* [URL Encoding](middleware/included/url-encoding.md) +* [JSON Encoding/Decoding](middleware/included/json.md) +* [Instrumentation](middleware/included/instrumentation.md) +* [Logging](middleware/included/logging.md) +* [Raising Errors](middleware/included/raising-errors.md) + +The [Awesome Faraday](https://github.com/lostisland/awesome-faraday/) project +has a complete list of useful, well-maintained Faraday middleware. Middleware is +often provided by external gems, like the +[faraday-retry](https://github.com/lostisland/faraday-retry) gem. + +### Detailed Example + +Here's a more realistic example: + +```ruby +Faraday.new(...) do |conn| + # POST/PUT params encoder conn.request :url_encoded - # Last middleware must be the adapter: + # Logging of requests/responses + conn.response :logger + + # Last middleware must be the adapter conn.adapter :net_http end ``` This request middleware setup affects POST/PUT requests in the following way: -1. `Request::Multipart` checks for files in the payload, otherwise leaves - everything untouched; -2. `Request::UrlEncoded` encodes as "application/x-www-form-urlencoded" if not - already encoded or of another type +1. `Request::UrlEncoded` encodes as "application/x-www-form-urlencoded" if not + already encoded or of another type. +2. `Response::Logger` logs request and response headers, can be configured to log bodies as well. Swapping middleware means giving the other priority. Specifying the "Content-Type" for the request is explicitly stating which middleware should process it. - -Examples: - -```ruby -# uploading a file: -payload[:profile_pic] = Faraday::FilePart.new('/path/to/avatar.jpg', 'image/jpeg') - -# "Multipart" middleware detects files and encodes with "multipart/form-data": -conn.put '/profile', payload -``` diff --git a/docs/middleware/request/authentication.md b/docs/middleware/request/authentication.md deleted file mode 100644 index 00771c71f..000000000 --- a/docs/middleware/request/authentication.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -layout: documentation -title: "Authentication Middleware" -permalink: /middleware/authentication -hide: true -next_name: Multipart Middleware -next_link: ./multipart -top_name: Back to Middleware -top_link: ./list ---- - -Basic and Token authentication are handled by Faraday::Request::BasicAuthentication -and Faraday::Request::TokenAuthentication respectively. -These can be added as middleware manually or through the helper methods. - -### Basic Authentication - -```ruby -Faraday.new(...) do |conn| - conn.basic_auth('username', 'password') -end -``` - -### Token Authentication - -```ruby -Faraday.new(...) do |conn| - conn.token_auth('authentication-token') -end -``` \ No newline at end of file diff --git a/docs/middleware/request/multipart.md b/docs/middleware/request/multipart.md deleted file mode 100644 index cc70dea76..000000000 --- a/docs/middleware/request/multipart.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -layout: documentation -title: "Multipart Middleware" -permalink: /middleware/multipart -hide: true -prev_name: Authentication Middleware -prev_link: ./authentication -next_name: UrlEncoded Middleware -next_link: ./url-encoded -top_name: Back to Middleware -top_link: ./list ---- - -The `Multipart` middleware converts a `Faraday::Request#body` Hash of key/value -pairs into a multipart form request, but only under these conditions: - -* The request's Content-Type is "multipart/form-data" -* Content-Type is unspecified, AND one of the values in the Body responds to -`#content_type`. - -Faraday contains a couple helper classes for multipart values: - -* `Faraday::FilePart` wraps binary file data with a Content-Type. The file data -can be specified with a String path to a local file, or an IO object. -* `Faraday::ParamPart` wraps a String value with a Content-Type, and optionally -a Content-ID. - -Note: `Faraday::ParamPart` was added in Faraday v0.16.0. Before that, -`Faraday::FilePart` was called `Faraday::UploadIO`. - -### Example Usage - -```ruby -conn = Faraday.new(...) do |f| - f.request :multipart - ... -end -``` - -Payload can be a mix of POST data and multipart values. - -```ruby -# regular POST form value -payload = { string: 'value' } - -# filename for this value is File.basename(__FILE__) -payload[:file] = Faraday::FilePart.new(__FILE__, 'text/x-ruby') - -# specify filename because IO object doesn't know it -payload[:file_with_name] = Faraday::FilePart.new(File.open(__FILE__), - 'text/x-ruby', - File.basename(__FILE__)) - -# Sets a custom Content-Disposition: -# nil filename still defaults to File.basename(__FILE__) -payload[:file_with_header] = Faraday::FilePart.new(__FILE__, - 'text/x-ruby', nil, - 'Content-Disposition' => 'form-data; foo=1') - -# Upload raw json with content type -payload[:raw_data] = Faraday::ParamPart.new({a: 1}.to_json, 'application/json') - -# optionally sets Content-ID too -payload[:raw_with_id] = Faraday::ParamPart.new({a: 1}.to_json, 'application/json', - 'foo-123') - -conn.post('/', payload) -``` diff --git a/docs/middleware/request/retry.md b/docs/middleware/request/retry.md deleted file mode 100644 index 4d5396fad..000000000 --- a/docs/middleware/request/retry.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -layout: documentation -title: "Retry Middleware" -permalink: /middleware/retry -hide: true -prev_name: UrlEncoded Middleware -prev_link: ./url-encoded -next_name: Instrumentation Middleware -next_link: ./instrumentation -top_name: Back to Middleware -top_link: ./list ---- - -The `Retry` middleware automatically retries requests that fail due to intermittent client -or server errors (such as network hiccups). -By default, it retries 2 times and handles only timeout exceptions. -It can be configured with an arbitrary number of retries, a list of exceptions to handle, -a retry interval, a percentage of randomness to add to the retry interval, and a backoff factor. - -### Example Usage - -This example will result in a first interval that is random between 0.05 and 0.075 -and a second interval that is random between 0.1 and 0.15. - -```ruby -retry_options = { - max: 2, - interval: 0.05, - interval_randomness: 0.5, - backoff_factor: 2 -} - -conn = Faraday.new(...) do |f| - f.request :retry, retry_options - ... -end - -conn.get('/') -``` - -### Control when the middleware will retry requests - -By default, the `Retry` middleware will only retry idempotent methods and the most common network-related exceptions. -You can change this behaviour by providing the right option when adding the middleware to your connection. - -#### Specify which methods will be retried - -You can provide a `methods` option with a list of HTTP methods. -This will replace the default list of HTTP methods: `delete`, `get`, `head`, `options`, `put`. - -```ruby -retry_options = { - methods: %i[get post] -} -``` - -#### Specify which exceptions should trigger a retry - -You can provide an `exceptions` option with a list of exceptions that will replace -the default list of network-related exceptions: `Errno::ETIMEDOUT`, `Timeout::Error`, `Faraday::TimeoutError`. -This can be particularly useful when combined with the [RaiseError][raise_error] middleware. - -```ruby -retry_options = { - exceptions: [Faraday::ResourceNotFound, Faraday::UnauthorizedError] -} -``` - -#### Specify on which response statuses to retry - -By default the `Retry` middleware will only retry the request if one of the expected exceptions arise. -However, you can specify a list of HTTP statuses you'd like to be retried. When you do so, the middleware will -check the response `status` code and will retry the request if included in the list. - -```ruby -retry_options = { - retry_statuses: [401, 409] -} -``` - -#### Specify a custom retry logic - -You can also specify a custom retry logic with the `retry_if` option. -This option accepts a block that will receive the `env` object and the exception raised -and should decide if the code should retry still the action or not independent of the retry count. -This would be useful if the exception produced is non-recoverable or if the the HTTP method called is not idempotent. - -**NOTE:** this option will only be used for methods that are not included in the `methods` option. -If you want this to apply to all HTTP methods, pass `methods: []` as an additional option. - -```ruby -# Retries the request if response contains { success: false } -retry_options = { - retry_if: -> (env, _exc) { env.body[:success] == 'false' } -} -``` - -### Call a block on every retry - -You can specify a block through the `retry_block` option that will be called every time the request is retried. -There are many different applications for this feature, spacing from instrumentation to monitoring. -Request environment, middleware options, current number of retries and the exception is passed to the block as parameters. -For example, you might want to keep track of the response statuses: - -```ruby -response_statuses = [] -retry_options = { - retry_block: -> (env, options, retries, exc) { response_statuses << env.status } -} -``` - - -[raise_error]: ../middleware/raise-error \ No newline at end of file diff --git a/docs/middleware/response/raise_error.md b/docs/middleware/response/raise_error.md deleted file mode 100644 index 286d0fbb8..000000000 --- a/docs/middleware/response/raise_error.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -layout: documentation -title: "Raise Error Middleware" -permalink: /middleware/raise-error -hide: true -prev_name: Logger Middleware -prev_link: ./logger -top_name: Back to Middleware -top_link: ./list ---- - -The `RaiseError` middleware raises a `Faraday::Error` exception if an HTTP -response returns with a 4xx or 5xx status code. All exceptions are initialized -providing the response `status`, `headers`, and `body`. - -```ruby -begin - conn.get('/wrong-url') # => Assume this raises a 404 response -rescue Faraday::ResourceNotFound => e - e.response[:status] #=> 404 - e.response[:headers] #=> { ... } - e.response[:body] #=> "..." -end -``` - -Specific exceptions are raised based on the HTTP Status code, according to the list below: - -An HTTP status in the 400-499 range typically represents an error -by the client. They raise error classes inheriting from `Faraday::ClientError`. - -* 400 => `Faraday::BadRequestError` -* 401 => `Faraday::UnauthorizedError` -* 403 => `Faraday::ForbiddenError` -* 404 => `Faraday::ResourceNotFound` -* 407 => `Faraday::ProxyAuthError` -* 409 => `Faraday::ConflictError` -* 422 => `Faraday::UnprocessableEntityError` -* 4xx => `Faraday::ClientError` - -An HTTP status in the 500-599 range represents a server error, and raises a -`Faraday::ServerError` exception. - -* 5xx => `Faraday::ServerError` - -The HTTP response status may be nil due to a malformed HTTP response from the -server, or a bug in the underlying HTTP library. It inherits from -`Faraday::ServerError`. - -* nil => `Faraday::NilStatusError` diff --git a/docs/team.md b/docs/team.md deleted file mode 100644 index 9833cdbba..000000000 --- a/docs/team.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -layout: page -title: Team -permalink: /team/ -order: 4 ---- - -
-
-
- - - - diff --git a/docs/usage/index.md b/docs/usage/index.md deleted file mode 100644 index 9ece9a527..000000000 --- a/docs/usage/index.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -layout: documentation -title: "Usage" -permalink: /usage/ -next_name: Customizing the Request -next_link: ./customize -order: 1 ---- - -Make a simple `GET` request by requiring the Faraday gem and using `Faraday.get`: - -```ruby -response = Faraday.get 'http://sushi.com/nigiri/sake.json' -``` - -This returns a `Faraday::Response` object with the response status, headers, and -body. - -```ruby -response.status -# => 200 - -response.headers -# => {"server"=>"sushi.com", "content-type"=>"text/html; charset=utf-8"... - -response.body -# => "... -``` - -### Requests without a body - -Faraday supports the following HTTP verbs that typically don't include a request -body: - -* `get` -* `head` -* `delete` -* `trace` - -You can specify URI query parameters and HTTP headers when making a request. - - -```ruby -url = 'http://sushi.com/nigiri/sake.json' -resp = Faraday.get(url, {a: 1}, {'Accept' => 'application/json'}) -# => GET http://sushi.com/nigiri/sake.json?a=1 -``` - -[Learn more about parameters encoding][encoding]. - -### Requests with a body - -Faraday also supports HTTP verbs that do include request bodies, though the -optional method arguments are different. Instead of HTTP query params, these -methods accept a response body. - -* `post` -* `put` -* `patch` - -```ruby -# POST 'application/x-www-form-urlencoded' content -url = 'http://sushi.com/fave' -resp = Faraday.post(url, "choice=sake") - -# POST JSON content -resp = Faraday.post(url, '{"choice": "sake"}', - "Content-Type" => "application/json") -``` - -#### Form upload - -Faraday can automatically convert hashes to values for form or multipart request -bodies. - -```ruby -url = 'http://sushi.com/fave' -resp = Faraday.post(url, choice: 'sake') -# => POST 'choice=sake' to http://sushi.com/fave -``` - -[Learn more about uploading files][multipart]. - -### Detailed HTTP Requests - -All HTTP verbs support a longer form style of making requests. This is handy if -you need to change a lot of the defaults, or if the details of the HTTP request -change according to method arguments. Each of the HTTP verb helpers can yield a -`Faraday::Request` that can be modified before being sent. - -This example shows a hypothetical search endpoint that accepts a JSON request -body as the actual search query. - -```ruby -resp = Faraday.get('http://sushi.com/search') do |req| - req.params['limit'] = 100 - req.headers['Content-Type'] = 'application/json' - req.body = {query: 'salmon'}.to_json -end -# => GET http://sushi.com/search?limit=100 -``` - -### The Connection Object - -A more flexible way to use Faraday is to start with a `Faraday::Connection` -object. Connection objects can store a common URL base path or HTTP headers to -apply to every request. All of the HTTP verb helpers described above -(`Faraday.get`, `Faraday.post`, etc) are available on the `Faraday::Connection` -instance. - -```ruby -conn = Faraday.new( - url: 'http://sushi.com', - params: {param: '1'}, - headers: {'Content-Type' => 'application/json'} -) - -resp = conn.get('search') do |req| - req.params['limit'] = 100 - req.body = {query: 'salmon'}.to_json -end -# => GET http://sushi.com/search?param=1&limit=100 -``` - -A `Faraday::Connection` object can also be used to change the default HTTP -adapter or add custom middleware that runs during Faraday's request/response -cycle. - -[Learn more about Middleware](../middleware). - -[encoding]: ../middleware/url-encoded -[multipart]: ../middleware/multipart diff --git a/docs/usage/streaming.md b/docs/usage/streaming.md deleted file mode 100644 index 24df5255b..000000000 --- a/docs/usage/streaming.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -layout: documentation -title: "Streaming Responses" -permalink: /usage/streaming -hide: true -top_name: Usage -top_link: ./ -prev_name: Customizing the Request -prev_link: ./customize ---- - -Sometimes you might need to receive a streaming response. -You can do this with the `on_data` request option. - -The `on_data` callback is a receives tuples of chunk Strings, and the total -of received bytes so far. - -This example implements such a callback: - -```ruby -# A buffer to store the streamed data -streamed = [] - -conn.get('/nigiri/sake.json') do |req| - # Set a callback which will receive tuples of chunk Strings - # and the sum of characters received so far - req.options.on_data = Proc.new do |chunk, overall_received_bytes| - puts "Received #{overall_received_bytes} characters" - streamed << chunk - end -end - -# Joins all response chunks together -streamed.join -``` - -The `on_data` streaming is currently only supported by the `Net::HTTP` adapter. diff --git a/examples/client_spec.rb b/examples/client_spec.rb index 59721455c..e30d86f79 100644 --- a/examples/client_spec.rb +++ b/examples/client_spec.rb @@ -12,54 +12,108 @@ def initialize(conn) @conn = conn end - def sushi(jname) - res = @conn.get("/#{jname}") + def httpbingo(jname, params: {}) + res = @conn.get("/#{jname}", params) data = JSON.parse(res.body) - data['name'] + data['origin'] + end + + def foo(params) + res = @conn.post('/foo', JSON.dump(params)) + res.status end end -Rspec.describe Client do +RSpec.describe Client do let(:stubs) { Faraday::Adapter::Test::Stubs.new } let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } } let(:client) { Client.new(conn) } - it 'parses name' do - stubs.get('/ebi') do |env| + it 'parses origin' do + stubs.get('/ip') do |env| # optional: you can inspect the Faraday::Env - expect(env.url.path).to eq('/ebi') + expect(env.url.path).to eq('/ip') [ 200, { 'Content-Type': 'application/javascript' }, - '{"name": "shrimp"}' + '{"origin": "127.0.0.1"}' ] end # uncomment to trigger stubs.verify_stubbed_calls failure # stubs.get('/unused') { [404, {}, ''] } - expect(client.sushi('ebi')).to eq('shrimp') + expect(client.httpbingo('ip')).to eq('127.0.0.1') stubs.verify_stubbed_calls end it 'handles 404' do - stubs.get('/ebi') do + stubs.get('/api') do [ 404, { 'Content-Type': 'application/javascript' }, '{}' ] end - expect(client.sushi('ebi')).to be_nil + expect(client.httpbingo('api')).to be_nil stubs.verify_stubbed_calls end it 'handles exception' do - stubs.get('/ebi') do - raise Faraday::ConnectionFailed, nil + stubs.get('/api') do + raise Faraday::ConnectionFailed end - expect { client.sushi('ebi') }.to raise_error(Faraday::ConnectionFailed) + expect { client.httpbingo('api') }.to raise_error(Faraday::ConnectionFailed) stubs.verify_stubbed_calls end + + context 'When the test stub is run in strict_mode' do + let(:stubs) { Faraday::Adapter::Test::Stubs.new(strict_mode: true) } + + it 'verifies the all parameter values are identical' do + stubs.get('/api?abc=123') do + [ + 200, + { 'Content-Type': 'application/javascript' }, + '{"origin": "127.0.0.1"}' + ] + end + + # uncomment to raise Stubs::NotFound + # expect(client.httpbingo('api', params: { abc: 123, foo: 'Kappa' })).to eq('127.0.0.1') + expect(client.httpbingo('api', params: { abc: 123 })).to eq('127.0.0.1') + stubs.verify_stubbed_calls + end + end + + context 'When the Faraday connection is configured with FlatParamsEncoder' do + let(:conn) { Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) { |b| b.adapter(:test, stubs) } } + + it 'handles the same multiple URL parameters' do + stubs.get('/api?a=x&a=y&a=z') { [200, { 'Content-Type' => 'application/json' }, '{"origin": "127.0.0.1"}'] } + + # uncomment to raise Stubs::NotFound + # expect(client.httpbingo('api', params: { a: %w[x y] })).to eq('127.0.0.1') + expect(client.httpbingo('api', params: { a: %w[x y z] })).to eq('127.0.0.1') + stubs.verify_stubbed_calls + end + end + + context 'When you want to test the body, you can use a proc as well as string' do + it 'tests with a string' do + stubs.post('/foo', '{"name":"YK"}') { [200, {}, ''] } + + expect(client.foo(name: 'YK')).to eq 200 + stubs.verify_stubbed_calls + end + + it 'tests with a proc' do + check = ->(request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } } + stubs.post('/foo', check) { [200, {}, ''] } + + expect(client.foo(name: 'YK', created_at: Time.now)).to eq 200 + stubs.verify_stubbed_calls + end + end end diff --git a/examples/client_test.rb b/examples/client_test.rb index 148f5fccf..3aad95762 100644 --- a/examples/client_test.rb +++ b/examples/client_test.rb @@ -13,24 +13,29 @@ def initialize(conn) @conn = conn end - def sushi(jname) - res = @conn.get("/#{jname}") + def httpbingo(jname, params: {}) + res = @conn.get("/#{jname}", params) data = JSON.parse(res.body) - data['name'] + data['origin'] + end + + def foo(params) + res = @conn.post('/foo', JSON.dump(params)) + res.status end end # Example API client test class ClientTest < Test::Unit::TestCase - def test_sushi_name + def test_httpbingo_name stubs = Faraday::Adapter::Test::Stubs.new - stubs.get('/ebi') do |env| + stubs.get('/api') do |env| # optional: you can inspect the Faraday::Env - assert_equal '/ebi', env.url.path + assert_equal '/api', env.url.path [ 200, { 'Content-Type': 'application/javascript' }, - '{"name": "shrimp"}' + '{"origin": "127.0.0.1"}' ] end @@ -38,13 +43,13 @@ def test_sushi_name # stubs.get('/unused') { [404, {}, ''] } cli = client(stubs) - assert_equal 'shrimp', cli.sushi('ebi') + assert_equal '127.0.0.1', cli.httpbingo('api') stubs.verify_stubbed_calls end - def test_sushi_404 + def test_httpbingo_not_found stubs = Faraday::Adapter::Test::Stubs.new - stubs.get('/ebi') do + stubs.get('/api') do [ 404, { 'Content-Type': 'application/javascript' }, @@ -53,20 +58,80 @@ def test_sushi_404 end cli = client(stubs) - assert_nil cli.sushi('ebi') + assert_nil cli.httpbingo('api') stubs.verify_stubbed_calls end - def test_sushi_exception + def test_httpbingo_exception stubs = Faraday::Adapter::Test::Stubs.new - stubs.get('/ebi') do - raise Faraday::ConnectionFailed, nil + stubs.get('/api') do + raise Faraday::ConnectionFailed end cli = client(stubs) assert_raise Faraday::ConnectionFailed do - cli.sushi('ebi') + cli.httpbingo('api') + end + stubs.verify_stubbed_calls + end + + def test_strict_mode + stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true) + stubs.get('/api?abc=123') do + [ + 200, + { 'Content-Type': 'application/javascript' }, + '{"origin": "127.0.0.1"}' + ] end + + cli = client(stubs) + assert_equal '127.0.0.1', cli.httpbingo('api', params: { abc: 123 }) + + # uncomment to raise Stubs::NotFound + # assert_equal '127.0.0.1', cli.httpbingo('api', params: { abc: 123, foo: 'Kappa' }) + stubs.verify_stubbed_calls + end + + def test_non_default_params_encoder + stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true) + stubs.get('/api?a=x&a=y&a=z') do + [ + 200, + { 'Content-Type': 'application/javascript' }, + '{"origin": "127.0.0.1"}' + ] + end + conn = Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) do |builder| + builder.adapter :test, stubs + end + + cli = Client.new(conn) + assert_equal '127.0.0.1', cli.httpbingo('api', params: { a: %w[x y z] }) + + # uncomment to raise Stubs::NotFound + # assert_equal '127.0.0.1', cli.httpbingo('api', params: { a: %w[x y] }) + stubs.verify_stubbed_calls + end + + def test_with_string_body + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + stub.post('/foo', '{"name":"YK"}') { [200, {}, ''] } + end + cli = client(stubs) + assert_equal 200, cli.foo(name: 'YK') + + stubs.verify_stubbed_calls + end + + def test_with_proc_body + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + check = ->(request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } } + stub.post('/foo', check) { [200, {}, ''] } + end + cli = client(stubs) + assert_equal 200, cli.foo(name: 'YK', created_at: Time.now) + stubs.verify_stubbed_calls end diff --git a/faraday.gemspec b/faraday.gemspec index fe2990f25..650634973 100644 --- a/faraday.gemspec +++ b/faraday.gemspec @@ -1,13 +1,10 @@ # frozen_string_literal: true -lib = 'faraday' -lib_file = File.expand_path("../lib/#{lib}.rb", __FILE__) -File.read(lib_file) =~ /\bVERSION\s*=\s*["'](.+?)["']/ -version = Regexp.last_match(1) +require_relative 'lib/faraday/version' Gem::Specification.new do |spec| - spec.name = lib - spec.version = version + spec.name = 'faraday' + spec.version = Faraday::VERSION spec.summary = 'HTTP/REST API client library.' @@ -16,12 +13,20 @@ Gem::Specification.new do |spec| spec.homepage = 'https://lostisland.github.io/faraday' spec.licenses = ['MIT'] - spec.required_ruby_version = '>= 2.3' + spec.required_ruby_version = '>= 3.0' - spec.add_dependency 'multipart-post', '>= 1.2', '< 3' + # faraday-net_http is the "default adapter", but being a Faraday dependency it can't + # control which version of faraday it will be pulled from. + # To avoid releasing a major version every time there's a new Faraday API, we should + # always fix its required version to the next MINOR version. + # This way, we can release minor versions of the adapter with "breaking" changes for older versions of Faraday + # and then bump the version requirement on the next compatible version of faraday. + spec.add_dependency 'faraday-net_http', '>= 2.0', '< 3.2' + spec.add_dependency 'logger' - files = %w[CHANGELOG.md LICENSE.md README.md Rakefile examples lib spec] - spec.files = `git ls-files -z #{files.join(' ')}`.split("\0") + # Includes `examples` and `spec` to allow external adapter gems to run Faraday unit and integration tests + spec.files = Dir['CHANGELOG.md', '{examples,lib,spec}/**/*', 'LICENSE.md', 'Rakefile', 'README.md'] + spec.require_paths = %w[lib spec/external_adapters] spec.metadata = { 'homepage_uri' => 'https://lostisland.github.io/faraday', 'changelog_uri' => diff --git a/lib/faraday.rb b/lib/faraday.rb index 3e99aeca4..758a08dee 100644 --- a/lib/faraday.rb +++ b/lib/faraday.rb @@ -1,11 +1,23 @@ # frozen_string_literal: true require 'cgi' +require 'date' require 'set' require 'forwardable' +require 'faraday/version' +require 'faraday/methods' +require 'faraday/error' require 'faraday/middleware_registry' -require 'faraday/dependency_loader' - +require 'faraday/utils' +require 'faraday/options' +require 'faraday/connection' +require 'faraday/rack_builder' +require 'faraday/parameters' +require 'faraday/middleware' +require 'faraday/adapter' +require 'faraday/request' +require 'faraday/response' +require 'faraday/net_http' # This is the main namespace for Faraday. # # It provides methods to create {Connection} objects, and HTTP-related @@ -19,9 +31,7 @@ # conn.get '/' # module Faraday - VERSION = '1.0.0' - METHODS_WITH_QUERY = %w[get head delete trace].freeze - METHODS_WITH_BODY = %w[post put patch].freeze + CONTENT_TYPE = 'Content-Type' class << self # The root path that Faraday is being loaded from. @@ -44,6 +54,10 @@ class << self # @return [Symbol] the new default_adapter. attr_reader :default_adapter + # Option for the default_adapter + # @return [Hash] default_adapter options + attr_accessor :default_adapter_options + # Documented below, see default_connection attr_writer :default_connection @@ -80,23 +94,10 @@ class << self # params: { page: 1 } # # => Faraday::Connection to http://faraday.com?page=1 def new(url = nil, options = {}, &block) - options = default_connection_options.merge(options) + options = Utils.deep_merge(default_connection_options, options) Faraday::Connection.new(url, options, &block) end - # @private - # Internal: Requires internal Faraday libraries. - # - # @param libs [Array] one or more relative String names to Faraday classes. - # @return [void] - def require_libs(*libs) - libs.each do |lib| - require "#{lib_path}/#{lib}" - end - end - - alias require_lib require_libs - # Documented elsewhere, see default_adapter reader def default_adapter=(adapter) @default_connection = nil @@ -107,6 +108,34 @@ def respond_to_missing?(symbol, include_private = false) default_connection.respond_to?(symbol, include_private) || super end + # @overload default_connection + # Gets the default connection used for simple scripts. + # @return [Faraday::Connection] a connection configured with + # the default_adapter. + # @overload default_connection=(connection) + # @param connection [Faraday::Connection] + # Sets the default {Faraday::Connection} for simple scripts that + # access the Faraday constant directly, such as + # Faraday.get "https://faraday.com". + def default_connection + @default_connection ||= Connection.new(default_connection_options) + end + + # Gets the default connection options used when calling {Faraday#new}. + # + # @return [Faraday::ConnectionOptions] + def default_connection_options + @default_connection_options ||= ConnectionOptions.new + end + + # Sets the default options used when calling {Faraday#new}. + # + # @param options [Hash, Faraday::ConnectionOptions] + def default_connection_options=(options) + @default_connection = nil + @default_connection_options = ConnectionOptions.from(options) + end + private # Internal: Proxies method calls on the Faraday constant to @@ -124,43 +153,5 @@ def method_missing(name, *args, &block) self.root_path = File.expand_path __dir__ self.lib_path = File.expand_path 'faraday', __dir__ self.default_adapter = :net_http - - # @overload default_connection - # Gets the default connection used for simple scripts. - # @return [Faraday::Connection] a connection configured with - # the default_adapter. - # @overload default_connection=(connection) - # @param connection [Faraday::Connection] - # Sets the default {Faraday::Connection} for simple scripts that - # access the Faraday constant directly, such as - # Faraday.get "https://faraday.com". - def self.default_connection - @default_connection ||= Connection.new(default_connection_options) - end - - # Gets the default connection options used when calling {Faraday#new}. - # - # @return [Faraday::ConnectionOptions] - def self.default_connection_options - @default_connection_options ||= ConnectionOptions.new - end - - # Sets the default options used when calling {Faraday#new}. - # - # @param options [Hash, Faraday::ConnectionOptions] - def self.default_connection_options=(options) - @default_connection = nil - @default_connection_options = ConnectionOptions.from(options) - end - - unless const_defined? :Timer - require 'timeout' - Timer = Timeout - end - - require_libs 'utils', 'options', 'connection', 'rack_builder', 'parameters', - 'middleware', 'adapter', 'request', 'response', 'error', - 'file_part', 'param_part' - - require_lib 'autoload' unless ENV['FARADAY_NO_AUTOLOAD'] + self.default_adapter_options = {} end diff --git a/lib/faraday/adapter.rb b/lib/faraday/adapter.rb index 407c2367f..1d9a45084 100644 --- a/lib/faraday/adapter.rb +++ b/lib/faraday/adapter.rb @@ -5,28 +5,13 @@ module Faraday # responsible for fulfilling a Faraday request. class Adapter extend MiddlewareRegistry - extend DependencyLoader CONTENT_LENGTH = 'Content-Length' - register_middleware File.expand_path('adapter', __dir__), - test: [:Test, 'test'], - net_http: [:NetHttp, 'net_http'], - net_http_persistent: [ - :NetHttpPersistent, - 'net_http_persistent' - ], - typhoeus: [:Typhoeus, 'typhoeus'], - patron: [:Patron, 'patron'], - em_synchrony: [:EMSynchrony, 'em_synchrony'], - em_http: [:EMHttp, 'em_http'], - excon: [:Excon, 'excon'], - rack: [:Rack, 'rack'], - httpclient: [:HTTPClient, 'httpclient'] - # This module marks an Adapter as supporting parallel requests. module Parallelism attr_writer :supports_parallel + def supports_parallel? @supports_parallel end @@ -41,7 +26,7 @@ def inherited(subclass) self.supports_parallel = false def initialize(_app = nil, opts = {}, &block) - @app = ->(env) { env.response } + @app = lambda(&:response) @connection_options = opts @config_block = block end @@ -74,7 +59,7 @@ def call(env) private - def save_response(env, status, body, headers = nil, reason_phrase = nil) + def save_response(env, status, body, headers = nil, reason_phrase = nil, finished: true) env.status = status env.body = body env.reason_phrase = reason_phrase&.to_s&.strip @@ -83,7 +68,7 @@ def save_response(env, status, body, headers = nil, reason_phrase = nil) yield(response_headers) if block_given? end - env.response.finish(env) unless env.parallel? + env.response.finish(env) unless env.parallel? || !finished env.response end @@ -93,8 +78,7 @@ def save_response(env, status, body, headers = nil, reason_phrase = nil) # @param type [Symbol] Describes which timeout setting to get: :read, # :write, or :open. # @param options [Hash] Hash containing Symbol keys like :timeout, - # :read_timeout, :write_timeout, :open_timeout, or - # :timeout + # :read_timeout, :write_timeout, or :open_timeout # # @return [Integer, nil] Timeout duration in seconds, or nil if no timeout # has been set. @@ -113,3 +97,5 @@ def request_timeout(type, options) }.freeze end end + +require 'faraday/adapter/test' diff --git a/lib/faraday/adapter/em_http.rb b/lib/faraday/adapter/em_http.rb deleted file mode 100644 index acadc35ff..000000000 --- a/lib/faraday/adapter/em_http.rb +++ /dev/null @@ -1,285 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Adapter - # EventMachine adapter. This adapter is useful for either asynchronous - # requests when in an EM reactor loop, or for making parallel requests in - # synchronous code. - class EMHttp < Faraday::Adapter - # Options is a module containing helpers to convert the Faraday env object - # into options hashes for EMHTTP method calls. - module Options - # @return [Hash] - def connection_config(env) - options = {} - configure_proxy(options, env) - configure_timeout(options, env) - configure_socket(options, env) - configure_ssl(options, env) - options - end - - def request_config(env) - options = { - body: read_body(env), - head: env[:request_headers] - # keepalive: true, - # file: 'path/to/file', # stream data off disk - } - configure_compression(options, env) - options - end - - def read_body(env) - body = env[:body] - body.respond_to?(:read) ? body.read : body - end - - # Reads out proxy settings from env into options - def configure_proxy(options, env) - proxy = request_options(env)[:proxy] - return unless proxy - - options[:proxy] = { - host: proxy[:uri].host, - port: proxy[:uri].port, - authorization: [proxy[:user], proxy[:password]] - } - end - - # Reads out host and port settings from env into options - def configure_socket(options, env) - bind = request_options(env)[:bind] - return unless bind - - options[:bind] = { - host: bind[:host], - port: bind[:port] - } - end - - # Reads out SSL certificate settings from env into options - def configure_ssl(options, env) - return unless env[:url].scheme == 'https' && env[:ssl] - - options[:ssl] = { - cert_chain_file: env[:ssl][:ca_file], - verify_peer: env[:ssl].fetch(:verify, true) - } - end - - # Reads out timeout settings from env into options - def configure_timeout(options, env) - req = request_options(env) - options[:inactivity_timeout] = request_timeout(:read, req) - options[:connect_timeout] = request_timeout(:open, req) - end - - # Reads out compression header settings from env into options - def configure_compression(options, env) - return unless (env[:method] == :get) && - !options[:head].key?('accept-encoding') - - options[:head]['accept-encoding'] = 'gzip, compressed' - end - - def request_options(env) - env[:request] - end - end - - include Options - - dependency 'em-http' - - self.supports_parallel = true - - # @return [Manager] - def self.setup_parallel_manager(_options = nil) - Manager.new - end - - def call(env) - super - perform_request env - @app.call env - end - - def perform_request(env) - if parallel?(env) - manager = env[:parallel_manager] - manager.add do - perform_single_request(env) - .callback { env[:response].finish(env) } - end - elsif EventMachine.reactor_running? - # EM is running: instruct upstream that this is an async request - env[:parallel_manager] = true - perform_single_request(env) - .callback { env[:response].finish(env) } - .errback do - # TODO: no way to communicate the error in async mode - raise NotImplementedError - end - else - error = nil - # start EM, block until request is completed - EventMachine.run do - perform_single_request(env) - .callback { EventMachine.stop } - .errback do |client| - error = error_message(client) - EventMachine.stop - end - end - raise_error(error) if error - end - rescue EventMachine::Connectify::CONNECTError => e - if e.message.include?('Proxy Authentication Required') - raise Faraday::ConnectionFailed, - %(407 "Proxy Authentication Required ") - end - - raise Faraday::ConnectionFailed, e - rescue StandardError => e - if defined?(OpenSSL) && e.is_a?(OpenSSL::SSL::SSLError) - raise Faraday::SSLError, e - end - - raise - end - - # TODO: reuse the connection to support pipelining - def perform_single_request(env) - req = create_request(env) - req = req.setup_request(env[:method], request_config(env)) - req.callback do |client| - if env[:request].stream_response? - warn "Streaming downloads for #{self.class.name} " \ - 'are not yet implemented.' - env[:request].on_data.call( - client.response, - client.response.bytesize - ) - end - status = client.response_header.status - reason = client.response_header.http_reason - save_response(env, status, client.response, nil, reason) do |headers| - client.response_header.each do |name, value| - headers[name.to_sym] = value - end - end - end - end - - def create_request(env) - EventMachine::HttpRequest.new( - env[:url], connection_config(env).merge(@connection_options) - ) - end - - def error_message(client) - client.error || 'request failed' - end - - def raise_error(msg) - error_class = Faraday::ClientError - if timeout_message?(msg) - error_class = Faraday::TimeoutError - msg = 'request timed out' - elsif msg == Errno::ECONNREFUSED - error_class = Faraday::ConnectionFailed - msg = 'connection refused' - elsif msg == 'connection closed by server' - error_class = Faraday::ConnectionFailed - end - raise error_class, msg - end - - def timeout_message?(msg) - msg == Errno::ETIMEDOUT || - (msg.is_a?(String) && msg.include?('timeout error')) - end - - # @return [Boolean] - def parallel?(env) - !!env[:parallel_manager] - end - - # This parallel manager is designed to start an EventMachine loop - # and block until all registered requests have been completed. - class Manager - # @see reset - def initialize - reset - end - - # Re-initializes instance variables - def reset - @registered_procs = [] - @num_registered = 0 - @num_succeeded = 0 - @errors = [] - @running = false - end - - # @return [Boolean] - def running? - @running - end - - def add(&block) - if running? - perform_request { yield } - else - @registered_procs << block - end - @num_registered += 1 - end - - def run - if @num_registered.positive? - @running = true - EventMachine.run do - @registered_procs.each do |proc| - perform_request(&proc) - end - end - unless @errors.empty? - raise Faraday::ClientError, @errors.first || 'connection failed' - end - end - ensure - reset - end - - def perform_request - client = yield - client.callback do - @num_succeeded += 1 - check_finished - end - client.errback do - @errors << client.error - check_finished - end - end - - def check_finished - EventMachine.stop if @num_succeeded + @errors.size == @num_registered - end - end - end - end -end - -if Faraday::Adapter::EMHttp.loaded? - begin - require 'openssl' - rescue LoadError - warn 'Warning: no such file to load -- openssl. ' \ - 'Make sure it is installed if you want HTTPS support' - else - require 'faraday/adapter/em_http_ssl_patch' - end -end diff --git a/lib/faraday/adapter/em_http_ssl_patch.rb b/lib/faraday/adapter/em_http_ssl_patch.rb deleted file mode 100644 index d33a9c4cf..000000000 --- a/lib/faraday/adapter/em_http_ssl_patch.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'openssl' -require 'em-http' - -# EventMachine patch to make SSL work. -module EmHttpSslPatch - def ssl_verify_peer(cert_string) - begin - @last_seen_cert = OpenSSL::X509::Certificate.new(cert_string) - rescue OpenSSL::X509::CertificateError - return false - end - - unless certificate_store.verify(@last_seen_cert) - raise OpenSSL::SSL::SSLError, - %(unable to verify the server certificate for "#{host}") - end - - begin - certificate_store.add_cert(@last_seen_cert) - rescue OpenSSL::X509::StoreError => e - raise e unless e.message == 'cert already in hash table' - end - true - end - - def ssl_handshake_completed - return true unless verify_peer? - - unless verified_cert_identity? - raise OpenSSL::SSL::SSLError, - %(host "#{host}" does not match the server certificate) - end - - true - end - - def verify_peer? - parent.connopts.tls[:verify_peer] - end - - def verified_cert_identity? - OpenSSL::SSL.verify_certificate_identity(@last_seen_cert, host) - end - - def host - parent.uri.host - end - - def certificate_store - @certificate_store ||= begin - store = OpenSSL::X509::Store.new - store.set_default_paths - ca_file = parent.connopts.tls[:cert_chain_file] - store.add_file(ca_file) if ca_file - store - end - end -end - -EventMachine::HttpStubConnection.include(EmHttpSslPatch) diff --git a/lib/faraday/adapter/em_synchrony.rb b/lib/faraday/adapter/em_synchrony.rb deleted file mode 100644 index 0e94c0dbe..000000000 --- a/lib/faraday/adapter/em_synchrony.rb +++ /dev/null @@ -1,150 +0,0 @@ -# frozen_string_literal: true - -require 'uri' - -module Faraday - class Adapter - # EventMachine Synchrony adapter. - class EMSynchrony < Faraday::Adapter - include EMHttp::Options - - dependency do - require 'em-synchrony/em-http' - require 'em-synchrony/em-multi' - require 'fiber' - end - - self.supports_parallel = true - - # @return [ParallelManager] - def self.setup_parallel_manager(_options = nil) - ParallelManager.new - end - - def call(env) - super - request = create_request(env) - - http_method = env[:method].to_s.downcase.to_sym - - if env[:parallel_manager] - # Queue requests for parallel execution. - execute_parallel_request(env, request, http_method) - else - # Execute single request. - execute_single_request(env, request, http_method) - end - - @app.call env - rescue Errno::ECONNREFUSED - raise Faraday::ConnectionFailed, $ERROR_INFO - rescue EventMachine::Connectify::CONNECTError => e - if e.message.include?('Proxy Authentication Required') - raise Faraday::ConnectionFailed, - %(407 "Proxy Authentication Required") - end - - raise Faraday::ConnectionFailed, e - rescue Errno::ETIMEDOUT => e - raise Faraday::TimeoutError, e - rescue RuntimeError => e - if e.message == 'connection closed by server' - raise Faraday::ConnectionFailed, e - end - - raise Faraday::TimeoutError, e if e.message.include?('timeout error') - - raise - rescue StandardError => e - if defined?(OpenSSL) && e.is_a?(OpenSSL::SSL::SSLError) - raise Faraday::SSLError, e - end - - raise - end - - def create_request(env) - EventMachine::HttpRequest.new( - Utils::URI(env[:url].to_s), - connection_config(env).merge(@connection_options) - ) - end - - private - - def execute_parallel_request(env, request, http_method) - env[:parallel_manager].add(request, http_method, - request_config(env)) do |resp| - if (req = env[:request]).stream_response? - warn "Streaming downloads for #{self.class.name} " \ - 'are not yet implemented.' - req.on_data.call(resp.response, resp.response.bytesize) - end - - save_response(env, resp.response_header.status, - resp.response) do |resp_headers| - resp.response_header.each do |name, value| - resp_headers[name.to_sym] = value - end - end - - # Finalize the response object with values from `env`. - env[:response].finish(env) - end - end - - def execute_single_request(env, request, http_method) - block = -> { request.send(http_method, request_config(env)) } - client = call_block(block) - - raise client.error if client&.error - - if env[:request].stream_response? - warn "Streaming downloads for #{self.class.name} " \ - 'are not yet implemented.' - env[:request].on_data.call( - client.response, - client.response.bytesize - ) - end - status = client.response_header.status - reason = client.response_header.http_reason - save_response(env, status, client.response, nil, reason) do |headers| - client.response_header.each do |name, value| - headers[name.to_sym] = value - end - end - end - - def call_block(block) - client = nil - - if EM.reactor_running? - client = block.call - else - EM.run do - Fiber.new do - client = block.call - EM.stop - end.resume - end - end - - client - end - end - end -end - -require 'faraday/adapter/em_synchrony/parallel_manager' - -if Faraday::Adapter::EMSynchrony.loaded? - begin - require 'openssl' - rescue LoadError - warn 'Warning: no such file to load -- openssl. ' \ - 'Make sure it is installed if you want HTTPS support' - else - require 'faraday/adapter/em_http_ssl_patch' - end -end diff --git a/lib/faraday/adapter/em_synchrony/parallel_manager.rb b/lib/faraday/adapter/em_synchrony/parallel_manager.rb deleted file mode 100644 index 1701ae82d..000000000 --- a/lib/faraday/adapter/em_synchrony/parallel_manager.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Adapter - class EMSynchrony < Faraday::Adapter - # A parallel manager for EMSynchrony. - class ParallelManager - # Add requests to queue. - # - # @param request [EM::HttpRequest] - # @param method [Symbol, String] HTTP method - # @param args [Array] the rest of the positional arguments - def add(request, method, *args, &block) - queue << { - request: request, - method: method, - args: args, - block: block - } - end - - # Run all requests on queue with `EM::Synchrony::Multi`, wrapping - # it in a reactor and fiber if needed. - def run - result = nil - if !EM.reactor_running? - EM.run do - Fiber.new do - result = perform - EM.stop - end.resume - end - else - result = perform - end - result - end - - private - - # The request queue. - def queue - @queue ||= [] - end - - # Main `EM::Synchrony::Multi` performer. - def perform - multi = ::EM::Synchrony::Multi.new - - queue.each do |item| - method = "a#{item[:method]}".to_sym - - req = item[:request].send(method, *item[:args]) - req.callback(&item[:block]) - - req_name = "req_#{multi.requests.size}".to_sym - multi.add(req_name, req) - end - - # Clear the queue, so parallel manager objects can be reused. - @queue = [] - - # Block fiber until all requests have returned. - multi.perform - end - end - end - end -end diff --git a/lib/faraday/adapter/excon.rb b/lib/faraday/adapter/excon.rb deleted file mode 100644 index 2ba264e3b..000000000 --- a/lib/faraday/adapter/excon.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Adapter - # Excon adapter. - class Excon < Faraday::Adapter - dependency 'excon' - - def build_connection(env) - opts = opts_from_env(env) - ::Excon.new(env[:url].to_s, opts.merge(@connection_options)) - end - - def call(env) - super - - req_opts = { - method: env[:method].to_s.upcase, - headers: env[:request_headers], - body: read_body(env) - } - - req = env[:request] - if req&.stream_response? - total = 0 - req_opts[:response_block] = lambda do |chunk, _remain, _total| - req.on_data.call(chunk, total += chunk.size) - end - end - - resp = connection(env) { |http| http.request(req_opts) } - save_response(env, resp.status.to_i, resp.body, resp.headers, - resp.reason_phrase) - - @app.call(env) - rescue ::Excon::Errors::SocketError => e - raise Faraday::TimeoutError, e if e.message =~ /\btimeout\b/ - - raise Faraday::SSLError, e if e.message =~ /\bcertificate\b/ - - raise Faraday::ConnectionFailed, e - rescue ::Excon::Errors::Timeout => e - raise Faraday::TimeoutError, e - end - - # TODO: support streaming requests - def read_body(env) - env[:body].respond_to?(:read) ? env[:body].read : env[:body] - end - - private - - def opts_from_env(env) - opts = {} - amend_opts_with_ssl!(opts, env[:ssl]) if needs_ssl_settings?(env) - - if (req = env[:request]) - amend_opts_with_timeouts!(opts, req) - amend_opts_with_proxy_settings!(opts, req) - end - - opts - end - - def needs_ssl_settings?(env) - env[:url].scheme == 'https' && env[:ssl] - end - - OPTS_KEYS = [ - %i[client_cert client_cert], - %i[client_key client_key], - %i[certificate certificate], - %i[private_key private_key], - %i[ssl_ca_path ca_path], - %i[ssl_ca_file ca_file], - %i[ssl_version version], - %i[ssl_min_version min_version], - %i[ssl_max_version max_version] - ].freeze - - def amend_opts_with_ssl!(opts, ssl) - opts[:ssl_verify_peer] = !!ssl.fetch(:verify, true) - # https://github.com/geemus/excon/issues/106 - # https://github.com/jruby/jruby-ossl/issues/19 - opts[:nonblock] = false - - OPTS_KEYS.each do |(key_in_opts, key_in_ssl)| - next unless ssl[key_in_ssl] - - opts[key_in_opts] = ssl[key_in_ssl] - end - end - - def amend_opts_with_timeouts!(opts, req) - if (sec = request_timeout(:read, req)) - opts[:read_timeout] = sec - end - - if (sec = request_timeout(:write, req)) - opts[:write_timeout] = sec - end - - return unless (sec = request_timeout(:open, req)) - - opts[:connect_timeout] = sec - end - - def amend_opts_with_proxy_settings!(opts, req) - opts[:proxy] = proxy_settings_for_opts(req[:proxy]) if req[:proxy] - end - - def proxy_settings_for_opts(proxy) - { - host: proxy[:uri].host, - hostname: proxy[:uri].hostname, - port: proxy[:uri].port, - scheme: proxy[:uri].scheme, - user: proxy[:user], - password: proxy[:password] - } - end - end - end -end diff --git a/lib/faraday/adapter/httpclient.rb b/lib/faraday/adapter/httpclient.rb deleted file mode 100644 index 539c15aaa..000000000 --- a/lib/faraday/adapter/httpclient.rb +++ /dev/null @@ -1,151 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Adapter - # HTTPClient adapter. - class HTTPClient < Faraday::Adapter - dependency 'httpclient' - - def build_connection(env) - @client ||= ::HTTPClient.new.tap do |cli| - # enable compression - cli.transparent_gzip_decompression = true - end - - if (req = env[:request]) - if (proxy = req[:proxy]) - configure_proxy @client, proxy - end - - if (bind = req[:bind]) - configure_socket @client, bind - end - - configure_timeouts @client, req - end - - if env[:url].scheme == 'https' && (ssl = env[:ssl]) - configure_ssl @client, ssl - end - - configure_client @client - - @client - end - - def call(env) - super - - # TODO: Don't stream yet. - # https://github.com/nahi/httpclient/pull/90 - env[:body] = env[:body].read if env[:body].respond_to? :read - - connection(env) do |http| - resp = http.request env[:method], env[:url], - body: env[:body], - header: env[:request_headers] - - if (req = env[:request]).stream_response? - warn "Streaming downloads for #{self.class.name} " \ - 'are not yet implemented.' - req.on_data.call(resp.body, resp.body.bytesize) - end - save_response env, resp.status, resp.body, resp.headers, resp.reason - - @app.call env - end - rescue ::HTTPClient::TimeoutError, Errno::ETIMEDOUT - raise Faraday::TimeoutError, $ERROR_INFO - rescue ::HTTPClient::BadResponseError => e - if e.message.include?('status 407') - raise Faraday::ConnectionFailed, - %(407 "Proxy Authentication Required ") - end - - raise Faraday::ClientError, $ERROR_INFO - rescue Errno::EADDRNOTAVAIL, Errno::ECONNREFUSED, IOError, SocketError - raise Faraday::ConnectionFailed, $ERROR_INFO - rescue StandardError => e - if defined?(OpenSSL) && e.is_a?(OpenSSL::SSL::SSLError) - raise Faraday::SSLError, e - end - - raise - end - - # @param bind [Hash] - def configure_socket(client, bind) - client.socket_local.host = bind[:host] - client.socket_local.port = bind[:port] - end - - # Configure proxy URI and any user credentials. - # - # @param proxy [Hash] - def configure_proxy(client, proxy) - client.proxy = proxy[:uri] - return unless proxy[:user] && proxy[:password] - - client.set_proxy_auth(proxy[:user], proxy[:password]) - end - - # @param ssl [Hash] - def configure_ssl(client, ssl) - ssl_config = client.ssl_config - ssl_config.verify_mode = ssl_verify_mode(ssl) - ssl_config.cert_store = ssl_cert_store(ssl) - - ssl_config.add_trust_ca ssl[:ca_file] if ssl[:ca_file] - ssl_config.add_trust_ca ssl[:ca_path] if ssl[:ca_path] - ssl_config.client_cert = ssl[:client_cert] if ssl[:client_cert] - ssl_config.client_key = ssl[:client_key] if ssl[:client_key] - ssl_config.verify_depth = ssl[:verify_depth] if ssl[:verify_depth] - end - - # @param req [Hash] - def configure_timeouts(client, req) - if (sec = request_timeout(:open, req)) - client.connect_timeout = sec - end - - if (sec = request_timeout(:write, req)) - client.send_timeout = sec - end - - return unless (sec = request_timeout(:read, req)) - - client.receive_timeout = sec - end - - def configure_client(client) - @config_block&.call(client) - end - - # @param ssl [Hash] - # @return [OpenSSL::X509::Store] - def ssl_cert_store(ssl) - return ssl[:cert_store] if ssl[:cert_store] - - # Memoize the cert store so that the same one is passed to - # HTTPClient each time, to avoid resyncing SSL sessions when - # it's changed - @ssl_cert_store ||= begin - # Use the default cert store by default, i.e. system ca certs - OpenSSL::X509::Store.new.tap(&:set_default_paths) - end - end - - # @param ssl [Hash] - def ssl_verify_mode(ssl) - ssl[:verify_mode] || begin - if ssl.fetch(:verify, true) - OpenSSL::SSL::VERIFY_PEER | - OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - else - OpenSSL::SSL::VERIFY_NONE - end - end - end - end - end -end diff --git a/lib/faraday/adapter/net_http.rb b/lib/faraday/adapter/net_http.rb deleted file mode 100644 index 191568dd4..000000000 --- a/lib/faraday/adapter/net_http.rb +++ /dev/null @@ -1,209 +0,0 @@ -# frozen_string_literal: true - -begin - require 'net/https' -rescue LoadError - warn 'Warning: no such file to load -- net/https. ' \ - 'Make sure openssl is installed if you want ssl support' - require 'net/http' -end -require 'zlib' - -module Faraday - class Adapter - # Net::HTTP adapter. - class NetHttp < Faraday::Adapter - exceptions = [ - IOError, - Errno::EADDRNOTAVAIL, - Errno::ECONNABORTED, - Errno::ECONNREFUSED, - Errno::ECONNRESET, - Errno::EHOSTUNREACH, - Errno::EINVAL, - Errno::ENETUNREACH, - Errno::EPIPE, - Net::HTTPBadResponse, - Net::HTTPHeaderSyntaxError, - Net::ProtocolError, - SocketError, - Zlib::GzipFile::Error - ] - - exceptions << OpenSSL::SSL::SSLError if defined?(OpenSSL) - exceptions << Net::OpenTimeout if defined?(Net::OpenTimeout) - - NET_HTTP_EXCEPTIONS = exceptions.freeze - - def initialize(app = nil, opts = {}, &block) - @ssl_cert_store = nil - super(app, opts, &block) - end - - def build_connection(env) - net_http_connection(env).tap do |http| - if http.respond_to?(:use_ssl=) - http.use_ssl = env[:url].scheme == 'https' - end - configure_ssl(http, env[:ssl]) - configure_request(http, env[:request]) - end - end - - def net_http_connection(env) - klass = if (proxy = env[:request][:proxy]) - Net::HTTP::Proxy(proxy[:uri].hostname, proxy[:uri].port, - proxy[:user], proxy[:password]) - else - Net::HTTP - end - port = env[:url].port || (env[:url].scheme == 'https' ? 443 : 80) - klass.new(env[:url].hostname, port) - end - - def call(env) - super - http_response = connection(env) do |http| - begin - perform_request(http, env) - rescue *NET_HTTP_EXCEPTIONS => e - if defined?(OpenSSL) && e.is_a?(OpenSSL::SSL::SSLError) - raise Faraday::SSLError, e - end - - raise Faraday::ConnectionFailed, e - end - end - - save_response(env, http_response.code.to_i, - http_response.body || +'', nil, - http_response.message) do |response_headers| - http_response.each_header do |key, value| - response_headers[key] = value - end - end - - @app.call env - rescue Timeout::Error, Errno::ETIMEDOUT => e - raise Faraday::TimeoutError, e - end - - private - - def create_request(env) - request = Net::HTTPGenericRequest.new \ - env[:method].to_s.upcase, # request method - !!env[:body], # is there request body - env[:method] != :head, # is there response body - env[:url].request_uri, # request uri path - env[:request_headers] # request headers - - if env[:body].respond_to?(:read) - request.body_stream = env[:body] - else - request.body = env[:body] - end - request - end - - def perform_request(http, env) - if env[:request].stream_response? - size = 0 - yielded = false - http_response = request_with_wrapped_block(http, env) do |chunk| - if chunk.bytesize.positive? || size.positive? - yielded = true - size += chunk.bytesize - env[:request].on_data.call(chunk, size) - end - end - env[:request].on_data.call(+'', 0) unless yielded - # Net::HTTP returns something, - # but it's not meaningful according to the docs. - http_response.body = nil - http_response - else - request_with_wrapped_block(http, env) - end - end - - def request_with_wrapped_block(http, env, &block) - if (env[:method] == :get) && !env[:body] - # prefer `get` to `request` because the former handles gzip (ruby 1.9) - request_via_get_method(http, env, &block) - else - request_via_request_method(http, env, &block) - end - end - - def request_via_get_method(http, env, &block) - http.get env[:url].request_uri, env[:request_headers], &block - end - - def request_via_request_method(http, env, &block) - if block_given? - http.request create_request(env) do |response| - response.read_body(&block) - end - else - http.request create_request(env) - end - end - - def configure_ssl(http, ssl) - return unless ssl - - http.verify_mode = ssl_verify_mode(ssl) - http.cert_store = ssl_cert_store(ssl) - - http.cert = ssl[:client_cert] if ssl[:client_cert] - http.key = ssl[:client_key] if ssl[:client_key] - http.ca_file = ssl[:ca_file] if ssl[:ca_file] - http.ca_path = ssl[:ca_path] if ssl[:ca_path] - http.verify_depth = ssl[:verify_depth] if ssl[:verify_depth] - http.ssl_version = ssl[:version] if ssl[:version] - http.min_version = ssl[:min_version] if ssl[:min_version] - http.max_version = ssl[:max_version] if ssl[:max_version] - end - - def configure_request(http, req) - if (sec = request_timeout(:read, req)) - http.read_timeout = sec - end - - if (sec = http.respond_to?(:write_timeout=) && - request_timeout(:write, req)) - http.write_timeout = sec - end - - if (sec = request_timeout(:open, req)) - http.open_timeout = sec - end - - # Only set if Net::Http supports it, since Ruby 2.5. - http.max_retries = 0 if http.respond_to?(:max_retries=) - - @config_block&.call(http) - end - - def ssl_cert_store(ssl) - return ssl[:cert_store] if ssl[:cert_store] - - @ssl_cert_store ||= begin - # Use the default cert store by default, i.e. system ca certs - OpenSSL::X509::Store.new.tap(&:set_default_paths) - end - end - - def ssl_verify_mode(ssl) - ssl[:verify_mode] || begin - if ssl.fetch(:verify, true) - OpenSSL::SSL::VERIFY_PEER - else - OpenSSL::SSL::VERIFY_NONE - end - end - end - end - end -end diff --git a/lib/faraday/adapter/net_http_persistent.rb b/lib/faraday/adapter/net_http_persistent.rb deleted file mode 100644 index ff20c2567..000000000 --- a/lib/faraday/adapter/net_http_persistent.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Adapter - # Net::HTTP::Persistent adapter. - class NetHttpPersistent < NetHttp - dependency 'net/http/persistent' - - private - - def net_http_connection(env) - @cached_connection ||= - if Net::HTTP::Persistent.instance_method(:initialize) - .parameters.first == %i[key name] - options = { name: 'Faraday' } - if @connection_options.key?(:pool_size) - options[:pool_size] = @connection_options[:pool_size] - end - Net::HTTP::Persistent.new(**options) - else - Net::HTTP::Persistent.new('Faraday') - end - - proxy_uri = proxy_uri(env) - if @cached_connection.proxy_uri != proxy_uri - @cached_connection.proxy = proxy_uri - end - @cached_connection - end - - def proxy_uri(env) - proxy_uri = nil - if (proxy = env[:request][:proxy]) - proxy_uri = if proxy[:uri].is_a?(::URI::HTTP) - proxy[:uri].dup - else - ::URI.parse(proxy[:uri].to_s) - end - proxy_uri.user = proxy_uri.password = nil - # awful patch for net-http-persistent 2.8 - # not unescaping user/password - if proxy[:user] - (class << proxy_uri; self; end).class_eval do - define_method(:user) { proxy[:user] } - define_method(:password) { proxy[:password] } - end - end - end - proxy_uri - end - - def perform_request(http, env) - http.request env[:url], create_request(env) - rescue Errno::ETIMEDOUT => e - raise Faraday::TimeoutError, e - rescue Net::HTTP::Persistent::Error => e - raise Faraday::TimeoutError, e if e.message.include? 'Timeout' - - if e.message.include? 'connection refused' - raise Faraday::ConnectionFailed, e - end - - raise - end - - SSL_CONFIGURATIONS = { - certificate: :client_cert, - private_key: :client_key, - ca_file: :ca_file, - ssl_version: :version, - min_version: :min_version, - max_version: :max_version - }.freeze - - def configure_ssl(http, ssl) - return unless ssl - - http_set(http, :verify_mode, ssl_verify_mode(ssl)) - http_set(http, :cert_store, ssl_cert_store(ssl)) - - SSL_CONFIGURATIONS - .select { |_, key| ssl[key] } - .each { |target, key| http_set(http, target, ssl[key]) } - end - - def http_set(http, attr, value) - http.send("#{attr}=", value) if http.send(attr) != value - end - end - end -end diff --git a/lib/faraday/adapter/patron.rb b/lib/faraday/adapter/patron.rb deleted file mode 100644 index c6e2935fb..000000000 --- a/lib/faraday/adapter/patron.rb +++ /dev/null @@ -1,132 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Adapter - # Patron adapter. - class Patron < Faraday::Adapter - dependency 'patron' - - def build_connection(env) - session = ::Patron::Session.new - @config_block&.call(session) - if (env[:url].scheme == 'https') && env[:ssl] - configure_ssl(session, env[:ssl]) - end - - if (req = env[:request]) - configure_timeouts(session, req) - configure_proxy(session, req[:proxy]) - end - - session - end - - def call(env) - super - # TODO: support streaming requests - env[:body] = env[:body].read if env[:body].respond_to? :read - - response = connection(env) do |session| - begin - data = env[:body] ? env[:body].to_s : nil - session.request(env[:method], env[:url].to_s, - env[:request_headers], data: data) - rescue Errno::ECONNREFUSED, ::Patron::ConnectionFailed - raise Faraday::ConnectionFailed, $ERROR_INFO - end - end - - if (req = env[:request]).stream_response? - warn "Streaming downloads for #{self.class.name} " \ - 'are not yet implemented.' - req.on_data.call(response.body, response.body.bytesize) - end - # Remove the "HTTP/1.1 200", leaving just the reason phrase - reason_phrase = response.status_line.gsub(/^.* \d{3} /, '') - - save_response(env, response.status, response.body, - response.headers, reason_phrase) - - @app.call env - rescue ::Patron::TimeoutError => e - if connection_timed_out_message?(e.message) - raise Faraday::ConnectionFailed, e - end - - raise Faraday::TimeoutError, e - rescue ::Patron::Error => e - if e.message.include?('code 407') - raise Faraday::ConnectionFailed, - %(407 "Proxy Authentication Required ") - end - - raise Faraday::ConnectionFailed, e - end - - if loaded? && defined?(::Patron::Request::VALID_ACTIONS) - # HAX: helps but doesn't work completely - # https://github.com/toland/patron/issues/34 - ::Patron::Request::VALID_ACTIONS.tap do |actions| - if actions[0].is_a?(Symbol) - actions << :patch unless actions.include? :patch - actions << :options unless actions.include? :options - else - # Patron 0.4.20 and up - actions << 'PATCH' unless actions.include? 'PATCH' - actions << 'OPTIONS' unless actions.include? 'OPTIONS' - end - end - end - - def configure_ssl(session, ssl) - if ssl.fetch(:verify, true) - session.cacert = ssl[:ca_file] - else - session.insecure = true - end - end - - def configure_timeouts(session, req) - return unless req - - if (sec = request_timeout(:read, req)) - session.timeout = sec - end - - return unless (sec = request_timeout(:open, req)) - - session.connect_timeout = sec - end - - def configure_proxy(session, proxy) - return unless proxy - - proxy_uri = proxy[:uri].dup - proxy_uri.user = proxy[:user] && - Utils.escape(proxy[:user]).gsub('+', '%20') - proxy_uri.password = proxy[:password] && - Utils.escape(proxy[:password]).gsub('+', '%20') - session.proxy = proxy_uri.to_s - end - - private - - CURL_TIMEOUT_MESSAGES = [ - 'Connection time-out', - 'Connection timed out', - 'Timed out before name resolve', - 'server connect has timed out', - 'Resolving timed out', - 'name lookup timed out', - 'timed out before SSL', - 'connect() timed out' - ].freeze - - def connection_timed_out_message?(message) - CURL_TIMEOUT_MESSAGES.any? do |curl_message| - message.include?(curl_message) - end - end - end - end -end diff --git a/lib/faraday/adapter/rack.rb b/lib/faraday/adapter/rack.rb deleted file mode 100644 index 6a237ee13..000000000 --- a/lib/faraday/adapter/rack.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Adapter - # Sends requests to a Rack app. - # - # @example - # - # class MyRackApp - # def call(env) - # [200, {'Content-Type' => 'text/html'}, ["hello world"]] - # end - # end - # - # Faraday.new do |conn| - # conn.adapter :rack, MyRackApp.new - # end - class Rack < Faraday::Adapter - dependency 'rack/test' - - # not prefixed with "HTTP_" - SPECIAL_HEADERS = %w[CONTENT_LENGTH CONTENT_TYPE].freeze - - def initialize(faraday_app, rack_app) - super(faraday_app) - mock_session = ::Rack::MockSession.new(rack_app) - @session = ::Rack::Test::Session.new(mock_session) - end - - def call(env) - super - rack_env = build_rack_env(env) - - env[:request_headers]&.each do |name, value| - name = name.upcase.tr('-', '_') - name = "HTTP_#{name}" unless SPECIAL_HEADERS.include? name - rack_env[name] = value - end - - timeout = request_timeout(:open, env[:request]) - timeout ||= request_timeout(:read, env[:request]) - response = if timeout - Timer.timeout(timeout, Faraday::TimeoutError) do - execute_request(env, rack_env) - end - else - execute_request(env, rack_env) - end - - if (req = env[:request]).stream_response? - warn "Streaming downloads for #{self.class.name} " \ - 'are not yet implemented.' - req.on_data.call(response.body, response.body.bytesize) - end - - save_response(env, response.status, response.body, response.headers) - @app.call env - end - - private - - def execute_request(env, rack_env) - @session.request(env[:url].to_s, rack_env) - end - - def build_rack_env(env) - { - method: env[:method], - input: env[:body].respond_to?(:read) ? env[:body].read : env[:body], - 'rack.url_scheme' => env[:url].scheme - } - end - end - end -end diff --git a/lib/faraday/adapter/test.rb b/lib/faraday/adapter/test.rb index 0230b853f..c637d139a 100644 --- a/lib/faraday/adapter/test.rb +++ b/lib/faraday/adapter/test.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'timeout' + module Faraday class Adapter # @example @@ -25,6 +27,18 @@ class Adapter # "showing item: #{meta[:match_data][1]}" # ] # end + # + # # Test the request body is the same as the stubbed body + # stub.post('/bar', 'name=YK&word=call') { [200, {}, ''] } + # + # # You can pass a proc as a stubbed body and check the request body in your way. + # # In this case, the proc should return true or false. + # stub.post('/foo', ->(request_body) do + # JSON.parse(request_body).slice('name') == { 'name' => 'YK' } }) { [200, {}, ''] + # end + # + # # You can set strict_mode to exactly match the stubbed requests. + # stub.strict_mode = true # end # end # @@ -39,6 +53,12 @@ class Adapter # # resp = test.get '/items/2' # resp.body # => 'showing item: 2' + # + # resp = test.post '/bar', 'name=YK&word=call' + # resp.status # => 200 + # + # resp = test.post '/foo', JSON.dump(name: 'YK', created_at: Time.now) + # resp.status # => 200 class Test < Faraday::Adapter attr_accessor :stubs @@ -47,10 +67,12 @@ class Stubs class NotFound < StandardError end - def initialize + def initialize(strict_mode: false) # { get: [Stub, Stub] } @stack = {} @consumed = {} + @strict_mode = strict_mode + @stubs_mutex = Monitor.new yield(self) if block_given? end @@ -58,18 +80,23 @@ def empty? @stack.empty? end - def match(request_method, host, path, headers, body) + # @param env [Faraday::Env] + def match(env) + request_method = env[:method] return false unless @stack.key?(request_method) stack = @stack[request_method] consumed = (@consumed[request_method] ||= []) - stub, meta = matches?(stack, host, path, headers, body) - if stub - consumed << stack.delete(stub) - return stub, meta + @stubs_mutex.synchronize do + stub, meta = matches?(stack, env) + if stub + removed = stack.delete(stub) + consumed << removed unless removed.nil? + return stub, meta + end end - matches?(consumed, host, path, headers, body) + matches?(consumed, env) end def get(path, headers = {}, &block) @@ -115,6 +142,17 @@ def verify_stubbed_calls raise failed_stubs.join(' ') unless failed_stubs.empty? end + # Set strict_mode. If the value is true, this adapter tries to find matched requests strictly, + # which means that all of a path, parameters, and headers must be the same as an actual request. + def strict_mode=(value) + @strict_mode = value + @stack.each_value do |stubs| + stubs.each do |stub| + stub.strict_mode = value + end + end + end + protected def new_stub(request_method, path, headers = {}, body = nil, &block) @@ -127,14 +165,18 @@ def new_stub(request_method, path, headers = {}, body = nil, &block) Faraday::Utils.URI(path).host ] end + path, query = normalized_path.respond_to?(:split) ? normalized_path.split('?') : normalized_path + headers = Utils::Headers.new(headers) - stub = Stub.new(host, normalized_path, headers, body, block) + stub = Stub.new(host, path, query, headers, body, @strict_mode, block) (@stack[request_method] ||= []) << stub end - def matches?(stack, host, path, headers, body) + # @param stack [Hash] + # @param env [Faraday::Env] + def matches?(stack, env) stack.each do |stub| - match_result, meta = stub.matches?(host, path, headers, body) + match_result, meta = stub.matches?(env) return stub, meta if match_result end nil @@ -142,36 +184,21 @@ def matches?(stack, host, path, headers, body) end # Stub request - # rubocop:disable Style/StructInheritance - class Stub < Struct.new(:host, :path, :params, :headers, :body, :block) - # rubocop:enable Style/StructInheritance - def initialize(host, full, headers, body, block) - path, query = full.respond_to?(:split) ? full.split('?') : full - params = - if query - Faraday::Utils.parse_nested_query(query) - else - {} - end + Stub = Struct.new(:host, :path, :query, :headers, :body, :strict_mode, :block) do + # @param env [Faraday::Env] + def matches?(env) + request_host = env[:url].host + request_path = Faraday::Utils.normalize_path(env[:url].path) + request_headers = env.request_headers + request_body = env[:body] - super(host, path, params, headers, body, block) - end - - def matches?(request_host, request_uri, request_headers, request_body) - request_path, request_query = request_uri.split('?') - request_params = - if request_query - Faraday::Utils.parse_nested_query(request_query) - else - {} - end # meta is a hash used as carrier # that will be yielded to consumer block meta = {} [(host.nil? || host == request_host) && path_match?(request_path, meta) && - params_match?(request_params) && - (body.to_s.size.zero? || request_body == body) && + params_match?(env) && + body_match?(request_body) && headers_match?(request_headers), meta] end @@ -183,18 +210,46 @@ def path_match?(request_path, meta) end end - def params_match?(request_params) + # @param env [Faraday::Env] + def params_match?(env) + request_params = env[:params] + params = env.params_encoder.decode(query) || {} + + if strict_mode + return Set.new(params) == Set.new(request_params) + end + params.keys.all? do |key| request_params[key] == params[key] end end def headers_match?(request_headers) + if strict_mode + headers_with_user_agent = headers.dup.tap do |hs| + # NOTE: Set User-Agent in case it's not set when creating Stubs. + # Users would not want to set Faraday's User-Agent explicitly. + hs[:user_agent] ||= Connection::USER_AGENT + end + return Set.new(headers_with_user_agent) == Set.new(request_headers) + end + headers.keys.all? do |key| request_headers[key] == headers[key] end end + def body_match?(request_body) + return true if body.to_s.empty? + + case body + when Proc + body.call(request_body) + else + request_body == body + end + end + def to_s "#{path} #{body}" end @@ -210,37 +265,47 @@ def configure yield(stubs) end + # @param env [Faraday::Env] def call(env) super - host = env[:url].host - normalized_path = Faraday::Utils.normalize_path(env[:url]) - params_encoder = env.request.params_encoder || - Faraday::Utils.default_params_encoder - stub, meta = stubs.match(env[:method], host, normalized_path, - env.request_headers, env[:body]) + env.request.params_encoder ||= Faraday::Utils.default_params_encoder + env[:params] = env.params_encoder.decode(env[:url].query) || {} + stub, meta = stubs.match(env) unless stub - raise Stubs::NotFound, "no stubbed request for #{env[:method]} "\ - "#{normalized_path} #{env[:body]}" + raise Stubs::NotFound, "no stubbed request for #{env[:method]} " \ + "#{env[:url]} #{env[:body]} #{env[:headers]}" end - env[:params] = if (query = env[:url].query) - params_encoder.decode(query) - else - {} - end block_arity = stub.block.arity + params = if block_arity >= 0 + [env, meta].take(block_arity) + else + [env, meta] + end + + timeout = request_timeout(:open, env[:request]) + timeout ||= request_timeout(:read, env[:request]) + status, headers, body = - if block_arity >= 0 - stub.block.call(*[env, meta].take(block_arity)) + if timeout + ::Timeout.timeout(timeout, Faraday::TimeoutError) do + stub.block.call(*params) + end else - stub.block.call(env, meta) + stub.block.call(*params) end - save_response(env, status, body, headers) + + # We need to explicitly pass `reason_phrase = nil` here to avoid keyword args conflicts. + # See https://github.com/lostisland/faraday/issues/1444 + # TODO: remove `nil` explicit reason_phrase once Ruby 3.0 becomes minimum req. version + save_response(env, status, body, headers, nil) @app.call(env) end end end end + +Faraday::Adapter.register_middleware(test: Faraday::Adapter::Test) diff --git a/lib/faraday/adapter/typhoeus.rb b/lib/faraday/adapter/typhoeus.rb deleted file mode 100644 index 306c0e45d..000000000 --- a/lib/faraday/adapter/typhoeus.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Adapter - # Typhoeus adapter. This class is just a stub, the real adapter is in - # https://github.com/philsturgeon/typhoeus/blob/master/lib/typhoeus/adapters/faraday.rb - class Typhoeus < Faraday::Adapter - # Needs to define this method in order to support Typhoeus <= 1.3.0 - def call; end - - dependency 'typhoeus' - dependency 'typhoeus/adapters/faraday' - end - end -end diff --git a/lib/faraday/adapter_registry.rb b/lib/faraday/adapter_registry.rb index 7b08c57ad..1cd1e7e17 100644 --- a/lib/faraday/adapter_registry.rb +++ b/lib/faraday/adapter_registry.rb @@ -12,7 +12,9 @@ def initialize end def get(name) - klass = @constants[name] + klass = @lock.synchronize do + @constants[name] + end return klass if klass Object.const_get(name).tap { |c| set(c, name) } diff --git a/lib/faraday/autoload.rb b/lib/faraday/autoload.rb deleted file mode 100644 index 20863bebc..000000000 --- a/lib/faraday/autoload.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -module Faraday - # Adds the ability for other modules to manage autoloadable - # constants. - # - # @api private - module AutoloadHelper - # Registers the constants to be auto loaded. - # - # @param prefix [String] The require prefix. If the path is inside Faraday, - # then it will be prefixed with the root path of this loaded - # Faraday version. - # @param options [{ Symbol => String }] library names. - # - # @example - # - # Faraday.autoload_all 'faraday/foo', - # Bar: 'bar' - # - # # requires faraday/foo/bar to load Faraday::Bar. - # Faraday::Bar - # - # @return [void] - def autoload_all(prefix, options) - if prefix =~ %r{^faraday(/|$)}i - prefix = File.join(Faraday.root_path, prefix) - end - - options.each do |const_name, path| - autoload const_name, File.join(prefix, path) - end - end - - # Loads each autoloaded constant. If thread safety is a concern, - # wrap this in a Mutex. - # - # @return [void] - def load_autoloaded_constants - constants.each do |const| - const_get(const) if autoload?(const) - end - end - - # Filters the module's contents with those that have been already - # autoloaded. - # - # @return [Array] - def all_loaded_constants - constants - .map { |c| const_get(c) } - .select { |a| a.respond_to?(:loaded?) && a.loaded? } - end - end - - # Adapter is the base class for all Faraday adapters. - # @see lib/faraday/adapter.rb Original class location - class Adapter - extend AutoloadHelper - autoload_all 'faraday/adapter', - NetHttp: 'net_http', - NetHttpPersistent: 'net_http_persistent', - EMSynchrony: 'em_synchrony', - EMHttp: 'em_http', - Typhoeus: 'typhoeus', - Patron: 'patron', - Excon: 'excon', - Test: 'test', - Rack: 'rack', - HTTPClient: 'httpclient' - end - - # Request represents a single HTTP request for a Faraday adapter to make. - # @see lib/faraday/request.rb Original class location - class Request - extend AutoloadHelper - autoload_all 'faraday/request', - UrlEncoded: 'url_encoded', - Multipart: 'multipart', - Retry: 'retry', - Authorization: 'authorization', - BasicAuthentication: 'basic_authentication', - TokenAuthentication: 'token_authentication', - Instrumentation: 'instrumentation' - end - - # Response represents the returned value of a sent Faraday request. - # @see lib/faraday/response.rb Original class location - class Response - extend AutoloadHelper - autoload_all 'faraday/response', - RaiseError: 'raise_error', - Logger: 'logger' - end -end diff --git a/lib/faraday/connection.rb b/lib/faraday/connection.rb index e90a6a54d..e979e8282 100644 --- a/lib/faraday/connection.rb +++ b/lib/faraday/connection.rb @@ -6,15 +6,16 @@ module Faraday # # @example # - # conn = Faraday::Connection.new 'http://sushi.com' + # conn = Faraday::Connection.new 'http://httpbingo.org' # - # # GET http://sushi.com/nigiri + # # GET http://httpbingo.org/nigiri # conn.get 'nigiri' # # => # # class Connection # A Set of allowed HTTP verbs. METHODS = Set.new %i[get post put delete head patch options trace] + USER_AGENT = "Faraday v#{VERSION}".freeze # @return [Hash] URI query unencoded key/value pairs. attr_reader :params @@ -26,7 +27,7 @@ class Connection # Connection. This includes a default host name, scheme, port, and path. attr_reader :url_prefix - # @return [Faraday::Builder] Builder for this Connection. + # @return [Faraday::RackBuilder] Builder for this Connection. attr_reader :builder # @return [Hash] SSL options. @@ -63,7 +64,7 @@ def initialize(url = nil, options = nil) options = ConnectionOptions.from(options) if url.is_a?(Hash) || url.is_a?(ConnectionOptions) - options = options.merge(url) + options = Utils.deep_merge(options, url) url = options.url end @@ -73,6 +74,7 @@ def initialize(url = nil, options = nil) @options = options.request @ssl = options.ssl @default_parallel_manager = options.parallel_manager + @manual_proxy = nil @builder = options.builder || begin # pass an empty block to Builder so it doesn't assume default middleware @@ -88,7 +90,7 @@ def initialize(url = nil, options = nil) yield(self) if block_given? - @headers[:user_agent] ||= "Faraday v#{VERSION}" + @headers[:user_agent] ||= USER_AGENT end def initialize_proxy(url, options) @@ -115,7 +117,7 @@ def headers=(hash) extend Forwardable - def_delegators :builder, :build, :use, :request, :response, :adapter, :app + def_delegators :builder, :use, :request, :response, :adapter, :app # Closes the underlying resources and/or connections. In the case of # persistent connections, this closes all currently open connections @@ -128,10 +130,10 @@ def close # Makes a GET HTTP request without a body. # @!scope class # - # @param url [String] The optional String base URL to use as a prefix for + # @param url [String, URI, nil] The optional String base URL to use as a prefix for # all requests. Can also be the options Hash. - # @param params [Hash] Hash of URI query unencoded key/value pairs. - # @param headers [Hash] unencoded HTTP header key/value pairs. + # @param params [Hash, nil] Hash of URI query unencoded key/value pairs. + # @param headers [Hash, nil] unencoded HTTP header key/value pairs. # # @example # conn.get '/items', { page: 1 }, :accept => 'application/json' @@ -150,10 +152,10 @@ def close # Makes a HEAD HTTP request without a body. # @!scope class # - # @param url [String] The optional String base URL to use as a prefix for + # @param url [String, URI, nil] The optional String base URL to use as a prefix for # all requests. Can also be the options Hash. - # @param params [Hash] Hash of URI query unencoded key/value pairs. - # @param headers [Hash] unencoded HTTP header key/value pairs. + # @param params [Hash, nil] Hash of URI query unencoded key/value pairs. + # @param headers [Hash, nil] unencoded HTTP header key/value pairs. # # @example # conn.head '/items/1' @@ -165,10 +167,10 @@ def close # Makes a DELETE HTTP request without a body. # @!scope class # - # @param url [String] The optional String base URL to use as a prefix for + # @param url [String, URI, nil] The optional String base URL to use as a prefix for # all requests. Can also be the options Hash. - # @param params [Hash] Hash of URI query unencoded key/value pairs. - # @param headers [Hash] unencoded HTTP header key/value pairs. + # @param params [Hash, nil] Hash of URI query unencoded key/value pairs. + # @param headers [Hash, nil] unencoded HTTP header key/value pairs. # # @example # conn.delete '/items/1' @@ -180,10 +182,10 @@ def close # Makes a TRACE HTTP request without a body. # @!scope class # - # @param url [String] The optional String base URL to use as a prefix for + # @param url [String, URI, nil] The optional String base URL to use as a prefix for # all requests. Can also be the options Hash. - # @param params [Hash] Hash of URI query unencoded key/value pairs. - # @param headers [Hash] unencoded HTTP header key/value pairs. + # @param params [Hash, nil] Hash of URI query unencoded key/value pairs. + # @param headers [Hash, nil] unencoded HTTP header key/value pairs. # # @example # conn.connect '/items/1' @@ -208,9 +210,9 @@ def #{method}(url = nil, params = nil, headers = nil) # # @overload options(url, params = nil, headers = nil) # Makes an OPTIONS HTTP request to the given URL. - # @param url [String] String base URL to sue as a prefix for all requests. - # @param params [Hash] Hash of URI query unencoded key/value pairs. - # @param headers [Hash] unencoded HTTP header key/value pairs. + # @param url [String, URI, nil] String base URL to sue as a prefix for all requests. + # @param params [Hash, nil] Hash of URI query unencoded key/value pairs. + # @param headers [Hash, nil] unencoded HTTP header key/value pairs. # # @example # conn.options '/items/1' @@ -218,7 +220,7 @@ def #{method}(url = nil, params = nil, headers = nil) # @yield [Faraday::Request] for further request customizations # @return [Faraday::Response] def options(*args) - return @options if args.size.zero? + return @options if args.empty? url, params, headers = *args run_request(:options, url, nil, headers) do |request| @@ -231,10 +233,10 @@ def options(*args) # Makes a POST HTTP request with a body. # @!scope class # - # @param url [String] The optional String base URL to use as a prefix for + # @param url [String, URI, nil] The optional String base URL to use as a prefix for # all requests. Can also be the options Hash. - # @param body [String] body for the request. - # @param headers [Hash] unencoded HTTP header key/value pairs. + # @param body [String, nil] body for the request. + # @param headers [Hash, nil] unencoded HTTP header key/value pairs. # # @example # conn.post '/items', data, content_type: 'application/json' @@ -253,20 +255,19 @@ def options(*args) # Makes a PUT HTTP request with a body. # @!scope class # - # @param url [String] The optional String base URL to use as a prefix for + # @param url [String, URI, nil] The optional String base URL to use as a prefix for # all requests. Can also be the options Hash. - # @param body [String] body for the request. - # @param headers [Hash] unencoded HTTP header key/value pairs. + # @param body [String, nil] body for the request. + # @param headers [Hash, nil] unencoded HTTP header key/value pairs. # # @example - # # TODO: Make it a PUT example - # conn.post '/items', data, content_type: 'application/json' + # conn.put '/products/123', data, content_type: 'application/json' # - # # Simple ElasticSearch indexing sample. - # conn.post '/twitter/tweet' do |req| - # req.headers[:content_type] = 'application/json' - # req.params[:routing] = 'kimchy' - # req.body = JSON.generate(user: 'kimchy', ...) + # # Star a gist. + # conn.put 'https://api.github.com/gists/GIST_ID/star' do |req| + # req.headers['Accept'] = 'application/vnd.github+json' + # req.headers['Authorization'] = 'Bearer ' + # req.headers['X-GitHub-Api-Version'] = '2022-11-28' # end # # @yield [Faraday::Request] for further request customizations @@ -281,62 +282,6 @@ def #{method}(url = nil, body = nil, headers = nil, &block) RUBY end - # Sets up the Authorization header with these credentials, encoded - # with base64. - # - # @param login [String] The authentication login. - # @param pass [String] The authentication password. - # - # @example - # - # conn.basic_auth 'Aladdin', 'open sesame' - # conn.headers['Authorization'] - # # => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" - # - # @return [void] - def basic_auth(login, pass) - set_authorization_header(:basic_auth, login, pass) - end - - # Sets up the Authorization header with the given token. - # - # @param token [String] - # @param options [Hash] extra token options. - # - # @example - # - # conn.token_auth 'abcdef', foo: 'bar' - # conn.headers['Authorization'] - # # => "Token token=\"abcdef\", - # foo=\"bar\"" - # - # @return [void] - def token_auth(token, options = nil) - set_authorization_header(:token_auth, token, options) - end - - # Sets up a custom Authorization header. - # - # @param type [String] authorization type - # @param token [String, Hash] token. A String value is taken literally, and - # a Hash is encoded into comma-separated key/value pairs. - # - # @example - # - # conn.authorization :Bearer, 'mF_9.B5f-4.1JqM' - # conn.headers['Authorization'] - # # => "Bearer mF_9.B5f-4.1JqM" - # - # conn.authorization :Token, token: 'abcdef', foo: 'bar' - # conn.headers['Authorization'] - # # => "Token token=\"abcdef\", - # foo=\"bar\"" - # - # @return [void] - def authorization(type, token) - set_authorization_header(:authorization, type, token) - end - # Check if the adapter is parallel-capable. # # @yield if the adapter isn't parallel-capable, or if no adapter is set yet. @@ -403,11 +348,11 @@ def proxy=(new_value) # @example # # conn = Faraday::Connection.new { ... } - # conn.url_prefix = "https://sushi.com/api" + # conn.url_prefix = "https://httpbingo.org/api" # conn.scheme # => https # conn.path_prefix # => "/api" # - # conn.get("nigiri?page=2") # accesses https://sushi.com/api/nigiri + # conn.get("nigiri?page=2") # accesses https://httpbingo.org/api/nigiri def url_prefix=(url, encoder = nil) uri = @url_prefix = Utils.URI(url) self.path_prefix = uri.path @@ -416,9 +361,16 @@ def url_prefix=(url, encoder = nil) uri.query = nil with_uri_credentials(uri) do |user, password| - basic_auth user, password + set_basic_auth(user, password) uri.user = uri.password = nil end + + @proxy = proxy_from_env(url) unless @manual_proxy + end + + def set_basic_auth(user, password) + header = Faraday::Utils.basic_header_from(user, password) + headers[Faraday::Request::Authorization::KEY] = header end # Sets the path prefix and ensures that it always has a leading @@ -429,7 +381,7 @@ def url_prefix=(url, encoder = nil) # @return [String] the new path prefix def path_prefix=(value) url_prefix.path = if value - value = '/' + value unless value[0, 1] == '/' + value = "/#{value}" unless value[0, 1] == '/' value end end @@ -437,20 +389,20 @@ def path_prefix=(value) # Takes a relative url for a request and combines it with the defaults # set on the connection instance. # - # @param url [String] + # @param url [String, URI, nil] # @param extra_params [Hash] # # @example # conn = Faraday::Connection.new { ... } - # conn.url_prefix = "https://sushi.com/api?token=abc" + # conn.url_prefix = "https://httpbingo.org/api?token=abc" # conn.scheme # => https # conn.path_prefix # => "/api" # # conn.build_url("nigiri?page=2") - # # => https://sushi.com/api/nigiri?token=abc&page=2 + # # => https://httpbingo.org/api/nigiri?token=abc&page=2 # # conn.build_url("nigiri", page: 2) - # # => https://sushi.com/api/nigiri?token=abc&page=2 + # # => https://httpbingo.org/api/nigiri?token=abc&page=2 # def build_url(url = nil, extra_params = nil) uri = build_exclusive_url(url) @@ -470,10 +422,10 @@ def build_url(url = nil, extra_params = nil) # Builds and runs the Faraday::Request. # # @param method [Symbol] HTTP method. - # @param url [String, URI] String or URI to access. - # @param body [Object] The request body that will eventually be converted to - # a string. - # @param headers [Hash] unencoded HTTP header key/value pairs. + # @param url [String, URI, nil] String or URI to access. + # @param body [String, Hash, Array, nil] The request body that will eventually be converted to + # a string; middlewares can be used to support more complex types. + # @param headers [Hash, nil] unencoded HTTP header key/value pairs. # # @return [Faraday::Response] def run_request(method, url, body, headers) @@ -509,7 +461,7 @@ def build_request(method) # Build an absolute URL based on url_prefix. # - # @param url [String, URI] + # @param url [String, URI, nil] # @param params [Faraday::Utils::ParamsHash] A Faraday::Utils::ParamsHash to # replace the query values # of the resulting url (default: nil). @@ -517,18 +469,17 @@ def build_request(method) # @return [URI] def build_exclusive_url(url = nil, params = nil, params_encoder = nil) url = nil if url.respond_to?(:empty?) && url.empty? - base = url_prefix - if url && base.path && base.path !~ %r{/$} - base = base.dup - base.path = base.path + '/' # ensure trailing slash + base = url_prefix.dup + if url && !base.path.end_with?('/') + base.path = "#{base.path}/" # ensure trailing slash end + # Ensure relative url will be parsed correctly (such as `service:search` ) + url = "./#{url}" if url.respond_to?(:start_with?) && !url.start_with?('http://', 'https://', '/', './', '../') uri = url ? base + url : base if params uri.query = params.to_query(params_encoder || options.params_encoder) end - # rubocop:disable Style/SafeNavigation uri.query = nil if uri.query && uri.query.empty? - # rubocop:enable Style/SafeNavigation uri end @@ -560,40 +511,31 @@ def with_uri_credentials(uri) yield(Utils.unescape(uri.user), Utils.unescape(uri.password)) end - def set_authorization_header(header_type, *args) - header = Faraday::Request - .lookup_middleware(header_type) - .header(*args) - - headers[Faraday::Request::Authorization::KEY] = header - end - def proxy_from_env(url) return if Faraday.ignore_env_proxy uri = nil - if URI.parse('').respond_to?(:find_proxy) - case url - when String - uri = Utils.URI(url) - uri = URI.parse("#{uri.scheme}://#{uri.hostname}").find_proxy - when URI - uri = url.find_proxy - when nil - uri = find_default_proxy - end - else - warn 'no_proxy is unsupported' if ENV['no_proxy'] || ENV['NO_PROXY'] + case url + when String + uri = Utils.URI(url) + uri = if uri.host.nil? + find_default_proxy + else + URI.parse("#{uri.scheme}://#{uri.host}").find_proxy + end + when URI + uri = url.find_proxy + when nil uri = find_default_proxy end ProxyOptions.from(uri) if uri end def find_default_proxy - uri = ENV['http_proxy'] + uri = ENV.fetch('http_proxy', nil) return unless uri && !uri.empty? - uri = 'http://' + uri if uri !~ /^http/i + uri = "http://#{uri}" unless uri.match?(/^http/i) uri end @@ -608,7 +550,7 @@ def proxy_for_request(url) end def support_parallel?(adapter) - adapter&.respond_to?(:supports_parallel?) && adapter&.supports_parallel? + adapter.respond_to?(:supports_parallel?) && adapter&.supports_parallel? end end end diff --git a/lib/faraday/dependency_loader.rb b/lib/faraday/dependency_loader.rb deleted file mode 100644 index 57345646d..000000000 --- a/lib/faraday/dependency_loader.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module Faraday - # DependencyLoader helps Faraday adapters and middleware load dependencies. - module DependencyLoader - attr_reader :load_error - - # Executes a block which should try to require and reference dependent - # libraries - def dependency(lib = nil) - lib ? require(lib) : yield - rescue LoadError, NameError => e - self.load_error = e - end - - def new(*) - unless loaded? - raise "missing dependency for #{self}: #{load_error.message}" - end - - super - end - - def loaded? - load_error.nil? - end - - def inherited(subclass) - super - subclass.send(:load_error=, load_error) - end - - private - - attr_writer :load_error - end -end diff --git a/lib/faraday/encoders/flat_params_encoder.rb b/lib/faraday/encoders/flat_params_encoder.rb index 8a4eae358..bc10c8b9a 100644 --- a/lib/faraday/encoders/flat_params_encoder.rb +++ b/lib/faraday/encoders/flat_params_encoder.rb @@ -33,9 +33,9 @@ def self.encode(params) key = key.to_s if key.is_a?(Symbol) [key, value] end - # Useful default for OAuth and caching. + # Only to be used for non-Array inputs. Arrays should preserve order. - params.sort! + params.sort! if @sort_params end # The params have form [['key1', 'value1'], ['key2', 'value2']]. @@ -94,5 +94,12 @@ def self.decode(query) end end end + + class << self + attr_accessor :sort_params + end + + # Useful default for OAuth and caching. + @sort_params = true end end diff --git a/lib/faraday/encoders/nested_params_encoder.rb b/lib/faraday/encoders/nested_params_encoder.rb index d726a535a..3ca3e73e7 100644 --- a/lib/faraday/encoders/nested_params_encoder.rb +++ b/lib/faraday/encoders/nested_params_encoder.rb @@ -21,9 +21,9 @@ def encode(params) key = key.to_s if key.is_a?(Symbol) [key, value] end - # Useful default for OAuth and caching. + # Only to be used for non-Array inputs. Arrays should preserve order. - params.sort! + params.sort! if @sort_params end # The params have form [['key1', 'value1'], ['key2', 'value2']]. @@ -62,11 +62,17 @@ def encode_hash(parent, value) end def encode_array(parent, value) - new_parent = "#{parent}%5B%5D" - return new_parent if value.empty? + return "#{parent}%5B%5D" if value.empty? buffer = +'' - value.each { |val| buffer << "#{encode_pair(new_parent, val)}&" } + value.each_with_index do |val, index| + new_parent = if @array_indices + "#{parent}%5B#{index}%5D" + else + "#{parent}%5B%5D" + end + buffer << "#{encode_pair(new_parent, val)}&" + end buffer.chop end end @@ -96,13 +102,13 @@ def decode(query) protected - SUBKEYS_REGEX = /[^\[\]]+(?:\]?\[\])?/.freeze + SUBKEYS_REGEX = /[^\[\]]+(?:\]?\[\])?/ def decode_pair(key, value, context) subkeys = key.scan(SUBKEYS_REGEX) subkeys.each_with_index do |subkey, i| is_array = subkey =~ /[\[\]]+\Z/ - subkey = $` if is_array + subkey = Regexp.last_match.pre_match if is_array last_subkey = i == subkeys.length - 1 context = prepare_context(context, subkey, is_array, last_subkey) @@ -124,7 +130,7 @@ def new_context(subkey, is_array, context) value_type = is_array ? Array : Hash if context[subkey] && !context[subkey].is_a?(value_type) raise TypeError, "expected #{value_type.name} " \ - "(got #{context[subkey].class.name}) for param `#{subkey}'" + "(got #{context[subkey].class.name}) for param `#{subkey}'" end context[subkey] ||= value_type.new @@ -161,10 +167,16 @@ def dehash(hash, depth) # for your requests. module NestedParamsEncoder class << self + attr_accessor :sort_params, :array_indices + extend Forwardable def_delegators :'Faraday::Utils', :escape, :unescape end + # Useful default for OAuth and caching. + @sort_params = true + @array_indices = false + extend EncodeMethods extend DecodeMethods end diff --git a/lib/faraday/error.rb b/lib/faraday/error.rb index 8e018bfe0..8cf98977a 100644 --- a/lib/faraday/error.rb +++ b/lib/faraday/error.rb @@ -6,7 +6,7 @@ module Faraday class Error < StandardError attr_reader :response, :wrapped_exception - def initialize(exc, response = nil) + def initialize(exc = nil, response = nil) @wrapped_exception = nil unless defined?(@wrapped_exception) @response = nil unless defined?(@response) super(exc_msg_and_response!(exc, response)) @@ -28,6 +28,24 @@ def inspect %(#<#{self.class}#{inner}>) end + def response_status + return unless @response + + @response.is_a?(Faraday::Response) ? @response.status : @response[:status] + end + + def response_headers + return unless @response + + @response.is_a?(Faraday::Response) ? @response.headers : @response[:headers] + end + + def response_body + return unless @response + + @response.is_a?(Faraday::Response) ? @response.body : @response[:body] + end + protected # Pulls out potential parent exception and response hash, storing them in @@ -38,6 +56,15 @@ def inspect # :headers - String key/value hash of HTTP response header # values. # :body - Optional string HTTP response body. + # :request - Hash + # :method - Symbol with the request HTTP method. + # :url - URI object with the url requested. + # :url_path - String with the url path requested. + # :params - String key/value hash of query params + # present in the request. + # :headers - String key/value hash of HTTP request + # header values. + # :body - String HTTP request body. # # If a subclass has to call this, then it should pass a string message # to `super`. See NilStatusError. @@ -85,6 +112,10 @@ class ResourceNotFound < ClientError class ProxyAuthError < ClientError end + # Raised by Faraday::Response::RaiseError in case of a 408 response. + class RequestTimeoutError < ClientError + end + # Raised by Faraday::Response::RaiseError in case of a 409 response. class ConflictError < ClientError end @@ -93,6 +124,10 @@ class ConflictError < ClientError class UnprocessableEntityError < ClientError end + # Raised by Faraday::Response::RaiseError in case of a 429 response. + class TooManyRequestsError < ClientError + end + # Faraday server error class. Represents 5xx status responses. class ServerError < Error end @@ -120,13 +155,11 @@ class ConnectionFailed < Error class SSLError < Error end - # Raised by FaradayMiddleware::ResponseMiddleware + # Raised by middlewares that parse the response, like the JSON response middleware. class ParsingError < Error end - # Exception used to control the Retry middleware. - # - # @see Faraday::Request::Retry - class RetriableResponse < Error + # Raised by Faraday::Middleware and subclasses when invalid default_options are used + class InitializationError < Error end end diff --git a/lib/faraday/file_part.rb b/lib/faraday/file_part.rb deleted file mode 100644 index d7c2c270b..000000000 --- a/lib/faraday/file_part.rb +++ /dev/null @@ -1,128 +0,0 @@ -# frozen_string_literal: true - -require 'stringio' - -# multipart-post gem -require 'composite_io' -require 'parts' - -module Faraday - # Multipart value used to POST a binary data from a file or - # - # @example - # payload = { file: Faraday::FilePart.new("file_name.ext", "content/type") } - # http.post("/upload", payload) - # - - # @!method initialize(filename_or_io, content_type, filename = nil, opts = {}) - # - # @param filename_or_io [String, IO] Either a String filename to a local - # file or an open IO object. - # @param content_type [String] String content type of the file data. - # @param filename [String] Optional String filename, usually to add context - # to a given IO object. - # @param opts [Hash] Optional Hash of String key/value pairs to describethis - # this uploaded file. Expected Header keys include: - # * Content-Transfer-Encoding - Defaults to "binary" - # * Content-Disposition - Defaults to "form-data" - # * Content-Type - Defaults to the content_type argument. - # * Content-ID - Optional. - # - # @return [Faraday::FilePart] - # - # @!attribute [r] content_type - # The uploaded binary data's content type. - # - # @return [String] - # - # @!attribute [r] original_filename - # The base filename, taken either from the filename_or_io or filename - # arguments in #initialize. - # - # @return [String] - # - # @!attribute [r] opts - # Extra String key/value pairs to make up the header for this uploaded file. - # - # @return [Hash] - # - # @!attribute [r] io - # The open IO object for the uploaded file. - # - # @return [IO] - FilePart = ::UploadIO - - # Multipart value used to POST a file. - # - # @deprecated Use FilePart instead of this class. It behaves identically, with - # a matching name to ParamPart. - UploadIO = ::UploadIO - - Parts = ::Parts - - # Similar to, but not compatible with CompositeReadIO provided by the - # multipart-post gem. - # https://github.com/nicksieger/multipart-post/blob/master/lib/composite_io.rb - class CompositeReadIO - def initialize(*parts) - @parts = parts.flatten - @ios = @parts.map(&:to_io) - @index = 0 - end - - # @return [Integer] sum of the lengths of all the parts - def length - @parts.inject(0) { |sum, part| sum + part.length } - end - - # Rewind each of the IOs and reset the index to 0. - # - # @return [void] - def rewind - @ios.each(&:rewind) - @index = 0 - end - - # Read from IOs in order until `length` bytes have been received. - # - # @param length [Integer, nil] - # @param outbuf [String, nil] - def read(length = nil, outbuf = nil) - got_result = false - outbuf = outbuf ? (+outbuf).replace('') : +'' - - while (io = current_io) - if (result = io.read(length)) - got_result ||= !result.nil? - result.force_encoding('BINARY') if result.respond_to?(:force_encoding) - outbuf << result - length -= result.length if length - break if length&.zero? - end - advance_io - end - !got_result && length ? nil : outbuf - end - - # Close each of the IOs. - # - # @return [void] - def close - @ios.each(&:close) - end - - def ensure_open_and_readable - # Rubinius compatibility - end - - private - - def current_io - @ios[@index] - end - - def advance_io - @index += 1 - end - end -end diff --git a/lib/faraday/logging/formatter.rb b/lib/faraday/logging/formatter.rb index ba3e49765..633afaadf 100644 --- a/lib/faraday/logging/formatter.rb +++ b/lib/faraday/logging/formatter.rb @@ -1,41 +1,54 @@ # frozen_string_literal: true -require 'pp' +require 'pp' # This require is necessary for Hash#pretty_inspect to work, do not remove it, people rely on it. + module Faraday module Logging # Serves as an integration point to customize logging class Formatter extend Forwardable - DEFAULT_OPTIONS = { headers: true, bodies: false, + DEFAULT_OPTIONS = { headers: true, bodies: false, errors: false, log_level: :info }.freeze def initialize(logger:, options:) @logger = logger - @filter = [] @options = DEFAULT_OPTIONS.merge(options) + unless %i[debug info warn error fatal].include?(@options[:log_level]) + @options[:log_level] = :info + end + @filter = [] end def_delegators :@logger, :debug, :info, :warn, :error, :fatal def request(env) - request_log = proc do + public_send(log_level, 'request') do "#{env.method.upcase} #{apply_filters(env.url.to_s)}" end - public_send(log_level, 'request', &request_log) log_headers('request', env.request_headers) if log_headers?(:request) log_body('request', env[:body]) if env[:body] && log_body?(:request) end def response(env) - status = proc { "Status #{env.status}" } - public_send(log_level, 'response', &status) + public_send(log_level, 'response') { "Status #{env.status}" } log_headers('response', env.response_headers) if log_headers?(:response) log_body('response', env[:body]) if env[:body] && log_body?(:response) end + def exception(exc) + return unless log_errors? + + public_send(log_level, 'error') { exc.full_message } + + log_headers('error', exc.response_headers) if exc.respond_to?(:response_headers) && log_headers?(:error) + return unless exc.respond_to?(:response_body) && exc.response_body && log_body?(:error) + + log_body('error', exc.response_body) + end + def filter(filter_word, filter_replacement) @filter.push([filter_word, filter_replacement]) end @@ -43,6 +56,8 @@ def filter(filter_word, filter_replacement) private def dump_headers(headers) + return if headers.nil? + headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n") end @@ -76,6 +91,10 @@ def log_body?(type) end end + def log_errors? + @options[:errors] + end + def apply_filters(output) @filter.each do |pattern, replacement| output = output.to_s.gsub(pattern, replacement) @@ -84,21 +103,15 @@ def apply_filters(output) end def log_level - unless %i[debug info warn error fatal].include?(@options[:log_level]) - return :info - end - @options[:log_level] end def log_headers(type, headers) - headers_log = proc { apply_filters(dump_headers(headers)) } - public_send(log_level, type, &headers_log) + public_send(log_level, type) { apply_filters(dump_headers(headers)) } end def log_body(type, body) - body_log = proc { apply_filters(dump_body(body)) } - public_send(log_level, type, &body_log) + public_send(log_level, type) { apply_filters(dump_body(body)) } end end end diff --git a/lib/faraday/methods.rb b/lib/faraday/methods.rb new file mode 100644 index 000000000..53e390379 --- /dev/null +++ b/lib/faraday/methods.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Faraday + METHODS_WITH_QUERY = %w[get head delete trace].freeze + METHODS_WITH_BODY = %w[post put patch].freeze +end diff --git a/lib/faraday/middleware.rb b/lib/faraday/middleware.rb index 5bbe4a0e1..9532b7553 100644 --- a/lib/faraday/middleware.rb +++ b/lib/faraday/middleware.rb @@ -1,20 +1,74 @@ # frozen_string_literal: true +require 'monitor' + module Faraday # Middleware is the basic base class of any Faraday middleware. class Middleware extend MiddlewareRegistry - extend DependencyLoader - def initialize(app = nil) + attr_reader :app, :options + + DEFAULT_OPTIONS = {}.freeze + + def initialize(app = nil, options = {}) @app = app + @options = self.class.default_options.merge(options) + end + + class << self + # Faraday::Middleware::default_options= allows user to set default options at the Faraday::Middleware + # class level. + # + # @example Set the Faraday::Response::RaiseError option, `include_request` to `false` + # my_app/config/initializers/my_faraday_middleware.rb + # + # Faraday::Response::RaiseError.default_options = { include_request: false } + # + def default_options=(options = {}) + validate_default_options(options) + lock.synchronize do + @default_options = default_options.merge(options) + end + end + + # default_options attr_reader that initializes class instance variable + # with the values of any Faraday::Middleware defaults, and merges with + # subclass defaults + def default_options + @default_options ||= DEFAULT_OPTIONS.merge(self::DEFAULT_OPTIONS) + end + + private + + def lock + @lock ||= Monitor.new + end + + def validate_default_options(options) + invalid_keys = options.keys.reject { |opt| self::DEFAULT_OPTIONS.key?(opt) } + return unless invalid_keys.any? + + raise(Faraday::InitializationError, + "Invalid options provided. Keys not found in #{self}::DEFAULT_OPTIONS: #{invalid_keys.join(', ')}") + end + end + + def call(env) + on_request(env) if respond_to?(:on_request) + app.call(env).on_complete do |environment| + on_complete(environment) if respond_to?(:on_complete) + end + rescue StandardError => e + on_error(e) if respond_to?(:on_error) + raise end def close - if @app.respond_to?(:close) - @app.close + if app.respond_to?(:close) + app.close else - warn "#{@app} does not implement \#close!" + warn "#{app} does not implement \#close!" end end end diff --git a/lib/faraday/middleware_registry.rb b/lib/faraday/middleware_registry.rb index 021038fec..fc70e2b87 100644 --- a/lib/faraday/middleware_registry.rb +++ b/lib/faraday/middleware_registry.rb @@ -6,59 +6,26 @@ module Faraday # Adds the ability for other modules to register and lookup # middleware classes. module MiddlewareRegistry + def registered_middleware + @registered_middleware ||= {} + end + # Register middleware class(es) on the current module. # - # @param autoload_path [String] Middleware autoload path - # @param mapping [Hash{ - # Symbol => Module, - # Symbol => Array, - # }] Middleware mapping from a lookup symbol to a reference to the - # middleware. - # Classes can be expressed as: - # - a fully qualified constant - # - a Symbol - # - a Proc that will be lazily called to return the former - # - an array is given, its first element is the constant or symbol, - # and its second is a file to `require`. + # @param mappings [Hash] Middleware mappings from a lookup symbol to a middleware class. # @return [void] # # @example Lookup by a constant # # module Faraday - # class Whatever + # class Whatever < Middleware # # Middleware looked up by :foo returns Faraday::Whatever::Foo. - # register_middleware foo: Foo - # end - # end - # - # @example Lookup by a symbol - # - # module Faraday - # class Whatever - # # Middleware looked up by :bar returns - # # Faraday::Whatever.const_get(:Bar) - # register_middleware bar: :Bar - # end - # end - # - # @example Lookup by a symbol and string in an array - # - # module Faraday - # class Whatever - # # Middleware looked up by :baz requires 'baz' and returns - # # Faraday::Whatever.const_get(:Baz) - # register_middleware baz: [:Baz, 'baz'] + # register_middleware(foo: Whatever) # end # end - # - def register_middleware(autoload_path = nil, mapping = nil) - if mapping.nil? - mapping = autoload_path - autoload_path = nil - end + def register_middleware(**mappings) middleware_mutex do - @middleware_autoload_path = autoload_path if autoload_path - (@registered_middleware ||= {}).update(mapping) + registered_middleware.update(mappings) end end @@ -66,7 +33,7 @@ def register_middleware(autoload_path = nil, mapping = nil) # # @param key [Symbol] key for the registered middleware. def unregister_middleware(key) - @registered_middleware.delete(key) + registered_middleware.delete(key) end # Lookup middleware class with a registered Symbol shortcut. @@ -78,30 +45,27 @@ def unregister_middleware(key) # @example # # module Faraday - # class Whatever - # register_middleware foo: Foo + # class Whatever < Middleware + # register_middleware(foo: Whatever) # end # end # - # Faraday::Whatever.lookup_middleware(:foo) - # # => Faraday::Whatever::Foo - # + # Faraday::Middleware.lookup_middleware(:foo) + # # => Faraday::Whatever def lookup_middleware(key) load_middleware(key) || raise(Faraday::Error, "#{key.inspect} is not registered on #{self}") end + private + def middleware_mutex(&block) @middleware_mutex ||= Monitor.new @middleware_mutex.synchronize(&block) end - def fetch_middleware(key) - defined?(@registered_middleware) && @registered_middleware[key] - end - def load_middleware(key) - value = fetch_middleware(key) + value = registered_middleware[key] case value when Module value @@ -113,16 +77,6 @@ def load_middleware(key) middleware_mutex do @registered_middleware[key] = value.call end - when Array - middleware_mutex do - const, path = value - if (root = @middleware_autoload_path) - path = "#{root}/#{path}" - end - require(path) - @registered_middleware[key] = const - end - load_middleware(key) end end end diff --git a/lib/faraday/options.rb b/lib/faraday/options.rb index b5af43d1b..b3e0dea1a 100644 --- a/lib/faraday/options.rb +++ b/lib/faraday/options.rb @@ -30,7 +30,7 @@ def update(obj) new_value = value end - send("#{key}=", new_value) unless new_value.nil? + send(:"#{key}=", new_value) unless new_value.nil? end self end @@ -38,7 +38,7 @@ def update(obj) # Public def delete(key) value = send(key) - send("#{key}=", nil) + send(:"#{key}=", nil) value end @@ -57,7 +57,7 @@ def merge!(other) else other_value end - send("#{key}=", new_value) unless new_value.nil? + send(:"#{key}=", new_value) unless new_value.nil? end self end @@ -103,12 +103,10 @@ def empty? end # Public - def each_key - return to_enum(:each_key) unless block_given? + def each_key(&block) + return to_enum(:each_key) unless block - keys.each do |key| - yield(key) - end + keys.each(&block) end # Public @@ -119,12 +117,10 @@ def key?(key) alias has_key? key? # Public - def each_value - return to_enum(:each_value) unless block_given? + def each_value(&block) + return to_enum(:each_value) unless block - values.each do |value| - yield(value) - end + values.each(&block) end # Public @@ -172,12 +168,13 @@ def self.attribute_options end def self.memoized(key, &block) - unless block_given? + unless block raise ArgumentError, '#memoized must be called with a block' end memoized_attributes[key.to_sym] = block class_eval <<-RUBY, __FILE__, __LINE__ + 1 + remove_method(key) if method_defined?(key, false) def #{key}() self[:#{key}]; end RUBY end diff --git a/lib/faraday/options/connection_options.rb b/lib/faraday/options/connection_options.rb index 5a729406c..0698940b4 100644 --- a/lib/faraday/options/connection_options.rb +++ b/lib/faraday/options/connection_options.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true module Faraday - # ConnectionOptions contains the configurable properties for a Faraday - # connection object. - class ConnectionOptions < Options.new(:request, :proxy, :ssl, :builder, :url, - :parallel_manager, :params, :headers, - :builder_class) - + # @!parse + # # ConnectionOptions contains the configurable properties for a Faraday + # # connection object. + # class ConnectionOptions < Options; end + ConnectionOptions = Options.new(:request, :proxy, :ssl, :builder, :url, + :parallel_manager, :params, :headers, + :builder_class) do options request: RequestOptions, ssl: SSLOptions memoized(:request) { self.class.options_for(:request).new } diff --git a/lib/faraday/options/env.rb b/lib/faraday/options/env.rb index d7dac7178..6c78ae33e 100644 --- a/lib/faraday/options/env.rb +++ b/lib/faraday/options/env.rb @@ -1,65 +1,70 @@ # frozen_string_literal: true module Faraday - # @!attribute method - # @return [Symbol] HTTP method (`:get`, `:post`) - # - # @!attribute body - # @return [String] The request body that will eventually be converted to a - # string. - # - # @!attribute url - # @return [URI] URI instance for the current request. - # - # @!attribute request - # @return [Hash] options for configuring the request. - # Options for configuring the request. - # - # - `:timeout` open/read timeout Integer in seconds - # - `:open_timeout` - read timeout Integer in seconds - # - `:on_data` - Proc for streaming - # - `:proxy` - Hash of proxy options - # - `:uri` - Proxy Server URI - # - `:user` - Proxy server username - # - `:password` - Proxy server password - # - # @!attribute request_headers - # @return [Hash] HTTP Headers to be sent to the server. - # - # @!attribute ssl - # @return [Hash] options for configuring SSL requests - # - # @!attribute parallel_manager - # @return [Object] sent if the connection is in parallel mode - # - # @!attribute params - # @return [Hash] - # - # @!attribute response - # @return [Response] - # - # @!attribute response_headers - # @return [Hash] HTTP headers from the server - # - # @!attribute status - # @return [Integer] HTTP response status code - # - # @!attribute reason_phrase - # @return [String] - class Env < Options.new(:method, :request_body, :url, :request, - :request_headers, :ssl, :parallel_manager, :params, - :response, :response_headers, :status, - :reason_phrase, :response_body) - - # rubocop:disable Naming/ConstantName - ContentLength = 'Content-Length' - StatusesWithoutBody = Set.new [204, 304] - SuccessfulStatuses = (200..299).freeze - # rubocop:enable Naming/ConstantName + # @!parse + # # @!attribute method + # # @return [Symbol] HTTP method (`:get`, `:post`) + # # + # # @!attribute body + # # @return [String] The request body that will eventually be converted to a + # # string. + # # + # # @!attribute url + # # @return [URI] URI instance for the current request. + # # + # # @!attribute request + # # @return [Hash] options for configuring the request. + # # Options for configuring the request. + # # + # # - `:timeout` - time limit for the entire request (Integer in + # # seconds) + # # - `:open_timeout` - time limit for just the connection phase (e.g. + # # handshake) (Integer in seconds) + # # - `:read_timeout` - time limit for the first response byte received from + # # the server (Integer in seconds) + # # - `:write_timeout` - time limit for the client to send the request to the + # # server (Integer in seconds) + # # - `:on_data` - Proc for streaming + # # - `:proxy` - Hash of proxy options + # # - `:uri` - Proxy server URI + # # - `:user` - Proxy server username + # # - `:password` - Proxy server password + # # + # # @!attribute request_headers + # # @return [Hash] HTTP Headers to be sent to the server. + # # + # # @!attribute ssl + # # @return [Hash] options for configuring SSL requests + # # + # # @!attribute parallel_manager + # # @return [Object] sent if the connection is in parallel mode + # # + # # @!attribute params + # # @return [Hash] + # # + # # @!attribute response + # # @return [Response] + # # + # # @!attribute response_headers + # # @return [Hash] HTTP headers from the server + # # + # # @!attribute status + # # @return [Integer] HTTP response status code + # # + # # @!attribute reason_phrase + # # @return [String] + # class Env < Options; end + Env = Options.new(:method, :request_body, :url, :request, + :request_headers, :ssl, :parallel_manager, :params, + :response, :response_headers, :status, + :reason_phrase, :response_body) do + const_set(:ContentLength, 'Content-Length') + const_set(:StatusesWithoutBody, Set.new([204, 304])) + const_set(:SuccessfulStatuses, (200..299)) # A Set of HTTP verbs that typically send a body. If no body is set for # these requests, the Content-Length header is set to 0. - MethodsWithBodies = Set.new(Faraday::METHODS_WITH_BODY.map(&:to_sym)) + const_set(:MethodsWithBodies, Set.new(Faraday::METHODS_WITH_BODY.map(&:to_sym))) options request: RequestOptions, request_headers: Utils::Headers, response_headers: Utils::Headers @@ -120,25 +125,25 @@ def body=(value) # @return [Boolean] true if status is in the set of {SuccessfulStatuses}. def success? - SuccessfulStatuses.include?(status) + Env::SuccessfulStatuses.include?(status) end # @return [Boolean] true if there's no body yet, and the method is in the - # set of {MethodsWithBodies}. + # set of {Env::MethodsWithBodies}. def needs_body? - !body && MethodsWithBodies.include?(method) + !body && Env::MethodsWithBodies.include?(method) end # Sets content length to zero and the body to the empty string. def clear_body - request_headers[ContentLength] = '0' + request_headers[Env::ContentLength] = '0' self.body = +'' end # @return [Boolean] true if the status isn't in the set of - # {StatusesWithoutBody}. + # {Env::StatusesWithoutBody}. def parse_body? - !StatusesWithoutBody.include?(status) + !Env::StatusesWithoutBody.include?(status) end # @return [Boolean] true if there is a parallel_manager @@ -157,6 +162,24 @@ def inspect %(#<#{self.class}#{attrs.join(' ')}>) end + def stream_response? + request.stream_response? + end + + def stream_response(&block) + size = 0 + yielded = false + block_result = block.call do |chunk| + if chunk.bytesize.positive? || size.positive? + yielded = true + size += chunk.bytesize + request.on_data.call(chunk, size, self) + end + end + request.on_data.call(+'', 0, self) unless yielded + block_result + end + # @private def custom_members @custom_members ||= {} diff --git a/lib/faraday/options/proxy_options.rb b/lib/faraday/options/proxy_options.rb index c91dfc8d2..8d8b02386 100644 --- a/lib/faraday/options/proxy_options.rb +++ b/lib/faraday/options/proxy_options.rb @@ -1,16 +1,23 @@ # frozen_string_literal: true module Faraday - # ProxyOptions contains the configurable properties for the proxy - # configuration used when making an HTTP request. - class ProxyOptions < Options.new(:uri, :user, :password) + # @!parse + # # ProxyOptions contains the configurable properties for the proxy + # # configuration used when making an HTTP request. + # class ProxyOptions < Options; end + ProxyOptions = Options.new(:uri, :user, :password) do extend Forwardable def_delegators :uri, :scheme, :scheme=, :host, :host=, :port, :port=, :path, :path= def self.from(value) case value + when '' + value = nil when String + # URIs without a scheme should default to http (like 'example:123'). + # This fixes #1282 and prevents a silent failure in some adapters. + value = "http://#{value}" unless value.include?('://') value = { uri: Utils.URI(value) } when URI value = { uri: value } @@ -19,6 +26,7 @@ def self.from(value) value[:uri] = Utils.URI(uri) end end + super(value) end diff --git a/lib/faraday/options/request_options.rb b/lib/faraday/options/request_options.rb index 1a96fb8f9..3bb67c8e4 100644 --- a/lib/faraday/options/request_options.rb +++ b/lib/faraday/options/request_options.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true module Faraday - # RequestOptions contains the configurable properties for a Faraday request. - class RequestOptions < Options.new(:params_encoder, :proxy, :bind, - :timeout, :open_timeout, :read_timeout, - :write_timeout, :boundary, :oauth, - :context, :on_data) - + # @!parse + # # RequestOptions contains the configurable properties for a Faraday request. + # class RequestOptions < Options; end + RequestOptions = Options.new(:params_encoder, :proxy, :bind, + :timeout, :open_timeout, :read_timeout, + :write_timeout, :boundary, :oauth, + :context, :on_data) do def []=(key, value) if key && key.to_sym == :proxy super(key, value ? ProxyOptions.from(value) : nil) diff --git a/lib/faraday/options/ssl_options.rb b/lib/faraday/options/ssl_options.rb index 1fa5811a9..2a04ea173 100644 --- a/lib/faraday/options/ssl_options.rb +++ b/lib/faraday/options/ssl_options.rb @@ -1,51 +1,57 @@ # frozen_string_literal: true module Faraday - # SSL-related options. - # - # @!attribute verify - # @return [Boolean] whether to verify SSL certificates or not - # - # @!attribute ca_file - # @return [String] CA file - # - # @!attribute ca_path - # @return [String] CA path - # - # @!attribute verify_mode - # @return [Integer] Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html) - # - # @!attribute cert_store - # @return [OpenSSL::X509::Store] certificate store - # - # @!attribute client_cert - # @return [String, OpenSSL::X509::Certificate] client certificate - # - # @!attribute client_key - # @return [String, OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] client key - # - # @!attribute certificate - # @return [OpenSSL::X509::Certificate] certificate (Excon only) - # - # @!attribute private_key - # @return [OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] private key (Excon only) - # - # @!attribute verify_depth - # @return [Integer] maximum depth for the certificate chain verification - # - # @!attribute version - # @return [String, Symbol] SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-ssl_version-3D) - # - # @!attribute min_version - # @return [String, Symbol] minimum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-min_version-3D) - # - # @!attribute max_version - # @return [String, Symbol] maximum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-max_version-3D) - class SSLOptions < Options.new(:verify, :ca_file, :ca_path, :verify_mode, - :cert_store, :client_cert, :client_key, - :certificate, :private_key, :verify_depth, - :version, :min_version, :max_version) - + # @!parse + # # SSL-related options. + # # + # # @!attribute verify + # # @return [Boolean] whether to verify SSL certificates or not + # # + # # @!attribute verify_hostname + # # @return [Boolean] whether to enable hostname verification on server certificates + # # during the handshake or not (see https://github.com/ruby/openssl/pull/60) + # # + # # @!attribute ca_file + # # @return [String] CA file + # # + # # @!attribute ca_path + # # @return [String] CA path + # # + # # @!attribute verify_mode + # # @return [Integer] Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html) + # # + # # @!attribute cert_store + # # @return [OpenSSL::X509::Store] certificate store + # # + # # @!attribute client_cert + # # @return [String, OpenSSL::X509::Certificate] client certificate + # # + # # @!attribute client_key + # # @return [String, OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] client key + # # + # # @!attribute certificate + # # @return [OpenSSL::X509::Certificate] certificate (Excon only) + # # + # # @!attribute private_key + # # @return [OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] private key (Excon only) + # # + # # @!attribute verify_depth + # # @return [Integer] maximum depth for the certificate chain verification + # # + # # @!attribute version + # # @return [String, Symbol] SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-ssl_version-3D) + # # + # # @!attribute min_version + # # @return [String, Symbol] minimum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-min_version-3D) + # # + # # @!attribute max_version + # # @return [String, Symbol] maximum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-max_version-3D) + # class SSLOptions < Options; end + SSLOptions = Options.new(:verify, :verify_hostname, + :ca_file, :ca_path, :verify_mode, + :cert_store, :client_cert, :client_key, + :certificate, :private_key, :verify_depth, + :version, :min_version, :max_version) do # @return [Boolean] true if should verify def verify? verify != false @@ -55,5 +61,10 @@ def verify? def disable? !verify? end + + # @return [Boolean] true if should verify_hostname + def verify_hostname? + verify_hostname != false + end end end diff --git a/lib/faraday/param_part.rb b/lib/faraday/param_part.rb deleted file mode 100644 index c1279c359..000000000 --- a/lib/faraday/param_part.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Faraday - # Multipart value used to POST data with a content type. - class ParamPart - # @param value [String] Uploaded content as a String. - # @param content_type [String] String content type of the value. - # @param content_id [String] Optional String of this value's Content-ID. - # - # @return [Faraday::ParamPart] - def initialize(value, content_type, content_id = nil) - @value = value - @content_type = content_type - @content_id = content_id - end - - # Converts this value to a form part. - # - # @param boundary [String] String multipart boundary that must not exist in - # the content exactly. - # @param key [String] String key name for this value. - # - # @return [Faraday::Parts::Part] - def to_part(boundary, key) - Faraday::Parts::Part.new(boundary, key, value, headers) - end - - # Returns a Hash of String key/value pairs. - # - # @return [Hash] - def headers - { - 'Content-Type' => content_type, - 'Content-ID' => content_id - } - end - - # The content to upload. - # - # @return [String] - attr_reader :value - - # The value's content type. - # - # @return [String] - attr_reader :content_type - - # The value's content ID, if given. - # - # @return [String, nil] - attr_reader :content_id - end -end diff --git a/lib/faraday/rack_builder.rb b/lib/faraday/rack_builder.rb index 8d5040248..e8e62082b 100644 --- a/lib/faraday/rack_builder.rb +++ b/lib/faraday/rack_builder.rb @@ -7,7 +7,7 @@ module Faraday # middleware stack (heavily inspired by Rack). # # @example - # Faraday::Connection.new(url: 'http://sushi.com') do |builder| + # Faraday::Connection.new(url: 'http://httpbingo.org') do |builder| # builder.request :url_encoded # Faraday::Request::UrlEncoded # builder.adapter :net_http # Faraday::Adapter::NetHttp # end @@ -27,7 +27,7 @@ class Handler attr_reader :name - def initialize(klass, *args, &block) + ruby2_keywords def initialize(klass, *args, &block) @name = klass.to_s REGISTRY.set(klass) if klass.respond_to?(:name) @args = args @@ -57,23 +57,22 @@ def build(app = nil) end end - def initialize(handlers = [], adapter = nil, &block) - @adapter = adapter - @handlers = handlers - if block_given? - build(&block) - elsif @handlers.empty? - # default stack, if nothing else is configured - request :url_encoded - self.adapter Faraday.default_adapter - end + def initialize(&block) + @adapter = nil + @handlers = [] + build(&block) + end + + def initialize_dup(original) + super + @adapter = original.adapter + @handlers = original.handlers.dup end - def build(options = {}) + def build raise_if_locked - @handlers.clear unless options[:keep] - yield(self) if block_given? - adapter(Faraday.default_adapter) unless @adapter + block_given? ? yield(self) : request(:url_encoded) + adapter(Faraday.default_adapter, **Faraday.default_adapter_options) unless @adapter end def [](idx) @@ -89,7 +88,7 @@ def locked? @handlers.frozen? end - def use(klass, *args, &block) + ruby2_keywords def use(klass, *args, &block) if klass.is_a? Symbol use_symbol(Faraday::Middleware, klass, *args, &block) else @@ -99,16 +98,16 @@ def use(klass, *args, &block) end end - def request(key, *args, &block) + ruby2_keywords def request(key, *args, &block) use_symbol(Faraday::Request, key, *args, &block) end - def response(key, *args, &block) + ruby2_keywords def response(key, *args, &block) use_symbol(Faraday::Response, key, *args, &block) end - def adapter(klass = NO_ARGUMENT, *args, &block) - return @adapter if klass == NO_ARGUMENT + ruby2_keywords def adapter(klass = NO_ARGUMENT, *args, &block) + return @adapter if klass == NO_ARGUMENT || klass.nil? klass = Faraday::Adapter.lookup_middleware(klass) if klass.is_a?(Symbol) @adapter = self.class::Handler.new(klass, *args, &block) @@ -116,7 +115,7 @@ def adapter(klass = NO_ARGUMENT, *args, &block) ## methods to push onto the various positions in the stack: - def insert(index, *args, &block) + ruby2_keywords def insert(index, *args, &block) raise_if_locked index = assert_index(index) handler = self.class::Handler.new(*args, &block) @@ -125,12 +124,12 @@ def insert(index, *args, &block) alias insert_before insert - def insert_after(index, *args, &block) + ruby2_keywords def insert_after(index, *args, &block) index = assert_index(index) insert(index + 1, *args, &block) end - def swap(index, *args, &block) + ruby2_keywords def swap(index, *args, &block) raise_if_locked index = assert_index(index) @handlers.delete_at(index) @@ -163,6 +162,7 @@ def build_response(connection, request) def app @app ||= begin lock! + ensure_adapter! to_app end end @@ -181,12 +181,8 @@ def ==(other) @adapter == other.adapter end - def dup - self.class.new(@handlers.dup, @adapter.dup) - end - # ENV Keys - # :method - a symbolized request method (:get, :post) + # :http_method - a symbolized request HTTP method (:get, :post) # :body - the request body that will eventually be converted to a string. # :url - URI instance for the current request. # :status - HTTP response status code @@ -207,7 +203,7 @@ def build_env(connection, request) request.options.params_encoder ) - Env.new(request.method, request.body, exclusive_url, + Env.new(request.http_method, request.body, exclusive_url, request.options, request.headers, connection.ssl, connection.parallel_manager) end @@ -215,6 +211,9 @@ def build_env(connection, request) private LOCK_ERR = "can't modify middleware stack after making a request" + MISSING_ADAPTER_ERROR = "An attempt to run a request with a Faraday::Connection without adapter has been made.\n" \ + "Please set Faraday.default_adapter or provide one when initializing the connection.\n" \ + 'For more info, check https://lostisland.github.io/faraday/usage/.' def raise_if_locked raise StackLocked, LOCK_ERR if locked? @@ -226,15 +225,19 @@ def raise_if_adapter(klass) raise 'Adapter should be set using the `adapter` method, not `use`' end + def ensure_adapter! + raise MISSING_ADAPTER_ERROR unless @adapter + end + def adapter_set? !@adapter.nil? end def is_adapter?(klass) # rubocop:disable Naming/PredicateName - klass.ancestors.include?(Faraday::Adapter) + klass <= Faraday::Adapter end - def use_symbol(mod, key, *args, &block) + ruby2_keywords def use_symbol(mod, key, *args, &block) use(mod.lookup_middleware(key), *args, &block) end diff --git a/lib/faraday/request.rb b/lib/faraday/request.rb index 352ce6cec..8c5bf9525 100644 --- a/lib/faraday/request.rb +++ b/lib/faraday/request.rb @@ -12,7 +12,7 @@ module Faraday # req.body = 'abc' # end # - # @!attribute method + # @!attribute http_method # @return [Symbol] the HTTP method of the Request # @!attribute path # @return [URI, String] the path @@ -21,30 +21,16 @@ module Faraday # @!attribute headers # @return [Faraday::Utils::Headers] headers # @!attribute body - # @return [Hash] body + # @return [String] body # @!attribute options # @return [RequestOptions] options - # - # rubocop:disable Style/StructInheritance - class Request < Struct.new(:method, :path, :params, :headers, :body, :options) - # rubocop:enable Style/StructInheritance - + Request = Struct.new(:http_method, :path, :params, :headers, :body, :options) do extend MiddlewareRegistry - register_middleware File.expand_path('request', __dir__), - url_encoded: [:UrlEncoded, 'url_encoded'], - multipart: [:Multipart, 'multipart'], - retry: [:Retry, 'retry'], - authorization: [:Authorization, 'authorization'], - basic_auth: [ - :BasicAuthentication, - 'basic_authentication' - ], - token_auth: [ - :TokenAuthentication, - 'token_authentication' - ], - instrumentation: [:Instrumentation, 'instrumentation'] + alias_method :member_get, :[] + private :member_get + alias_method :member_set, :[]= + private :member_set # @param request_method [String] # @yield [request] for block customization, if block given @@ -56,6 +42,7 @@ def self.create(request_method) end end + remove_method :params= # Replace params, preserving the existing hash type. # # @param hash [Hash] new params @@ -63,10 +50,11 @@ def params=(hash) if params params.replace hash else - super + member_set(:params, hash) end end + remove_method :headers= # Replace request headers, preserving the existing hash type. # # @param hash [Hash] new headers @@ -74,7 +62,7 @@ def headers=(hash) if headers headers.replace hash else - super + member_set(:headers, hash) end end @@ -116,7 +104,7 @@ def []=(key, value) # @return [Hash] the hash ready to be serialized in Marshal. def marshal_dump { - method: method, + http_method: http_method, body: body, headers: headers, path: path, @@ -129,18 +117,23 @@ def marshal_dump # Restores the instance variables according to the +serialised+. # @param serialised [Hash] the serialised object. def marshal_load(serialised) - self.method = serialised[:method] - self.body = serialised[:body] + self.http_method = serialised[:http_method] + self.body = serialised[:body] self.headers = serialised[:headers] - self.path = serialised[:path] - self.params = serialised[:params] + self.path = serialised[:path] + self.params = serialised[:params] self.options = serialised[:options] end # @return [Env] the Env for this Request def to_env(connection) - Env.new(method, body, connection.build_exclusive_url(path, params), + Env.new(http_method, body, connection.build_exclusive_url(path, params), options, headers, connection.ssl, connection.parallel_manager) end end end + +require 'faraday/request/authorization' +require 'faraday/request/instrumentation' +require 'faraday/request/json' +require 'faraday/request/url_encoded' diff --git a/lib/faraday/request/authorization.rb b/lib/faraday/request/authorization.rb index 5797016d7..43732432d 100644 --- a/lib/faraday/request/authorization.rb +++ b/lib/faraday/request/authorization.rb @@ -4,50 +4,51 @@ module Faraday class Request # Request middleware for the Authorization HTTP header class Authorization < Faraday::Middleware - KEY = 'Authorization' unless defined? KEY - - # @param type [String, Symbol] - # @param token [String, Symbol, Hash] - # @return [String] a header value - def self.header(type, token) - case token - when String, Symbol - "#{type} #{token}" - when Hash - build_hash(type.to_s, token) - else - raise ArgumentError, - "Can't build an Authorization #{type}" \ - "header from #{token.inspect}" - end - end - - # @param type [String] - # @param hash [Hash] - # @return [String] type followed by comma-separated key=value pairs - # @api private - def self.build_hash(type, hash) - comma = ', ' - values = [] - hash.each do |key, value| - values << "#{key}=#{value.to_s.inspect}" - end - "#{type} #{values * comma}" - end + KEY = 'Authorization' # @param app [#call] # @param type [String, Symbol] Type of Authorization - # @param token [String, Symbol, Hash] Token value for the Authorization - def initialize(app, type, token) - @header_value = self.class.header(type, token) + # @param params [Array] parameters to build the Authorization header. + # If the type is `:basic`, then these can be a login and password pair. + # Otherwise, a single value is expected that will be appended after the type. + # This value can be a proc or an object responding to `.call`, in which case + # it will be invoked on each request. + def initialize(app, type, *params) + @type = type + @params = params super(app) end # @param env [Faraday::Env] - def call(env) - env.request_headers[KEY] = @header_value unless env.request_headers[KEY] - @app.call(env) + def on_request(env) + return if env.request_headers[KEY] + + env.request_headers[KEY] = header_from(@type, env, *@params) + end + + private + + # @param type [String, Symbol] + # @param env [Faraday::Env] + # @param params [Array] + # @return [String] a header value + def header_from(type, env, *params) + if type.to_s.casecmp('basic').zero? && params.size == 2 + Utils.basic_header_from(*params) + elsif params.size != 1 + raise ArgumentError, "Unexpected params received (got #{params.size} instead of 1)" + else + value = params.first + if (value.is_a?(Proc) && value.arity == 1) || (value.respond_to?(:call) && value.method(:call).arity == 1) + value = value.call(env) + elsif value.is_a?(Proc) || value.respond_to?(:call) + value = value.call + end + "#{type} #{value}" + end end end end end + +Faraday::Request.register_middleware(authorization: Faraday::Request::Authorization) diff --git a/lib/faraday/request/basic_authentication.rb b/lib/faraday/request/basic_authentication.rb deleted file mode 100644 index 61c9a5bc3..000000000 --- a/lib/faraday/request/basic_authentication.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'base64' - -module Faraday - class Request - # Authorization middleware for Basic Authentication. - class BasicAuthentication < load_middleware(:authorization) - # @param login [String] - # @param pass [String] - # - # @return [String] a Basic Authentication header line - def self.header(login, pass) - value = Base64.encode64([login, pass].join(':')) - value.delete!("\n") - super(:Basic, value) - end - end - end -end diff --git a/lib/faraday/request/instrumentation.rb b/lib/faraday/request/instrumentation.rb index a24442f3a..a5020598d 100644 --- a/lib/faraday/request/instrumentation.rb +++ b/lib/faraday/request/instrumentation.rb @@ -5,12 +5,14 @@ class Request # Middleware for instrumenting Requests. class Instrumentation < Faraday::Middleware # Options class used in Request::Instrumentation class. - class Options < Faraday::Options.new(:name, :instrumenter) + Options = Faraday::Options.new(:name, :instrumenter) do + remove_method :name # @return [String] def name self[:name] ||= 'request.faraday' end + remove_method :instrumenter # @return [Class] def instrumenter self[:instrumenter] ||= ActiveSupport::Notifications @@ -52,3 +54,5 @@ def call(env) end end end + +Faraday::Request.register_middleware(instrumentation: Faraday::Request::Instrumentation) diff --git a/lib/faraday/request/json.rb b/lib/faraday/request/json.rb new file mode 100644 index 000000000..f12ebe298 --- /dev/null +++ b/lib/faraday/request/json.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'json' + +module Faraday + class Request + # Request middleware that encodes the body as JSON. + # + # Processes only requests with matching Content-type or those without a type. + # If a request doesn't have a type but has a body, it sets the Content-type + # to JSON MIME-type. + # + # Doesn't try to encode bodies that already are in string form. + class Json < Middleware + MIME_TYPE = 'application/json' + MIME_TYPE_REGEX = %r{^application/(vnd\..+\+)?json$} + + def on_request(env) + match_content_type(env) do |data| + env[:body] = encode(data) + end + end + + private + + def encode(data) + if options[:encoder].is_a?(Array) && options[:encoder].size >= 2 + options[:encoder][0].public_send(options[:encoder][1], data) + elsif options[:encoder].respond_to?(:dump) + options[:encoder].dump(data) + else + ::JSON.generate(data) + end + end + + def match_content_type(env) + return unless process_request?(env) + + env[:request_headers][CONTENT_TYPE] ||= MIME_TYPE + yield env[:body] unless env[:body].respond_to?(:to_str) + end + + def process_request?(env) + type = request_type(env) + body?(env) && (type.empty? || type.match?(MIME_TYPE_REGEX)) + end + + def body?(env) + body = env[:body] + case body + when true, false + true + when nil + # NOTE: nil can be converted to `"null"`, but this middleware doesn't process `nil` for the compatibility. + false + else + !(body.respond_to?(:to_str) && body.empty?) + end + end + + def request_type(env) + type = env[:request_headers][CONTENT_TYPE].to_s + type = type.split(';', 2).first if type.index(';') + type + end + end + end +end + +Faraday::Request.register_middleware(json: Faraday::Request::Json) diff --git a/lib/faraday/request/multipart.rb b/lib/faraday/request/multipart.rb deleted file mode 100644 index 399c434f4..000000000 --- a/lib/faraday/request/multipart.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -require File.expand_path('url_encoded', __dir__) -require 'securerandom' - -module Faraday - class Request - # Middleware for supporting multi-part requests. - class Multipart < UrlEncoded - self.mime_type = 'multipart/form-data' - unless defined? DEFAULT_BOUNDARY_PREFIX - DEFAULT_BOUNDARY_PREFIX = '-----------RubyMultipartPost' - end - - # Checks for files in the payload, otherwise leaves everything untouched. - # - # @param env [Faraday::Env] - def call(env) - match_content_type(env) do |params| - env.request.boundary ||= unique_boundary - env.request_headers[CONTENT_TYPE] += - "; boundary=#{env.request.boundary}" - env.body = create_multipart(env, params) - end - @app.call env - end - - # @param env [Faraday::Env] - def process_request?(env) - type = request_type(env) - env.body.respond_to?(:each_key) && !env.body.empty? && ( - (type.empty? && has_multipart?(env.body)) || - (type == self.class.mime_type) - ) - end - - # Returns true if obj is an enumerable with values that are multipart. - # - # @param obj [Object] - # @return [Boolean] - def has_multipart?(obj) # rubocop:disable Naming/PredicateName - if obj.respond_to?(:each) - (obj.respond_to?(:values) ? obj.values : obj).each do |val| - return true if val.respond_to?(:content_type) || has_multipart?(val) - end - end - false - end - - # @param env [Faraday::Env] - # @param params [Hash] - def create_multipart(env, params) - boundary = env.request.boundary - parts = process_params(params) do |key, value| - part(boundary, key, value) - end - parts << Faraday::Parts::EpiloguePart.new(boundary) - - body = Faraday::CompositeReadIO.new(parts) - env.request_headers[Faraday::Env::ContentLength] = body.length.to_s - body - end - - def part(boundary, key, value) - if value.respond_to?(:to_part) - value.to_part(boundary, key) - else - Faraday::Parts::Part.new(boundary, key, value) - end - end - - # @return [String] - def unique_boundary - "#{DEFAULT_BOUNDARY_PREFIX}-#{SecureRandom.hex}" - end - - # @param params [Hash] - # @param prefix [String] - # @param pieces [Array] - def process_params(params, prefix = nil, pieces = nil, &block) - params.inject(pieces || []) do |all, (key, value)| - key = "#{prefix}[#{key}]" if prefix - - case value - when Array - values = value.inject([]) { |a, v| a << [nil, v] } - process_params(values, key, all, &block) - when Hash - process_params(value, key, all, &block) - else - # rubocop:disable Performance/RedundantBlockCall - all << block.call(key, value) - # rubocop:enable Performance/RedundantBlockCall - end - end - end - end - end -end diff --git a/lib/faraday/request/retry.rb b/lib/faraday/request/retry.rb deleted file mode 100644 index e76eae214..000000000 --- a/lib/faraday/request/retry.rb +++ /dev/null @@ -1,239 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Request - # Catches exceptions and retries each request a limited number of times. - # - # By default, it retries 2 times and handles only timeout exceptions. It can - # be configured with an arbitrary number of retries, a list of exceptions to - # handle, a retry interval, a percentage of randomness to add to the retry - # interval, and a backoff factor. - # - # @example Configure Retry middleware using intervals - # Faraday.new do |conn| - # conn.request(:retry, max: 2, - # interval: 0.05, - # interval_randomness: 0.5, - # backoff_factor: 2, - # exceptions: [CustomException, 'Timeout::Error']) - # - # conn.adapter(:net_http) # NB: Last middleware must be the adapter - # end - # - # This example will result in a first interval that is random between 0.05 - # and 0.075 and a second interval that is random between 0.1 and 0.15. - class Retry < Faraday::Middleware - DEFAULT_EXCEPTIONS = [ - Errno::ETIMEDOUT, 'Timeout::Error', - Faraday::TimeoutError, Faraday::RetriableResponse - ].freeze - IDEMPOTENT_METHODS = %i[delete get head options put].freeze - - # Options contains the configurable parameters for the Retry middleware. - class Options < Faraday::Options.new(:max, :interval, :max_interval, - :interval_randomness, - :backoff_factor, :exceptions, - :methods, :retry_if, :retry_block, - :retry_statuses) - - DEFAULT_CHECK = ->(_env, _exception) { false } - - def self.from(value) - if value.is_a?(Integer) - new(value) - else - super(value) - end - end - - def max - (self[:max] ||= 2).to_i - end - - def interval - (self[:interval] ||= 0).to_f - end - - def max_interval - (self[:max_interval] ||= Float::MAX).to_f - end - - def interval_randomness - (self[:interval_randomness] ||= 0).to_f - end - - def backoff_factor - (self[:backoff_factor] ||= 1).to_f - end - - def exceptions - Array(self[:exceptions] ||= DEFAULT_EXCEPTIONS) - end - - def methods - Array(self[:methods] ||= IDEMPOTENT_METHODS) - end - - def retry_if - self[:retry_if] ||= DEFAULT_CHECK - end - - def retry_block - self[:retry_block] ||= proc {} - end - - def retry_statuses - Array(self[:retry_statuses] ||= []) - end - end - - # @param app [#call] - # @param options [Hash] - # @option options [Integer] :max (2) Maximum number of retries - # @option options [Integer] :interval (0) Pause in seconds between retries - # @option options [Integer] :interval_randomness (0) The maximum random - # interval amount expressed as a float between - # 0 and 1 to use in addition to the interval. - # @option options [Integer] :max_interval (Float::MAX) An upper limit - # for the interval - # @option options [Integer] :backoff_factor (1) The amount to multiply - # each successive retry's interval amount by in order to provide backoff - # @option options [Array] :exceptions ([ Errno::ETIMEDOUT, - # 'Timeout::Error', Faraday::TimeoutError, Faraday::RetriableResponse]) - # The list of exceptions to handle. Exceptions can be given as - # Class, Module, or String. - # @option options [Array] :methods (the idempotent HTTP methods - # in IDEMPOTENT_METHODS) A list of HTTP methods to retry without - # calling retry_if. Pass an empty Array to call retry_if - # for all exceptions. - # @option options [Block] :retry_if (false) block that will receive - # the env object and the exception raised - # and should decide if the code should retry still the action or - # not independent of the retry count. This would be useful - # if the exception produced is non-recoverable or if the - # the HTTP method called is not idempotent. - # @option options [Block] :retry_block block that is executed after - # every retry. Request environment, middleware options, current number - # of retries and the exception is passed to the block as parameters. - # @option options [Array] :retry_statuses Array of Integer HTTP status - # codes or a single Integer value that determines whether to raise - # a Faraday::RetriableResponse exception based on the HTTP status code - # of an HTTP response. - def initialize(app, options = nil) - super(app) - @options = Options.from(options) - @errmatch = build_exception_matcher(@options.exceptions) - end - - def calculate_sleep_amount(retries, env) - retry_after = calculate_retry_after(env) - retry_interval = calculate_retry_interval(retries) - - return if retry_after && retry_after > @options.max_interval - - if retry_after && retry_after >= retry_interval - retry_after - else - retry_interval - end - end - - # @param env [Faraday::Env] - def call(env) - retries = @options.max - request_body = env[:body] - begin - # after failure env[:body] is set to the response body - env[:body] = request_body - @app.call(env).tap do |resp| - if @options.retry_statuses.include?(resp.status) - raise Faraday::RetriableResponse.new(nil, resp) - end - end - rescue @errmatch => e - if retries.positive? && retry_request?(env, e) - retries -= 1 - rewind_files(request_body) - @options.retry_block.call(env, @options, retries, e) - if (sleep_amount = calculate_sleep_amount(retries + 1, env)) - sleep sleep_amount - retry - end - end - - raise unless e.is_a?(Faraday::RetriableResponse) - - e.response - end - end - - # An exception matcher for the rescue clause can usually be any object - # that responds to `===`, but for Ruby 1.8 it has to be a Class or Module. - # - # @param exceptions [Array] - # @api private - # @return [Module] an exception matcher - def build_exception_matcher(exceptions) - matcher = Module.new - ( - class << matcher - self - end).class_eval do - define_method(:===) do |error| - exceptions.any? do |ex| - if ex.is_a? Module - error.is_a? ex - else - error.class.to_s == ex.to_s - end - end - end - end - matcher - end - - private - - def retry_request?(env, exception) - @options.methods.include?(env[:method]) || - @options.retry_if.call(env, exception) - end - - def rewind_files(body) - return unless body.is_a?(Hash) - - body.each do |_, value| - value.rewind if value.is_a?(UploadIO) - end - end - - # MDN spec for Retry-After header: - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After - def calculate_retry_after(env) - response_headers = env[:response_headers] - return unless response_headers - - retry_after_value = env[:response_headers]['Retry-After'] - - # Try to parse date from the header value - begin - datetime = DateTime.rfc2822(retry_after_value) - datetime.to_time - Time.now.utc - rescue ArgumentError - retry_after_value.to_f - end - end - - def calculate_retry_interval(retries) - retry_index = @options.max - retries - current_interval = @options.interval * - (@options.backoff_factor**retry_index) - current_interval = [current_interval, @options.max_interval].min - random_interval = rand * @options.interval_randomness.to_f * - @options.interval - - current_interval + random_interval - end - end - end -end diff --git a/lib/faraday/request/token_authentication.rb b/lib/faraday/request/token_authentication.rb deleted file mode 100644 index f28264b1c..000000000 --- a/lib/faraday/request/token_authentication.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Request - # TokenAuthentication is a middleware that adds a 'Token' header to a - # Faraday request. - class TokenAuthentication < load_middleware(:authorization) - # Public - def self.header(token, options = nil) - options ||= {} - options[:token] = token - super(:Token, options) - end - - def initialize(app, token, options = nil) - super(app, token, options) - end - end - end -end diff --git a/lib/faraday/request/url_encoded.rb b/lib/faraday/request/url_encoded.rb index 7534b267d..5ac7dcb35 100644 --- a/lib/faraday/request/url_encoded.rb +++ b/lib/faraday/request/url_encoded.rb @@ -4,7 +4,9 @@ module Faraday class Request # Middleware for supporting urlencoded requests. class UrlEncoded < Faraday::Middleware - CONTENT_TYPE = 'Content-Type' unless defined? CONTENT_TYPE + unless defined?(::Faraday::Request::UrlEncoded::CONTENT_TYPE) + CONTENT_TYPE = 'Content-Type' + end class << self attr_accessor :mime_type @@ -29,7 +31,9 @@ def match_content_type(env) return unless process_request?(env) env.request_headers[CONTENT_TYPE] ||= self.class.mime_type - yield(env.body) unless env.body.respond_to?(:to_str) + return if env.body.respond_to?(:to_str) || env.body.respond_to?(:read) + + yield(env.body) end # @param env [Faraday::Env] @@ -52,3 +56,5 @@ def request_type(env) end end end + +Faraday::Request.register_middleware(url_encoded: Faraday::Request::UrlEncoded) diff --git a/lib/faraday/response.rb b/lib/faraday/response.rb index 0da52f112..d1fa9320d 100644 --- a/lib/faraday/response.rb +++ b/lib/faraday/response.rb @@ -5,28 +5,9 @@ module Faraday # Response represents an HTTP response from making an HTTP request. class Response - # Used for simple response middleware. - class Middleware < Faraday::Middleware - def call(env) - @app.call(env).on_complete do |environment| - on_complete(environment) - end - end - - # Override this to modify the environment after the response has finished. - # Calls the `parse` method if defined - def on_complete(env) - env.body = parse(env.body) if respond_to?(:parse) && env.parse_body? - end - end - extend Forwardable extend MiddlewareRegistry - register_middleware File.expand_path('response', __dir__), - raise_error: [:RaiseError, 'raise_error'], - logger: [:Logger, 'logger'] - def initialize(env = nil) @env = Env.from(env) if env @on_complete_callbacks = [] @@ -45,6 +26,7 @@ def reason_phrase def headers finished? ? env.response_headers : {} end + def_delegator :headers, :[] def body @@ -56,10 +38,10 @@ def finished? end def on_complete(&block) - if !finished? - @on_complete_callbacks << block - else + if finished? yield(env) + else + @on_complete_callbacks << block end self end @@ -79,7 +61,8 @@ def success? def to_hash { status: env.status, body: env.body, - response_headers: env.response_headers + response_headers: env.response_headers, + url: env.url } end @@ -102,3 +85,7 @@ def apply_request(request_env) end end end + +require 'faraday/response/json' +require 'faraday/response/logger' +require 'faraday/response/raise_error' diff --git a/lib/faraday/response/json.rb b/lib/faraday/response/json.rb new file mode 100644 index 000000000..71a57edb7 --- /dev/null +++ b/lib/faraday/response/json.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'json' + +module Faraday + class Response + # Parse response bodies as JSON. + class Json < Middleware + def initialize(app = nil, parser_options: nil, content_type: /\bjson$/, preserve_raw: false) + super(app) + @parser_options = parser_options + @content_types = Array(content_type) + @preserve_raw = preserve_raw + + process_parser_options + end + + def on_complete(env) + process_response(env) if parse_response?(env) + end + + private + + def process_response(env) + env[:raw_body] = env[:body] if @preserve_raw + env[:body] = parse(env[:body]) + rescue StandardError, SyntaxError => e + raise Faraday::ParsingError.new(e, env[:response]) + end + + def parse(body) + return if body.strip.empty? + + decoder, method_name = @decoder_options + + decoder.public_send(method_name, body, @parser_options || {}) + end + + def parse_response?(env) + process_response_type?(env) && + env[:body].respond_to?(:to_str) + end + + def process_response_type?(env) + type = response_type(env) + @content_types.empty? || @content_types.any? do |pattern| + pattern.is_a?(Regexp) ? type.match?(pattern) : type == pattern + end + end + + def response_type(env) + type = env[:response_headers][CONTENT_TYPE].to_s + type = type.split(';', 2).first if type.index(';') + type + end + + def process_parser_options + @decoder_options = @parser_options&.delete(:decoder) + + @decoder_options = + if @decoder_options.is_a?(Array) && @decoder_options.size >= 2 + @decoder_options.slice(0, 2) + elsif @decoder_options&.respond_to?(:load) # rubocop:disable Lint/RedundantSafeNavigation + # In some versions of Rails, `nil` responds to `load` - hence the safe navigation check above + [@decoder_options, :load] + else + [::JSON, :parse] + end + end + end + end +end + +Faraday::Response.register_middleware(json: Faraday::Response::Json) diff --git a/lib/faraday/response/logger.rb b/lib/faraday/response/logger.rb index 35aefb860..d46414620 100644 --- a/lib/faraday/response/logger.rb +++ b/lib/faraday/response/logger.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'forwardable' +require 'logger' require 'faraday/logging/formatter' module Faraday @@ -11,10 +12,7 @@ class Response class Logger < Middleware def initialize(app, logger = nil, options = {}) super(app) - logger ||= begin - require 'logger' - ::Logger.new($stdout) - end + logger ||= ::Logger.new($stdout) formatter_class = options.delete(:formatter) || Logging::Formatter @formatter = formatter_class.new(logger: logger, options: options) yield @formatter if block_given? @@ -28,6 +26,12 @@ def call(env) def on_complete(env) @formatter.response(env) end + + def on_error(exc) + @formatter.exception(exc) if @formatter.respond_to?(:exception) + end end end end + +Faraday::Response.register_middleware(logger: Faraday::Response::Logger) diff --git a/lib/faraday/response/raise_error.rb b/lib/faraday/response/raise_error.rb index 9b624c828..957b6e785 100644 --- a/lib/faraday/response/raise_error.rb +++ b/lib/faraday/response/raise_error.rb @@ -6,10 +6,12 @@ class Response # client or server error responses. class RaiseError < Middleware # rubocop:disable Naming/ConstantName - ClientErrorStatuses = (400...500).freeze - ServerErrorStatuses = (500...600).freeze + ClientErrorStatuses = (400...500) + ServerErrorStatuses = (500...600) # rubocop:enable Naming/ConstantName + DEFAULT_OPTIONS = { include_request: true }.freeze + def on_complete(env) case env[:status] when 400 @@ -24,10 +26,14 @@ def on_complete(env) # mimic the behavior that we get with proxy requests with HTTPS msg = %(407 "Proxy Authentication Required") raise Faraday::ProxyAuthError.new(msg, response_values(env)) + when 408 + raise Faraday::RequestTimeoutError, response_values(env) when 409 raise Faraday::ConflictError, response_values(env) when 422 raise Faraday::UnprocessableEntityError, response_values(env) + when 429 + raise Faraday::TooManyRequestsError, response_values(env) when ClientErrorStatuses raise Faraday::ClientError, response_values(env) when ServerErrorStatuses @@ -37,9 +43,43 @@ def on_complete(env) end end + # Returns a hash of response data with the following keys: + # - status + # - headers + # - body + # - request + # + # The `request` key is omitted when the middleware is explicitly + # configured with the option `include_request: false`. def response_values(env) - { status: env.status, headers: env.response_headers, body: env.body } + response = { + status: env.status, + headers: env.response_headers, + body: env.body + } + + # Include the request data by default. If the middleware was explicitly + # configured to _not_ include request data, then omit it. + return response unless options[:include_request] + + response.merge( + request: { + method: env.method, + url: env.url, + url_path: env.url.path, + params: query_params(env), + headers: env.request_headers, + body: env.request_body + } + ) + end + + def query_params(env) + env.request.params_encoder ||= Faraday::Utils.default_params_encoder + env.params_encoder.decode(env.url.query) end end end end + +Faraday::Response.register_middleware(raise_error: Faraday::Response::RaiseError) diff --git a/lib/faraday/utils.rb b/lib/faraday/utils.rb index efe2feb92..809b3a88e 100644 --- a/lib/faraday/utils.rb +++ b/lib/faraday/utils.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'uri' require 'faraday/utils/headers' require 'faraday/utils/params_hash' @@ -16,19 +17,27 @@ def build_nested_query(params) NestedParamsEncoder.encode(params) end - ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/.freeze + def default_space_encoding + @default_space_encoding ||= '+' + end + + class << self + attr_writer :default_space_encoding + end + + ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/ def escape(str) str.to_s.gsub(ESCAPE_RE) do |match| - '%' + match.unpack('H2' * match.bytesize).join('%').upcase - end.tr(' ', '+') + "%#{match.unpack('H2' * match.bytesize).join('%').upcase}" + end.gsub(' ', default_space_encoding) end def unescape(str) CGI.unescape str.to_s end - DEFAULT_SEP = /[&;] */n.freeze + DEFAULT_SEP = /[&;] */n # Adapted from Rack def parse_query(query) @@ -43,6 +52,12 @@ def default_params_encoder @default_params_encoder ||= NestedParamsEncoder end + def basic_header_from(login, pass) + value = ["#{login}:#{pass}"].pack('m') # Base64 encoding + value.delete!("\n") + "Basic #{value}" + end + class << self attr_writer :default_params_encoder end @@ -63,10 +78,7 @@ def URI(url) # rubocop:disable Naming/MethodName end def default_uri_parser - @default_uri_parser ||= begin - require 'uri' - Kernel.method(:URI) - end + @default_uri_parser ||= Kernel.method(:URI) end def default_uri_parser=(parser) @@ -81,14 +93,14 @@ def default_uri_parser=(parser) # the path with the query string sorted. def normalize_path(url) url = URI(url) - (url.path.start_with?('/') ? url.path : '/' + url.path) + + (url.path.start_with?('/') ? url.path : "/#{url.path}") + (url.query ? "?#{sort_query_params(url.query)}" : '') end # Recursive hash update def deep_merge!(target, hash) hash.each do |key, value| - target[key] = if value.is_a?(Hash) && target[key].is_a?(Hash) + target[key] = if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options)) deep_merge(target[key], value) else value diff --git a/lib/faraday/utils/headers.rb b/lib/faraday/utils/headers.rb index 7d06d91a0..27b06e964 100644 --- a/lib/faraday/utils/headers.rb +++ b/lib/faraday/utils/headers.rb @@ -62,10 +62,10 @@ def []=(key, val) super(key, val) end - def fetch(key, *args, &block) + def fetch(key, ...) key = KeyMap[key] key = @names.fetch(key.downcase, key) - super(key, *args, &block) + super(key, ...) end def delete(key) @@ -77,6 +77,12 @@ def delete(key) super(key) end + def dig(key, *rest) + key = KeyMap[key] + key = @names.fetch(key.downcase, key) + super(key, *rest) + end + def include?(key) @names.include? key.downcase end @@ -105,16 +111,16 @@ def replace(other) end def to_hash - ::Hash.new.update(self) + {}.update(self) end def parse(header_string) return unless header_string && !header_string.empty? - headers = header_string.split(/\r\n/) + headers = header_string.split("\r\n") # Find the last set of response headers. - start_index = headers.rindex { |x| x.match(%r{^HTTP/}) } || 0 + start_index = headers.rindex { |x| x.start_with?('HTTP/') } || 0 last_response = headers.slice(start_index, headers.size) last_response @@ -132,7 +138,12 @@ def parse(header_string) # Join multiple values with a comma. def add_parsed(key, value) - self[key] ? self[key] << ', ' << value : self[key] = value + if key?(key) + self[key] = self[key].to_s + self[key] << ', ' << value + else + self[key] = value + end end end end diff --git a/lib/faraday/version.rb b/lib/faraday/version.rb new file mode 100644 index 000000000..fef7539a2 --- /dev/null +++ b/lib/faraday/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Faraday + VERSION = '2.10.0' +end diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..e38081b36 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2029 @@ +{ + "name": "faraday-docs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "faraday-docs", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "docsify-cli": "^4.4.4" + } + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/boxen/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/boxen/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect-livereload": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/connect-livereload/-/connect-livereload-0.6.1.tgz", + "integrity": "sha512-3R0kMOdL7CjJpU66fzAkCe6HNtd3AavCS4m+uW4KtJjrdGPT0SQEZieAYd+cm+lJoBznNQ4lqipYWkhBMgk00g==", + "engines": { + "node": "*" + } + }, + "node_modules/cp-file": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-7.0.0.tgz", + "integrity": "sha512-0Cbj7gyvFVApzpK/uhCtQ/9kE9UnYpxMzaq5nQQC/Dh4iaj5fxp7iEFIullrYwzj8nf0qnsI1Qsx34hAeAebvw==", + "dependencies": { + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "nested-error-stacks": "^2.0.0", + "p-event": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/docsify": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/docsify/-/docsify-4.13.1.tgz", + "integrity": "sha512-BsDypTBhw0mfslw9kZgAspCMZSM+sUIIDg5K/t1hNLkvbem9h64ZQc71e1IpY+iWsi/KdeqfazDfg52y2Lmm0A==", + "hasInstallScript": true, + "dependencies": { + "marked": "^1.2.9", + "medium-zoom": "^1.0.6", + "opencollective-postinstall": "^2.0.2", + "prismjs": "^1.27.0", + "strip-indent": "^3.0.0", + "tinydate": "^1.3.0", + "tweezer.js": "^1.4.0" + } + }, + "node_modules/docsify-cli": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/docsify-cli/-/docsify-cli-4.4.4.tgz", + "integrity": "sha512-NAZgg6b0BsDuq/Pe+P19Qb2J1d+ZVbS0eGkeCNxyu4F9/CQSsRqZqAvPJ9/0I+BCHn4sgftA2jluqhQVzKzrSA==", + "dependencies": { + "chalk": "^2.4.2", + "connect": "^3.6.0", + "connect-history-api-fallback": "^1.6.0", + "connect-livereload": "^0.6.0", + "cp-file": "^7.0.0", + "docsify": "^4.12.2", + "docsify-server-renderer": ">=4.10.0", + "enquirer": "^2.3.6", + "fs-extra": "^8.1.0", + "get-port": "^5.0.0", + "livereload": "^0.9.2", + "lru-cache": "^5.1.1", + "open": "^6.4.0", + "serve-static": "^1.12.1", + "update-notifier": "^4.1.0", + "yargonaut": "^1.1.2", + "yargs": "^15.3.0" + }, + "bin": { + "docsify": "bin/docsify" + }, + "engines": { + "node": ">= 10", + "npm": ">= 6" + } + }, + "node_modules/docsify-server-renderer": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/docsify-server-renderer/-/docsify-server-renderer-4.13.1.tgz", + "integrity": "sha512-XNJeCK3zp+mVO7JZFn0bH4hNBAMMC1MbuCU7CBsjLHYn4NHrjIgCBGmylzEan3/4Qm6kbSzQx8XzUK5T7GQxHw==", + "dependencies": { + "debug": "^4.3.3", + "docsify": "^4.12.4", + "node-fetch": "^2.6.6", + "resolve-pathname": "^3.0.0" + } + }, + "node_modules/docsify-server-renderer/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/docsify-server-renderer/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/figlet": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.6.0.tgz", + "integrity": "sha512-31EQGhCEITv6+hi2ORRPyn3bulaV9Fl4xOdR169cBzH/n1UqcxsiSB/noo6SJdD7Kfb1Ljit+IgR1USvF/XbdA==", + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "dependencies": { + "ini": "1.3.7" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "dependencies": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/livereload": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", + "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", + "dependencies": { + "chokidar": "^3.5.0", + "livereload-js": "^3.3.1", + "opts": ">= 1.2.0", + "ws": "^7.4.3" + }, + "bin": { + "livereload": "bin/livereload.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/livereload-js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.1.tgz", + "integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.2.9.tgz", + "integrity": "sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw==", + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">= 8.16.2" + } + }, + "node_modules/medium-zoom": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.0.8.tgz", + "integrity": "sha512-CjFVuFq/IfrdqesAXfg+hzlDKu6A2n80ZIq0Kl9kWjoHh9j1N9Uvk5X0/MmN0hOfm5F9YBswlClhcwnmtwz7gA==" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/nested-error-stacks": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz", + "integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==" + }, + "node_modules/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "bin": { + "opencollective-postinstall": "index.js" + } + }, + "node_modules/opts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", + "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==" + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parent-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", + "integrity": "sha512-2MXDNZC4aXdkkap+rBBMv0lUsfJqvX5/2FiYYnfCnorZt3Pk06/IOR5KeaoghgS2w07MLWgjbsnyaq6PdHn2LQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/registry-auth-token": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", + "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinydate": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz", + "integrity": "sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tweezer.js": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/tweezer.js/-/tweezer.js-1.5.0.tgz", + "integrity": "sha512-aSiJz7rGWNAQq7hjMK9ZYDuEawXupcCWgl3woQQSoDP2Oh8O4srWb/uO1PzzHIsrPEOqrjJ2sUb9FERfzuBabQ==" + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "dependencies": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/update-notifier/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/update-notifier/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yargonaut": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz", + "integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==", + "dependencies": { + "chalk": "^1.1.1", + "figlet": "^1.1.1", + "parent-require": "^1.0.0" + } + }, + "node_modules/yargonaut/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargonaut/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargonaut/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargonaut/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargonaut/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..117126d9f --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "faraday-docs", + "version": "0.1.0", + "description": "Faraday Docs", + "main": "index.js", + "directories": { + "doc": "docs", + "example": "examples", + "lib": "lib" + }, + "scripts": { + "docs": "docsify serve ./docs" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lostisland/faraday.git" + }, + "keywords": [ + "docs", + "faraday" + ], + "author": "Mattia Giuffrida", + "license": "MIT", + "bugs": { + "url": "https://github.com/lostisland/faraday/issues" + }, + "homepage": "https://github.com/lostisland/faraday#readme", + "dependencies": { + "docsify-cli": "^4.4.4" + } +} diff --git a/script/bootstrap b/script/bootstrap deleted file mode 100755 index 5c2694b65..000000000 --- a/script/bootstrap +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e - -if ! bundle config build.eventmachine | grep -q 'cppflags='; then - if openssl_dir="$(brew --prefix openssl 2>/dev/null)"; then - bundle config --local build.eventmachine \ - --with-cppflags="-I${openssl_dir}/include" \ - --with-ldflags="-L${openssl_dir}/lib" - fi -fi - -bundle install --path vendor/bundle --jobs 4 diff --git a/script/console b/script/console deleted file mode 100755 index 4e16d58e7..000000000 --- a/script/console +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# Usage: script/console -# Starts an IRB console with this library loaded. - -gemspec="$(ls *.gemspec | head -1)" - -exec bundle exec irb -r "${gemspec%.*}" "$@" diff --git a/script/generate_certs b/script/generate_certs deleted file mode 100755 index 513c4e29d..000000000 --- a/script/generate_certs +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Usage: generate_certs [options] -# options: -# -s Display shell exports that link env variables to filenames -# Generate test certs for testing Faraday with SSL - -require 'openssl' -require 'fileutils' - -# Adapted from WEBrick::Utils. Skips cert extensions so it -# can be used as a CA bundle -def create_self_signed_cert(bits, cname, _comment) - rsa = OpenSSL::PKey::RSA.new(bits) - cert = OpenSSL::X509::Certificate.new - cert.version = 2 - cert.serial = 1 - name = OpenSSL::X509::Name.new(cname) - cert.subject = name - cert.issuer = name - cert.not_before = Time.now - cert.not_after = Time.now + (365 * 24 * 60 * 60) - cert.public_key = rsa.public_key - cert.sign(rsa, OpenSSL::Digest::SHA1.new) - [cert, rsa] -end - -def write(file, contents, env_var) - FileUtils.mkdir_p(File.dirname(file)) - File.open(file, 'w') { |f| f.puts(contents) } - puts %(export #{env_var}="#{file}") if ARGV.include? '-s' -end - -# One cert / CA for ease of testing when ignoring verification -cert, key = create_self_signed_cert(1024, [%w[CN localhost]], - 'Faraday Test CA') - -write 'tmp/faraday-cert.key', key, 'SSL_KEY' -write 'tmp/faraday-cert.crt', cert, 'SSL_FILE' - -# And a second CA to prove that verification can fail -cert, key = create_self_signed_cert(1024, [%w[CN real-ca.com]], - 'A different CA') - -write 'tmp/faraday-different-ca-cert.key', key, 'SSL_KEY_ALT' -write 'tmp/faraday-different-ca-cert.crt', cert, 'SSL_FILE_ALT' diff --git a/script/proxy-server b/script/proxy-server deleted file mode 100755 index e0df54b5d..000000000 --- a/script/proxy-server +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Usage: script/proxy-server [-p PORT] [-u USER:PASSWORD] -require 'webrick' -require 'webrick/httpproxy' - -port = 4001 - -if (found = ARGV.index('-p')) - port = ARGV[found + 1].to_i -end -if (found = ARGV.index('-u')) - username, password = ARGV[found + 1].split(':', 2) -end - -match_credentials = lambda { |credentials| - got_username, got_password = credentials.to_s.unpack('m*')[0].split(':', 2) - got_username == username && got_password == password -} - -log_io = $stdout -log_io.sync = true - -webrick_opts = { - Port: port, Logger: WEBrick::Log.new(log_io), - AccessLog: [[log_io, '[%{X-Faraday-Adapter}i] %m %U -> %s %b']], - ProxyAuthProc: lambda { |req, res| - if username - type, credentials = req.header['proxy-authorization'] - .first.to_s.split(/\s+/, 2) - unless type == 'Basic' && match_credentials.call(credentials) - res['proxy-authenticate'] = %(Basic realm="testing") - raise WEBrick::HTTPStatus::ProxyAuthenticationRequired - end - end - } -} - -proxy = WEBrick::HTTPProxyServer.new(webrick_opts) - -trap(:TERM) { proxy.shutdown } -trap(:INT) { proxy.shutdown } - -proxy.start diff --git a/script/server b/script/server deleted file mode 100755 index 1105c629e..000000000 --- a/script/server +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -old_verbose = $VERBOSE -$VERBOSE = nil -begin - require File.expand_path('../test/live_server', __dir__) -ensure - $VERBOSE = old_verbose -end -require 'webrick' - -port = 4000 -if (found = ARGV.index('-p')) - port = ARGV[found + 1].to_i -end - -log_io = $stdout -log_io.sync = true - -webrick_opts = { - Port: port, Logger: WEBrick::Log.new(log_io), - AccessLog: [[log_io, '[%{X-Faraday-Adapter}i] %m %U -> %s %b']] -} - -if ENV['SSL_KEY'] - require 'openssl' - require 'webrick/https' - webrick_opts.update \ - SSLEnable: true, - SSLPrivateKey: OpenSSL::PKey::RSA.new(File.read(ENV['SSL_KEY'])), - SSLCertificate: OpenSSL::X509::Certificate.new(File.read(ENV['SSL_FILE'])), - SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE -end - -Rack::Handler::WEBrick.run(Faraday::LiveServer, webrick_opts) do |server| - trap(:INT) { server.stop } - trap(:TERM) { server.stop } -end diff --git a/script/test b/script/test deleted file mode 100755 index 2a918bb76..000000000 --- a/script/test +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env bash -# Usage: script/test [file] [adapter]... -- [test/unit options] -# Runs the test suite against a local server spawned automatically in a -# thread. After tests are done, the server is shut down. -# -# If filename arguments are given, only those files are run. If arguments given -# are not filenames, they are taken as words that filter the list of files to run. -# -# Examples: -# -# $ script/test -# $ script/test test/env_test.rb -# $ script/test excon patron -# -# # Run only tests matching /ssl/ for the net_http adapter, with SSL enabled. -# $ SSL=yes script/test net_http -- -n /ssl/ -# -# # Run against multiple rbenv versions -# $ RBENV_VERSIONS="1.9.3-p194 ree-1.8.7-2012.02" script/test -set -e - -if [[ "$RUBYOPT" != *"bundler/setup"* ]]; then - export RUBYOPT="-rbundler/setup $RUBYOPT" -fi - -port=3999 -proxy_port=3998 -scheme=http - -if [ "$SSL" = "yes" ]; then - scheme=https - if [ -z "$SSL_KEY" ] || [ -z "$SSL_FILE" ]; then - eval "$(script/generate_certs -s)" - fi -fi - -find_test_files() { - find "$1" -name '*_test.rb' -} - -filter_matching() { - pattern="$1" - shift - for line in "$@"; do - [[ $line == *"$pattern"* ]] && echo "$line" - done -} - -start_server() { - mkdir -p log - script/server -p $port >log/test.log 2>&1 & - echo $! -} - -start_proxy() { - mkdir -p log - script/proxy-server -p $proxy_port -u "faraday@test.local:there is cake" >log/proxy.log 2>&1 & - echo $! -} - -server_started() { - lsof -i :${1?} >/dev/null -} - -timestamp() { - date +%s -} - -wait_for_server() { - timeout=$(( `timestamp` + $1 )) - while true; do - if server_started "$2"; then - break - elif [ `timestamp` -gt "$timeout" ]; then - echo "timed out after $1 seconds" >&2 - return 1 - fi - done -} - -filtered= -IFS=$'\n' test_files=($(find_test_files "test")) -declare -a explicit_files - -# Process filter arguments: -# - test filenames as taken as-is -# - other words are taken as pattern to match the list of known files against -# - arguments after "--" are forwarded to the ruby process -while [ $# -gt 0 ]; do - arg="$1" - shift - if [ "$arg" = "--" ]; then - break - elif [ -f "$arg" ]; then - filtered=true - explicit_files[${#explicit_files[@]}+1]="$arg" - else - filtered=true - IFS=$'\n' explicit_files=( - ${explicit_files[@]} - $(filter_matching "$arg" "${test_files[@]}" || true) - ) - fi -done - -# If there were filter args, replace test files list with the results -if [ -n "$filtered" ]; then - if [ ${#explicit_files[@]} -eq 0 ]; then - echo "Error: no test files match" >&2 - exit 1 - else - test_files=(${explicit_files[@]}) - echo running "${test_files[@]}" - fi -fi - -# If there are any adapter tests, spin up the HTTP server -if [ -n "$(filter_matching "adapters" "${test_files[@]}")" ]; then - if server_started $port; then - echo "aborted: another instance of server running on $port" >&2 - exit 1 - fi - server_pid=$(start_server) - proxy_pid=$(start_proxy) - wait_for_server 30 $port || { - cat log/test.log - kill "$server_pid" - kill "$proxy_pid" - exit 1 - } - wait_for_server 5 $proxy_port - cleanup() { - if [ $? -ne 0 ] && [ -n "$TRAVIS" ]; then - cat log/test.log - cat log/proxy.log - fi - kill "$server_pid" - kill "$proxy_pid" - } - trap cleanup INT EXIT - export LIVE="${scheme}://localhost:${port}" - export LIVE_PROXY="http://faraday%40test.local:there%20is%20cake@localhost:${proxy_port}" -fi - -warnings="${TMPDIR:-/tmp}/faraday-warnings.$$" - -run_test_files() { - # Save warnings on stderr to a separate file - RUBYOPT="$RUBYOPT -w" ruby -e 'while f=ARGV.shift and f!="--"; load f; end' "${test_files[@]}" -- "$@" \ - 2> >(tee >(grep 'warning:' >"$warnings") | grep -v 'warning:') -} - -check_warnings() { - # Display Ruby warnings from this project's source files. Abort if any were found. - num="$(grep -F "$PWD" "$warnings" | grep -v "${PWD}/vendor/bundle" | sort | uniq -c | sort -rn | tee /dev/stderr | wc -l)" - rm -f "$warnings" - if [ "$num" -gt 0 ]; then - echo "FAILED: this test suite doesn't tolerate Ruby syntax warnings!" >&2 - exit 1 - fi -} - -if [ -n "$RBENV_VERSIONS" ]; then - IFS=' ' versions=($RBENV_VERSIONS) - for version in "${versions[@]}"; do - echo "[${version}]" - RBENV_VERSION="$version" run_test_files "$@" - done -else - run_test_files "$@" -fi - -check_warnings diff --git a/spec/faraday/adapter/em_http_spec.rb b/spec/faraday/adapter/em_http_spec.rb deleted file mode 100644 index 41bf8e2af..000000000 --- a/spec/faraday/adapter/em_http_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Adapter::EMHttp do - features :request_body_on_query_methods, :reason_phrase_parse, :trace_method, - :skip_response_body_on_head, :parallel, :local_socket_binding - - it_behaves_like 'an adapter' - - it 'allows to provide adapter specific configs' do - url = URI('https://example.com:1234') - adapter = described_class.new nil, inactivity_timeout: 20 - req = adapter.create_request(url: url, request: {}) - - expect(req.connopts.inactivity_timeout).to eq(20) - end - - context 'Options' do - let(:request) { Faraday::RequestOptions.new } - let(:env) { { request: request } } - let(:options) { {} } - let(:adapter) { Faraday::Adapter::EMHttp.new } - - it 'configures timeout' do - request.timeout = 5 - adapter.configure_timeout(options, env) - expect(options[:inactivity_timeout]).to eq(5) - expect(options[:connect_timeout]).to eq(5) - end - - it 'configures timeout and open_timeout' do - request.timeout = 5 - request.open_timeout = 1 - adapter.configure_timeout(options, env) - expect(options[:inactivity_timeout]).to eq(5) - expect(options[:connect_timeout]).to eq(1) - end - - it 'configures all timeout settings' do - request.timeout = 5 - request.read_timeout = 3 - request.open_timeout = 1 - adapter.configure_timeout(options, env) - expect(options[:inactivity_timeout]).to eq(3) - expect(options[:connect_timeout]).to eq(1) - end - end -end diff --git a/spec/faraday/adapter/em_synchrony_spec.rb b/spec/faraday/adapter/em_synchrony_spec.rb deleted file mode 100644 index 223ccf644..000000000 --- a/spec/faraday/adapter/em_synchrony_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Adapter::EMSynchrony do - features :request_body_on_query_methods, :reason_phrase_parse, - :skip_response_body_on_head, :parallel, :local_socket_binding - - it_behaves_like 'an adapter' - - it 'allows to provide adapter specific configs' do - url = URI('https://example.com:1234') - adapter = described_class.new nil, inactivity_timeout: 20 - req = adapter.create_request(url: url, request: {}) - - expect(req.connopts.inactivity_timeout).to eq(20) - end -end diff --git a/spec/faraday/adapter/excon_spec.rb b/spec/faraday/adapter/excon_spec.rb deleted file mode 100644 index d80abc376..000000000 --- a/spec/faraday/adapter/excon_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Adapter::Excon do - features :request_body_on_query_methods, :reason_phrase_parse, :trace_method - - it_behaves_like 'an adapter' - - it 'allows to provide adapter specific configs' do - url = URI('https://example.com:1234') - - adapter = described_class.new(nil, debug_request: true) - - conn = adapter.build_connection(url: url) - - expect(conn.data[:debug_request]).to be_truthy - end - - context 'config' do - let(:adapter) { Faraday::Adapter::Excon.new } - let(:request) { Faraday::RequestOptions.new } - let(:uri) { URI.parse('https://example.com') } - let(:env) { { request: request, url: uri } } - - it 'sets timeout' do - request.timeout = 5 - options = adapter.send(:opts_from_env, env) - expect(options[:read_timeout]).to eq(5) - expect(options[:write_timeout]).to eq(5) - expect(options[:connect_timeout]).to eq(5) - end - - it 'sets timeout and open_timeout' do - request.timeout = 5 - request.open_timeout = 3 - options = adapter.send(:opts_from_env, env) - expect(options[:read_timeout]).to eq(5) - expect(options[:write_timeout]).to eq(5) - expect(options[:connect_timeout]).to eq(3) - end - - it 'sets open_timeout' do - request.open_timeout = 3 - options = adapter.send(:opts_from_env, env) - expect(options[:read_timeout]).to eq(nil) - expect(options[:write_timeout]).to eq(nil) - expect(options[:connect_timeout]).to eq(3) - end - end -end diff --git a/spec/faraday/adapter/httpclient_spec.rb b/spec/faraday/adapter/httpclient_spec.rb deleted file mode 100644 index 3cbf2c624..000000000 --- a/spec/faraday/adapter/httpclient_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Adapter::HTTPClient do - # ruby gem defaults for testing purposes - HTTPCLIENT_OPEN = 60 - HTTPCLIENT_READ = 60 - HTTPCLIENT_WRITE = 120 - - features :request_body_on_query_methods, :reason_phrase_parse, :compression, - :trace_method, :local_socket_binding - - it_behaves_like 'an adapter' - - it 'allows to provide adapter specific configs' do - adapter = described_class.new do |client| - client.keep_alive_timeout = 20 - client.ssl_config.timeout = 25 - end - - client = adapter.build_connection(url: URI.parse('https://example.com')) - expect(client.keep_alive_timeout).to eq(20) - expect(client.ssl_config.timeout).to eq(25) - end - - context 'Options' do - let(:request) { Faraday::RequestOptions.new } - let(:env) { { request: request } } - let(:options) { {} } - let(:adapter) { Faraday::Adapter::HTTPClient.new } - let(:client) { adapter.connection(url: URI.parse('https://example.com')) } - - it 'configures timeout' do - assert_default_timeouts! - - request.timeout = 5 - adapter.configure_timeouts(client, request) - - expect(client.connect_timeout).to eq(5) - expect(client.send_timeout).to eq(5) - expect(client.receive_timeout).to eq(5) - end - - it 'configures open timeout' do - assert_default_timeouts! - - request.open_timeout = 1 - adapter.configure_timeouts(client, request) - - expect(client.connect_timeout).to eq(1) - expect(client.send_timeout).to eq(HTTPCLIENT_WRITE) - expect(client.receive_timeout).to eq(HTTPCLIENT_READ) - end - - it 'configures multiple timeouts' do - assert_default_timeouts! - - request.open_timeout = 1 - request.write_timeout = 10 - request.read_timeout = 5 - adapter.configure_timeouts(client, request) - - expect(client.connect_timeout).to eq(1) - expect(client.send_timeout).to eq(10) - expect(client.receive_timeout).to eq(5) - end - - def assert_default_timeouts! - expect(client.connect_timeout).to eq(HTTPCLIENT_OPEN) - expect(client.send_timeout).to eq(HTTPCLIENT_WRITE) - expect(client.receive_timeout).to eq(HTTPCLIENT_READ) - end - end -end diff --git a/spec/faraday/adapter/net_http_persistent_spec.rb b/spec/faraday/adapter/net_http_persistent_spec.rb deleted file mode 100644 index 3a35e0a56..000000000 --- a/spec/faraday/adapter/net_http_persistent_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Adapter::NetHttpPersistent do - features :request_body_on_query_methods, :reason_phrase_parse, :compression, :trace_method - - it_behaves_like 'an adapter' - - it 'allows to provide adapter specific configs' do - url = URI('https://example.com') - - adapter = described_class.new do |http| - http.idle_timeout = 123 - end - - http = adapter.send(:connection, url: url, request: {}) - adapter.send(:configure_request, http, {}) - - expect(http.idle_timeout).to eq(123) - end - - it 'sets max_retries to 0' do - url = URI('http://example.com') - - adapter = described_class.new - - http = adapter.send(:connection, url: url, request: {}) - adapter.send(:configure_request, http, {}) - - # `max_retries=` is only present in Ruby 2.5 - expect(http.max_retries).to eq(0) if http.respond_to?(:max_retries=) - end - - it 'allows to set pool_size on initialize' do - url = URI('https://example.com') - - adapter = described_class.new(nil, pool_size: 5) - - http = adapter.send(:connection, url: url, request: {}) - - # `pool` is only present in net_http_persistent >= 3.0 - expect(http.pool.size).to eq(5) if http.respond_to?(:pool) - end - - context 'min_version' do - it 'allows to set min_version in SSL settings' do - url = URI('https://example.com') - - adapter = described_class.new(nil) - - http = adapter.send(:connection, url: url, request: {}) - adapter.send(:configure_ssl, http, min_version: :TLS1_2) - - # `min_version` is only present in net_http_persistent >= 3.1 (UNRELEASED) - expect(http.min_version).to eq(:TLS1_2) if http.respond_to?(:min_version) - end - end -end diff --git a/spec/faraday/adapter/net_http_spec.rb b/spec/faraday/adapter/net_http_spec.rb deleted file mode 100644 index fda9b64d5..000000000 --- a/spec/faraday/adapter/net_http_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Adapter::NetHttp do - features :request_body_on_query_methods, :reason_phrase_parse, :compression, :streaming, :trace_method - - it_behaves_like 'an adapter' - - context 'checking http' do - let(:url) { URI('http://example.com') } - let(:adapter) { described_class.new } - let(:http) { adapter.send(:connection, url: url, request: {}) } - - it { expect(http.port).to eq(80) } - - it 'sets max_retries to 0' do - adapter.send(:configure_request, http, {}) - - expect(http.max_retries).to eq(0) if http.respond_to?(:max_retries=) - end - - it 'supports write_timeout' do - adapter.send(:configure_request, http, write_timeout: 10) - - expect(http.write_timeout).to eq(10) if http.respond_to?(:write_timeout=) - end - - it 'supports open_timeout' do - adapter.send(:configure_request, http, open_timeout: 10) - - expect(http.open_timeout).to eq(10) - end - - it 'supports read_timeout' do - adapter.send(:configure_request, http, read_timeout: 10) - - expect(http.read_timeout).to eq(10) - end - - context 'with https url' do - let(:url) { URI('https://example.com') } - - it { expect(http.port).to eq(443) } - end - - context 'with http url including port' do - let(:url) { URI('https://example.com:1234') } - - it { expect(http.port).to eq(1234) } - end - - context 'with custom adapter config' do - let(:adapter) do - described_class.new do |http| - http.continue_timeout = 123 - end - end - - it do - adapter.send(:configure_request, http, {}) - expect(http.continue_timeout).to eq(123) - end - end - end -end diff --git a/spec/faraday/adapter/patron_spec.rb b/spec/faraday/adapter/patron_spec.rb deleted file mode 100644 index 812fd1a06..000000000 --- a/spec/faraday/adapter/patron_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Adapter::Patron do - features :reason_phrase_parse - - it_behaves_like 'an adapter' - - it 'allows to provide adapter specific configs' do - conn = Faraday.new do |f| - f.adapter :patron do |session| - session.max_redirects = 10 - raise 'Configuration block called' - end - end - - expect { conn.get('/') }.to raise_error(RuntimeError, 'Configuration block called') - end -end diff --git a/spec/faraday/adapter/rack_spec.rb b/spec/faraday/adapter/rack_spec.rb deleted file mode 100644 index 4fe6cba28..000000000 --- a/spec/faraday/adapter/rack_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Adapter::Rack do - features :request_body_on_query_methods, :trace_method, - :skip_response_body_on_head - - it_behaves_like 'an adapter', adapter_options: WebmockRackApp.new -end diff --git a/spec/faraday/adapter/test_spec.rb b/spec/faraday/adapter/test_spec.rb new file mode 100644 index 000000000..117bb7899 --- /dev/null +++ b/spec/faraday/adapter/test_spec.rb @@ -0,0 +1,442 @@ +# frozen_string_literal: true + +RSpec.describe Faraday::Adapter::Test do + let(:stubs) do + described_class::Stubs.new do |stub| + stub.get('http://domain.test/hello') do + [200, { 'Content-Type' => 'text/html' }, 'domain: hello'] + end + + stub.get('http://wrong.test/hello') do + [200, { 'Content-Type' => 'text/html' }, 'wrong: hello'] + end + + stub.get('http://wrong.test/bait') do + [404, { 'Content-Type' => 'text/html' }] + end + + stub.get('/hello') do + [200, { 'Content-Type' => 'text/html' }, 'hello'] + end + + stub.get('/method-echo') do |env| + [200, { 'Content-Type' => 'text/html' }, env[:method].to_s] + end + + stub.get(%r{\A/resources/\d+(?:\?|\z)}) do + [200, { 'Content-Type' => 'text/html' }, 'show'] + end + + stub.get(%r{\A/resources/(specified)\z}) do |_env, meta| + [200, { 'Content-Type' => 'text/html' }, "show #{meta[:match_data][1]}"] + end + end + end + + let(:connection) do + Faraday.new do |builder| + builder.adapter :test, stubs + end + end + + let(:response) { connection.get('/hello') } + + context 'with simple path sets status' do + subject { response.status } + + it { is_expected.to eq 200 } + end + + context 'with simple path sets headers' do + subject { response.headers['Content-Type'] } + + it { is_expected.to eq 'text/html' } + end + + context 'with simple path sets body' do + subject { response.body } + + it { is_expected.to eq 'hello' } + end + + context 'with host points to the right stub' do + subject { connection.get('http://domain.test/hello').body } + + it { is_expected.to eq 'domain: hello' } + end + + describe 'can be called several times' do + subject { connection.get('/hello').body } + + it { is_expected.to eq 'hello' } + end + + describe 'can handle regular expression path' do + subject { connection.get('/resources/1').body } + + it { is_expected.to eq 'show' } + end + + describe 'can handle single parameter block' do + subject { connection.get('/method-echo').body } + + it { is_expected.to eq 'get' } + end + + describe 'can handle regular expression path with captured result' do + subject { connection.get('/resources/specified').body } + + it { is_expected.to eq 'show specified' } + end + + context 'with get params' do + subject { connection.get('/param?a=1').body } + + before do + stubs.get('/param?a=1') { [200, {}, 'a'] } + end + + it { is_expected.to eq 'a' } + end + + describe 'ignoring unspecified get params' do + before do + stubs.get('/optional?a=1') { [200, {}, 'a'] } + end + + context 'with multiple params' do + subject { connection.get('/optional?a=1&b=1').body } + + it { is_expected.to eq 'a' } + end + + context 'with single param' do + subject { connection.get('/optional?a=1').body } + + it { is_expected.to eq 'a' } + end + + context 'without params' do + subject(:request) { connection.get('/optional') } + + it do + expect { request }.to raise_error( + Faraday::Adapter::Test::Stubs::NotFound + ) + end + end + end + + context 'with http headers' do + before do + stubs.get('/yo', 'X-HELLO' => 'hello') { [200, {}, 'a'] } + stubs.get('/yo') { [200, {}, 'b'] } + end + + context 'with header' do + subject do + connection.get('/yo') { |env| env.headers['X-HELLO'] = 'hello' }.body + end + + it { is_expected.to eq 'a' } + end + + context 'without header' do + subject do + connection.get('/yo').body + end + + it { is_expected.to eq 'b' } + end + end + + describe 'different outcomes for the same request' do + def make_request + connection.get('/foo') + end + + subject(:request) { make_request.body } + + before do + stubs.get('/foo') { [200, { 'Content-Type' => 'text/html' }, 'hello'] } + stubs.get('/foo') { [200, { 'Content-Type' => 'text/html' }, 'world'] } + end + + context 'the first request' do + it { is_expected.to eq 'hello' } + end + + context 'the second request' do + before do + make_request + end + + it { is_expected.to eq 'world' } + end + end + + describe 'yielding env to stubs' do + subject { connection.get('http://foo.com/foo?a=1').body } + + before do + stubs.get '/foo' do |env| + expect(env[:url].path).to eq '/foo' + expect(env[:url].host).to eq 'foo.com' + expect(env[:params]['a']).to eq '1' + expect(env[:request_headers]['Accept']).to eq 'text/plain' + [200, {}, 'a'] + end + + connection.headers['Accept'] = 'text/plain' + end + + it { is_expected.to eq 'a' } + end + + describe 'params parsing' do + subject { connection.get('http://foo.com/foo?a[b]=1').body } + + context 'with default encoder' do + before do + stubs.get '/foo' do |env| + expect(env[:params]['a']['b']).to eq '1' + [200, {}, 'a'] + end + end + + it { is_expected.to eq 'a' } + end + + context 'with nested encoder' do + before do + stubs.get '/foo' do |env| + expect(env[:params]['a']['b']).to eq '1' + [200, {}, 'a'] + end + + connection.options.params_encoder = Faraday::NestedParamsEncoder + end + + it { is_expected.to eq 'a' } + end + + context 'with flat encoder' do + before do + stubs.get '/foo' do |env| + expect(env[:params]['a[b]']).to eq '1' + [200, {}, 'a'] + end + + connection.options.params_encoder = Faraday::FlatParamsEncoder + end + + it { is_expected.to eq 'a' } + end + end + + describe 'raising an error if no stub was found' do + describe 'for request' do + subject(:request) { connection.get('/invalid') { [200, {}, []] } } + + it { expect { request }.to raise_error described_class::Stubs::NotFound } + end + + describe 'for specified host' do + subject(:request) { connection.get('http://domain.test/bait') } + + it { expect { request }.to raise_error described_class::Stubs::NotFound } + end + + describe 'for request without specified header' do + subject(:request) { connection.get('/yo') } + + before do + stubs.get('/yo', 'X-HELLO' => 'hello') { [200, {}, 'a'] } + end + + it { expect { request }.to raise_error described_class::Stubs::NotFound } + end + end + + describe 'for request with non default params encoder' do + let(:connection) do + Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) do |builder| + builder.adapter :test, stubs + end + end + let(:stubs) do + described_class::Stubs.new do |stubs| + stubs.get('/path?a=x&a=y&a=z') { [200, {}, 'a'] } + end + end + + context 'when all flat param values are correctly set' do + subject(:request) { connection.get('/path?a=x&a=y&a=z') } + + it { expect(request.status).to eq 200 } + end + + shared_examples 'raise NotFound when params do not satisfy the flat param values' do |params| + subject(:request) { connection.get('/path', params) } + + context "with #{params.inspect}" do + it { expect { request }.to raise_error described_class::Stubs::NotFound } + end + end + + it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { a: %w[x] } + it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { a: %w[x y] } + it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { a: %w[x z y] } # NOTE: The order of the value is also compared. + it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { b: %w[x y z] } + end + + describe 'strict_mode' do + let(:stubs) do + described_class::Stubs.new(strict_mode: true) do |stubs| + stubs.get('/strict?a=12&b=xy', 'Authorization' => 'Bearer m_ck', 'X-C' => 'hello') { [200, {}, 'a'] } + stubs.get('/with_user_agent?a=12&b=xy', authorization: 'Bearer m_ck', 'User-Agent' => 'My Agent') { [200, {}, 'a'] } + end + end + + context 'when params and headers are exactly set' do + subject(:request) { connection.get('/strict', { a: '12', b: 'xy' }, { authorization: 'Bearer m_ck', x_c: 'hello' }) } + + it { expect(request.status).to eq 200 } + end + + context 'when params and headers are exactly set with a custom user agent' do + subject(:request) { connection.get('/with_user_agent', { a: '12', b: 'xy' }, { authorization: 'Bearer m_ck', 'User-Agent' => 'My Agent' }) } + + it { expect(request.status).to eq 200 } + end + + shared_examples 'raise NotFound when params do not satisfy the strict check' do |params| + subject(:request) { connection.get('/strict', params, { 'Authorization' => 'Bearer m_ck', 'X-C' => 'hello' }) } + + context "with #{params.inspect}" do + it { expect { request }.to raise_error described_class::Stubs::NotFound } + end + end + + it_behaves_like 'raise NotFound when params do not satisfy the strict check', { a: '12' } + it_behaves_like 'raise NotFound when params do not satisfy the strict check', { b: 'xy' } + it_behaves_like 'raise NotFound when params do not satisfy the strict check', { a: '123', b: 'xy' } + it_behaves_like 'raise NotFound when params do not satisfy the strict check', { a: '12', b: 'xyz' } + it_behaves_like 'raise NotFound when params do not satisfy the strict check', { a: '12', b: 'xy', c: 'hello' } + it_behaves_like 'raise NotFound when params do not satisfy the strict check', { additional: 'special', a: '12', b: 'xy', c: 'hello' } + + shared_examples 'raise NotFound when headers do not satisfy the strict check' do |path, headers| + subject(:request) { connection.get(path, { a: 12, b: 'xy' }, headers) } + + context "with #{headers.inspect}" do + it { expect { request }.to raise_error described_class::Stubs::NotFound } + end + end + + it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck' } + it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { 'X-C' => 'hello' } + it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'Hi' } + it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { authorization: 'Basic m_ck', 'x-c': 'hello' } + it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'hello', x_special: 'special' } + it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck' } + it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'Unknown' } + it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'My Agent', x_special: 'special' } + + context 'when strict_mode is disabled' do + before do + stubs.strict_mode = false + end + + shared_examples 'does not raise NotFound even when params do not satisfy the strict check' do |params| + subject(:request) { connection.get('/strict', params, { 'Authorization' => 'Bearer m_ck', 'X-C' => 'hello' }) } + + context "with #{params.inspect}" do + it { expect(request.status).to eq 200 } + end + end + + it_behaves_like 'does not raise NotFound even when params do not satisfy the strict check', { a: '12', b: 'xy' } + it_behaves_like 'does not raise NotFound even when params do not satisfy the strict check', { a: '12', b: 'xy', c: 'hello' } + it_behaves_like 'does not raise NotFound even when params do not satisfy the strict check', { additional: 'special', a: '12', b: 'xy', c: 'hello' } + + shared_examples 'does not raise NotFound even when headers do not satisfy the strict check' do |path, headers| + subject(:request) { connection.get(path, { a: 12, b: 'xy' }, headers) } + + context "with #{headers.inspect}" do + it { expect(request.status).to eq 200 } + end + end + + it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'hello' } + it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'hello', x_special: 'special' } + it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'hello', user_agent: 'Special Agent' } + it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'My Agent' } + it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'My Agent', x_special: 'special' } + end + + describe 'body_match?' do + let(:stubs) do + described_class::Stubs.new do |stubs| + stubs.post('/no_check') { [200, {}, 'ok'] } + stubs.post('/with_string', 'abc') { [200, {}, 'ok'] } + stubs.post( + '/with_proc', + ->(request_body) { JSON.parse(request_body, symbolize_names: true) == { x: '!', a: [{ m: [{ a: true }], n: 123 }] } }, + { content_type: 'application/json' } + ) do + [200, {}, 'ok'] + end + end + end + + context 'when trying without any args for body' do + subject(:without_body) { connection.post('/no_check') } + + it { expect(without_body.status).to eq 200 } + end + + context 'when trying with string body stubs' do + subject(:with_string) { connection.post('/with_string', 'abc') } + + it { expect(with_string.status).to eq 200 } + end + + context 'when trying with proc body stubs' do + subject(:with_proc) do + connection.post('/with_proc', JSON.dump(a: [{ n: 123, m: [{ a: true }] }], x: '!'), { 'Content-Type' => 'application/json' }) + end + + it { expect(with_proc.status).to eq 200 } + end + end + end + + describe 'request timeout' do + subject(:request) do + connection.get('/sleep') do |req| + req.options.timeout = timeout + end + end + + before do + stubs.get('/sleep') do + sleep(0.01) + [200, {}, ''] + end + end + + context 'when request is within timeout' do + let(:timeout) { 1 } + + it { expect(request.status).to eq 200 } + end + + context 'when request is too slow' do + let(:timeout) { 0.001 } + + it 'raises an exception' do + expect { request }.to raise_error(Faraday::TimeoutError) + end + end + end +end diff --git a/spec/faraday/adapter/typhoeus_spec.rb b/spec/faraday/adapter/typhoeus_spec.rb deleted file mode 100644 index 7f63f9700..000000000 --- a/spec/faraday/adapter/typhoeus_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Adapter::Typhoeus do - features :request_body_on_query_methods, :parallel, :trace_method - - it_behaves_like 'an adapter' -end diff --git a/spec/faraday/composite_read_io_spec.rb b/spec/faraday/composite_read_io_spec.rb deleted file mode 100644 index ccba34f3d..000000000 --- a/spec/faraday/composite_read_io_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'stringio' - -RSpec.describe Faraday::CompositeReadIO do - Part = Struct.new(:to_io) do - def length - to_io.string.length - end - end - - def part(str) - Part.new StringIO.new(str) - end - - def composite_io(*parts) - Faraday::CompositeReadIO.new(*parts) - end - - context 'with empty composite_io' do - subject { composite_io } - - it { expect(subject.length).to eq(0) } - it { expect(subject.read).to eq('') } - it { expect(subject.read(1)).to be_nil } - end - - context 'with empty parts' do - subject { composite_io(part(''), part('')) } - - it { expect(subject.length).to eq(0) } - it { expect(subject.read).to eq('') } - it { expect(subject.read(1)).to be_nil } - end - - context 'with 2 parts' do - subject { composite_io(part('abcd'), part('1234')) } - - it { expect(subject.length).to eq(8) } - it { expect(subject.read).to eq('abcd1234') } - it 'allows to read in chunks' do - expect(subject.read(3)).to eq('abc') - expect(subject.read(3)).to eq('d12') - expect(subject.read(3)).to eq('34') - expect(subject.read(3)).to be_nil - end - it 'allows to rewind while reading in chunks' do - expect(subject.read(3)).to eq('abc') - expect(subject.read(3)).to eq('d12') - subject.rewind - expect(subject.read(3)).to eq('abc') - expect(subject.read(5)).to eq('d1234') - expect(subject.read(3)).to be_nil - subject.rewind - expect(subject.read(2)).to eq('ab') - end - end - - context 'with mix of empty and non-empty parts' do - subject { composite_io(part(''), part('abcd'), part(''), part('1234'), part('')) } - - it 'allows to read in chunks' do - expect(subject.read(6)).to eq('abcd12') - expect(subject.read(6)).to eq('34') - expect(subject.read(6)).to be_nil - end - end - - context 'with utf8 multibyte part' do - subject { composite_io(part("\x86"), part('ファイル')) } - - it { expect(subject.read).to eq(String.new("\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB", encoding: 'BINARY')) } - it 'allows to read in chunks' do - expect(subject.read(3)).to eq(String.new("\x86\xE3\x83", encoding: 'BINARY')) - expect(subject.read(3)).to eq(String.new("\x95\xE3\x82", encoding: 'BINARY')) - expect(subject.read(8)).to eq(String.new("\xA1\xE3\x82\xA4\xE3\x83\xAB", encoding: 'BINARY')) - expect(subject.read(3)).to be_nil - end - end -end diff --git a/spec/faraday/connection_spec.rb b/spec/faraday/connection_spec.rb index 4f51f5682..7fa726a4c 100644 --- a/spec/faraday/connection_spec.rb +++ b/spec/faraday/connection_spec.rb @@ -1,10 +1,20 @@ # frozen_string_literal: true +class CustomEncoder + def encode(params) + params.map { |k, v| "#{k.upcase}-#{v.to_s.upcase}" }.join(',') + end + + def decode(params) + params.split(',').to_h { |pair| pair.split('-') } + end +end + shared_examples 'initializer with url' do context 'with simple url' do - let(:address) { 'http://sushi.com' } + let(:address) { 'http://httpbingo.org' } - it { expect(subject.host).to eq('sushi.com') } + it { expect(subject.host).to eq('httpbingo.org') } it { expect(subject.port).to eq(80) } it { expect(subject.scheme).to eq('http') } it { expect(subject.path_prefix).to eq('/') } @@ -12,29 +22,36 @@ end context 'with complex url' do - let(:address) { 'http://sushi.com:815/fish?a=1' } + let(:address) { 'http://httpbingo.org:815/fish?a=1' } it { expect(subject.port).to eq(815) } it { expect(subject.path_prefix).to eq('/fish') } it { expect(subject.params).to eq('a' => '1') } end + + context 'with IPv6 address' do + let(:address) { 'http://[::1]:85/' } + + it { expect(subject.host).to eq('[::1]') } + it { expect(subject.port).to eq(85) } + end end shared_examples 'default connection options' do after { Faraday.default_connection_options = nil } it 'works with implicit url' do - conn = Faraday.new 'http://sushi.com/foo' + conn = Faraday.new 'http://httpbingo.org/foo' expect(conn.options.timeout).to eq(10) end it 'works with option url' do - conn = Faraday.new url: 'http://sushi.com/foo' + conn = Faraday.new url: 'http://httpbingo.org/foo' expect(conn.options.timeout).to eq(10) end it 'works with instance connection options' do - conn = Faraday.new 'http://sushi.com/foo', request: { open_timeout: 1 } + conn = Faraday.new 'http://httpbingo.org/foo', request: { open_timeout: 1 } expect(conn.options.timeout).to eq(10) expect(conn.options.open_timeout).to eq(1) end @@ -44,7 +61,7 @@ conn.options.timeout = 1 expect(Faraday.default_connection_options.request.timeout).to eq(10) - other = Faraday.new url: 'https://sushi.com/foo' + other = Faraday.new url: 'https://httpbingo.org/foo' other.options.timeout = 1 expect(Faraday.default_connection_options.request.timeout).to eq(10) @@ -64,14 +81,14 @@ subject { conn } context 'with implicit url param' do - # Faraday::Connection.new('http://sushi.com') + # Faraday::Connection.new('http://httpbingo.org') let(:url) { address } it_behaves_like 'initializer with url' end context 'with explicit url param' do - # Faraday::Connection.new(url: 'http://sushi.com') + # Faraday::Connection.new(url: 'http://httpbingo.org') let(:url) { { url: address } } it_behaves_like 'initializer with url' @@ -91,11 +108,17 @@ end context 'with custom params and params in url' do - let(:url) { 'http://sushi.com/fish?a=1&b=2' } + let(:url) { 'http://httpbingo.org/fish?a=1&b=2' } let(:options) { { params: { a: 3 } } } it { expect(subject.params).to eq('a' => 3, 'b' => '2') } end + context 'with basic_auth in url' do + let(:url) { 'http://Aladdin:open%20sesame@httpbingo.org/fish' } + + it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') } + end + context 'with custom headers' do let(:options) { { headers: { user_agent: 'Faraday' } } } @@ -108,6 +131,12 @@ it { expect(subject.ssl.verify?).to be_falsey } end + context 'with verify_hostname false' do + let(:options) { { ssl: { verify_hostname: false } } } + + it { expect(subject.ssl.verify_hostname?).to be_falsey } + end + context 'with empty block' do let(:conn) { Faraday::Connection.new {} } @@ -117,8 +146,8 @@ context 'with block' do let(:conn) do Faraday::Connection.new(params: { 'a' => '1' }) do |faraday| - faraday.adapter :net_http - faraday.url_prefix = 'http://sushi.com/omnom' + faraday.adapter :test + faraday.url_prefix = 'http://httpbingo.org/omnom' end end @@ -128,41 +157,22 @@ end describe '#close' do + before { Faraday.default_adapter = :test } + after { Faraday.default_adapter = nil } + it 'can close underlying app' do expect(conn.app).to receive(:close) conn.close end end - describe 'basic_auth' do - subject { conn } - - context 'calling the #basic_auth method' do - before { subject.basic_auth 'Aladdin', 'open sesame' } - - it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') } - end - - context 'adding basic auth info to url' do - let(:url) { 'http://Aladdin:open%20sesame@sushi.com/fish' } - - it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') } - end - end - - describe '#token_auth' do - before { subject.token_auth('abcdef', nonce: 'abc') } - - it { expect(subject.headers['Authorization']).to eq('Token nonce="abc", token="abcdef"') } - end - describe '#build_exclusive_url' do context 'with relative path' do subject { conn.build_exclusive_url('sake.html') } it 'uses connection host as default host' do - conn.host = 'sushi.com' - expect(subject.host).to eq('sushi.com') + conn.host = 'httpbingo.org' + expect(subject.host).to eq('httpbingo.org') expect(subject.scheme).to eq('http') end @@ -199,10 +209,10 @@ end context 'with complete url' do - subject { conn.build_exclusive_url('http://sushi.com/sake.html?a=1') } + subject { conn.build_exclusive_url('http://httpbingo.org/sake.html?a=1') } it { expect(subject.scheme).to eq('http') } - it { expect(subject.host).to eq('sushi.com') } + it { expect(subject.host).to eq('httpbingo.org') } it { expect(subject.port).to eq(80) } it { expect(subject.path).to eq('/sake.html') } it { expect(subject.query).to eq('a=1') } @@ -210,35 +220,35 @@ it 'overrides connection port for absolute url' do conn.port = 23 - uri = conn.build_exclusive_url('http://sushi.com') + uri = conn.build_exclusive_url('http://httpbingo.org') expect(uri.port).to eq(80) end it 'does not add ending slash given nil url' do - conn.url_prefix = 'http://sushi.com/nigiri' + conn.url_prefix = 'http://httpbingo.org/nigiri' uri = conn.build_exclusive_url expect(uri.path).to eq('/nigiri') end it 'does not add ending slash given empty url' do - conn.url_prefix = 'http://sushi.com/nigiri' + conn.url_prefix = 'http://httpbingo.org/nigiri' uri = conn.build_exclusive_url('') expect(uri.path).to eq('/nigiri') end it 'does not use connection params' do - conn.url_prefix = 'http://sushi.com/nigiri' + conn.url_prefix = 'http://httpbingo.org/nigiri' conn.params = { a: 1 } - expect(conn.build_exclusive_url.to_s).to eq('http://sushi.com/nigiri') + expect(conn.build_exclusive_url.to_s).to eq('http://httpbingo.org/nigiri') end it 'allows to provide params argument' do - conn.url_prefix = 'http://sushi.com/nigiri' + conn.url_prefix = 'http://httpbingo.org/nigiri' conn.params = { a: 1 } params = Faraday::Utils::ParamsHash.new params[:a] = 2 uri = conn.build_exclusive_url(nil, params) - expect(uri.to_s).to eq('http://sushi.com/nigiri?a=2') + expect(uri.to_s).to eq('http://httpbingo.org/nigiri?a=2') end it 'handles uri instances' do @@ -246,49 +256,94 @@ expect(uri.path).to eq('/sake.html') end + it 'always returns new URI instance' do + conn.url_prefix = 'http://httpbingo.org' + uri1 = conn.build_exclusive_url(nil) + uri2 = conn.build_exclusive_url(nil) + expect(uri1).not_to equal(uri2) + end + context 'with url_prefixed connection' do - let(:url) { 'http://sushi.com/sushi/' } + let(:url) { 'http://httpbingo.org/get/' } it 'parses url and changes scheme' do conn.scheme = 'https' uri = conn.build_exclusive_url('sake.html') - expect(uri.to_s).to eq('https://sushi.com/sushi/sake.html') + expect(uri.to_s).to eq('https://httpbingo.org/get/sake.html') end it 'joins url to base with ending slash' do uri = conn.build_exclusive_url('sake.html') - expect(uri.to_s).to eq('http://sushi.com/sushi/sake.html') + expect(uri.to_s).to eq('http://httpbingo.org/get/sake.html') end it 'used default base with ending slash' do uri = conn.build_exclusive_url - expect(uri.to_s).to eq('http://sushi.com/sushi/') + expect(uri.to_s).to eq('http://httpbingo.org/get/') end it 'overrides base' do uri = conn.build_exclusive_url('/sake/') - expect(uri.to_s).to eq('http://sushi.com/sake/') + expect(uri.to_s).to eq('http://httpbingo.org/sake/') + end + end + + context 'with colon in path' do + let(:url) { 'http://service.com' } + + it 'joins url to base when used absolute path' do + conn = Faraday.new(url: url) + uri = conn.build_exclusive_url('/service:search?limit=400') + expect(uri.to_s).to eq('http://service.com/service:search?limit=400') + end + + it 'joins url to base when used relative path' do + conn = Faraday.new(url: url) + uri = conn.build_exclusive_url('service:search?limit=400') + expect(uri.to_s).to eq('http://service.com/service:search?limit=400') + end + + it 'joins url to base when used with path prefix' do + conn = Faraday.new(url: url) + conn.path_prefix = '/api' + uri = conn.build_exclusive_url('service:search?limit=400') + expect(uri.to_s).to eq('http://service.com/api/service:search?limit=400') + end + end + + context 'with a custom `default_uri_parser`' do + let(:url) { 'http://httpbingo.org' } + let(:parser) { Addressable::URI } + + around do |example| + with_default_uri_parser(parser) do + example.run + end + end + + it 'does not raise error' do + expect { conn.build_exclusive_url('/nigiri') }.not_to raise_error end end end describe '#build_url' do - let(:url) { 'http://sushi.com/nigiri' } + let(:url) { 'http://httpbingo.org/nigiri' } it 'uses params' do conn.params = { a: 1, b: 1 } - expect(conn.build_url.to_s).to eq('http://sushi.com/nigiri?a=1&b=1') + expect(conn.build_url.to_s).to eq('http://httpbingo.org/nigiri?a=1&b=1') end it 'merges params' do conn.params = { a: 1, b: 1 } url = conn.build_url(nil, b: 2, c: 3) - expect(url.to_s).to eq('http://sushi.com/nigiri?a=1&b=2&c=3') + expect(url.to_s).to eq('http://httpbingo.org/nigiri?a=1&b=2&c=3') end end describe '#build_request' do - let(:url) { 'https://asushi.com/sake.html' } + let(:url) { 'https://ahttpbingo.org/sake.html' } let(:request) { conn.build_request(:get) } before do @@ -305,7 +360,7 @@ describe '#to_env' do subject { conn.build_request(:get).to_env(conn).url } - let(:url) { 'http://sushi.com/sake.html' } + let(:url) { 'http://httpbingo.org/sake.html' } let(:options) { { params: @params } } it 'parses url params into query' do @@ -412,6 +467,14 @@ end end + it 'allows when url in no proxy list with url_prefix' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do + conn = Faraday::Connection.new + conn.url_prefix = 'http://example.com' + expect(conn.proxy).to be_nil + end + end + it 'allows when prefixed url is not in no proxy list' do with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do conn = Faraday::Connection.new('http://prefixedexample.com') @@ -463,7 +526,7 @@ it 'uses env http_proxy' do with_env 'http_proxy' => 'http://proxy.com' do conn = Faraday.new - expect(conn.instance_variable_get('@manual_proxy')).to be_falsey + expect(conn.instance_variable_get(:@manual_proxy)).to be_falsey expect(conn.proxy_for_request('http://google.co.uk').host).to eq('proxy.com') end end @@ -471,7 +534,7 @@ it 'uses processes no_proxy before http_proxy' do with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'google.co.uk' do conn = Faraday.new - expect(conn.instance_variable_get('@manual_proxy')).to be_falsey + expect(conn.instance_variable_get(:@manual_proxy)).to be_falsey expect(conn.proxy_for_request('http://google.co.uk')).to be_nil end end @@ -479,7 +542,7 @@ it 'uses env https_proxy' do with_env 'https_proxy' => 'https://proxy.com' do conn = Faraday.new - expect(conn.instance_variable_get('@manual_proxy')).to be_falsey + expect(conn.instance_variable_get(:@manual_proxy)).to be_falsey expect(conn.proxy_for_request('https://google.co.uk').host).to eq('proxy.com') end end @@ -487,7 +550,7 @@ it 'uses processes no_proxy before https_proxy' do with_env 'https_proxy' => 'https://proxy.com', 'no_proxy' => 'google.co.uk' do conn = Faraday.new - expect(conn.instance_variable_get('@manual_proxy')).to be_falsey + expect(conn.instance_variable_get(:@manual_proxy)).to be_falsey expect(conn.proxy_for_request('https://google.co.uk')).to be_nil end end @@ -497,7 +560,7 @@ conn = Faraday.new conn.proxy = 'http://proxy2.com' - expect(conn.instance_variable_get('@manual_proxy')).to be_truthy + expect(conn.instance_variable_get(:@manual_proxy)).to be_truthy expect(conn.proxy_for_request('https://google.co.uk').host).to eq('proxy2.com') end end @@ -511,26 +574,32 @@ end context 'performing a request' do - before { stub_request(:get, 'http://example.com') } + let(:url) { 'http://example.com' } + let(:conn) do + Faraday.new do |f| + f.adapter :test do |stubs| + stubs.get(url) do + [200, {}, 'ok'] + end + end + end + end it 'dynamically checks proxy' do with_env 'http_proxy' => 'http://proxy.com:80' do - conn = Faraday.new expect(conn.proxy.uri.host).to eq('proxy.com') - conn.get('http://example.com') do |req| + conn.get(url) do |req| expect(req.options.proxy.uri.host).to eq('proxy.com') end end - conn.get('http://example.com') - expect(conn.instance_variable_get('@temp_proxy')).to be_nil + conn.get(url) + expect(conn.instance_variable_get(:@temp_proxy)).to be_nil end it 'dynamically check no proxy' do with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do - conn = Faraday.new - expect(conn.proxy.uri.host).to eq('proxy.com') conn.get('http://example.com') do |req| @@ -544,7 +613,7 @@ describe '#dup' do subject { conn.dup } - let(:url) { 'http://sushi.com/foo' } + let(:url) { 'http://httpbingo.org/foo' } let(:options) do { ssl: { verify: :none }, @@ -560,7 +629,6 @@ context 'after manual changes' do before do - subject.basic_auth('', '') subject.headers['content-length'] = 12 subject.params['b'] = '2' subject.options[:open_timeout] = 10 @@ -595,14 +663,42 @@ it_behaves_like 'default connection options' end + + context 'preserving a user_agent assigned via default_conncetion_options' do + around do |example| + old = Faraday.default_connection_options + Faraday.default_connection_options = { headers: { user_agent: 'My Agent 1.2' } } + example.run + Faraday.default_connection_options = old + end + + context 'when url is a Hash' do + let(:conn) { Faraday.new(url: 'http://example.co', headers: { 'CustomHeader' => 'CustomValue' }) } + + it { expect(conn.headers).to eq('CustomHeader' => 'CustomValue', 'User-Agent' => 'My Agent 1.2') } + end + + context 'when url is a String' do + let(:conn) { Faraday.new('http://example.co', headers: { 'CustomHeader' => 'CustomValue' }) } + + it { expect(conn.headers).to eq('CustomHeader' => 'CustomValue', 'User-Agent' => 'My Agent 1.2') } + end + end end describe 'request params' do context 'with simple url' do let(:url) { 'http://example.com' } - let!(:stubbed) { stub_request(:get, 'http://example.com?a=a&p=3') } + let(:stubs) { Faraday::Adapter::Test::Stubs.new } - after { expect(stubbed).to have_been_made.once } + before do + conn.adapter(:test, stubs) + stubs.get('http://example.com?a=a&p=3') do + [200, {}, 'ok'] + end + end + + after { stubs.verify_stubbed_calls } it 'test_overrides_request_params' do conn.get('?p=2&a=a', p: 3) @@ -624,15 +720,22 @@ context 'with url and extra params' do let(:url) { 'http://example.com?a=1&b=2' } let(:options) { { params: { c: 3 } } } + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + + before do + conn.adapter(:test, stubs) + end it 'merges connection and request params' do - stubbed = stub_request(:get, 'http://example.com?a=1&b=2&c=3&limit=5&page=1') + expected = 'http://example.com?a=1&b=2&c=3&limit=5&page=1' + stubs.get(expected) { [200, {}, 'ok'] } conn.get('?page=1', limit: 5) - expect(stubbed).to have_been_made.once + stubs.verify_stubbed_calls end it 'allows to override all params' do - stubbed = stub_request(:get, 'http://example.com?b=b') + expected = 'http://example.com?b=b' + stubs.get(expected) { [200, {}, 'ok'] } conn.get('?p=1&a=a', p: 2) do |req| expect(req.params[:a]).to eq('a') expect(req.params['c']).to eq(3) @@ -640,47 +743,61 @@ req.params = { b: 'b' } expect(req.params['b']).to eq('b') end - expect(stubbed).to have_been_made.once + stubs.verify_stubbed_calls end it 'allows to set params_encoder for single request' do - encoder = Object.new - def encoder.encode(params) - params.map { |k, v| "#{k.upcase}-#{v.to_s.upcase}" }.join(',') - end - stubbed = stub_request(:get, 'http://example.com/?A-1,B-2,C-3,FEELING-BLUE') + encoder = CustomEncoder.new + expected = 'http://example.com/?A-1,B-2,C-3,FEELING-BLUE' + stubs.get(expected) { [200, {}, 'ok'] } - conn.get('/', feeling: 'blue') do |req| + conn.get('/', a: 1, b: 2, c: 3, feeling: 'blue') do |req| req.options.params_encoder = encoder end - expect(stubbed).to have_been_made.once + stubs.verify_stubbed_calls end end context 'with default params encoder' do - let!(:stubbed) { stub_request(:get, 'http://example.com?color%5B%5D=red&color%5B%5D=blue') } - after { expect(stubbed).to have_been_made.once } + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + + before do + conn.adapter(:test, stubs) + stubs.get('http://example.com?color%5B%5D=blue&color%5B%5D=red') do + [200, {}, 'ok'] + end + end + + after { stubs.verify_stubbed_calls } it 'supports array params in url' do - conn.get('http://example.com?color[]=red&color[]=blue') + conn.get('http://example.com?color[]=blue&color[]=red') end it 'supports array params in params' do - conn.get('http://example.com', color: %w[red blue]) + conn.get('http://example.com', color: %w[blue red]) end end context 'with flat params encoder' do let(:options) { { request: { params_encoder: Faraday::FlatParamsEncoder } } } - let!(:stubbed) { stub_request(:get, 'http://example.com?color=blue') } - after { expect(stubbed).to have_been_made.once } + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + + before do + conn.adapter(:test, stubs) + stubs.get('http://example.com?color=blue&color=red') do + [200, {}, 'ok'] + end + end + + after { stubs.verify_stubbed_calls } it 'supports array params in params' do - conn.get('http://example.com', color: %w[red blue]) + conn.get('http://example.com', color: %w[blue red]) end context 'with array param in url' do - let(:url) { 'http://example.com?color[]=red&color[]=blue' } + let(:url) { 'http://example.com?color[]=blue&color[]=red' } it do conn.get('/') diff --git a/spec/faraday/error_spec.rb b/spec/faraday/error_spec.rb index 98a31b84e..fd30e2d34 100644 --- a/spec/faraday/error_spec.rb +++ b/spec/faraday/error_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe Faraday::ClientError do +RSpec.describe Faraday::Error do describe '.initialize' do subject { described_class.new(exception, response) } let(:response) { nil } @@ -12,7 +12,10 @@ it { expect(subject.response).to be_nil } it { expect(subject.message).to eq(exception.message) } it { expect(subject.backtrace).to eq(exception.backtrace) } - it { expect(subject.inspect).to eq('#>') } + it { expect(subject.inspect).to eq('#>') } + it { expect(subject.response_status).to be_nil } + it { expect(subject.response_headers).to be_nil } + it { expect(subject.response_body).to be_nil } end context 'with response hash' do @@ -21,7 +24,10 @@ it { expect(subject.wrapped_exception).to be_nil } it { expect(subject.response).to eq(exception) } it { expect(subject.message).to eq('the server responded with status 400') } - it { expect(subject.inspect).to eq('#400}>') } + it { expect(subject.inspect).to eq('#400}>') } + it { expect(subject.response_status).to eq(400) } + it { expect(subject.response_headers).to be_nil } + it { expect(subject.response_body).to be_nil } end context 'with string' do @@ -30,7 +36,10 @@ it { expect(subject.wrapped_exception).to be_nil } it { expect(subject.response).to be_nil } it { expect(subject.message).to eq('custom message') } - it { expect(subject.inspect).to eq('#>') } + it { expect(subject.inspect).to eq('#>') } + it { expect(subject.response_status).to be_nil } + it { expect(subject.response_headers).to be_nil } + it { expect(subject.response_body).to be_nil } end context 'with anything else #to_s' do @@ -39,7 +48,38 @@ it { expect(subject.wrapped_exception).to be_nil } it { expect(subject.response).to be_nil } it { expect(subject.message).to eq('["error1", "error2"]') } - it { expect(subject.inspect).to eq('#>') } + it { expect(subject.inspect).to eq('#>') } + it { expect(subject.response_status).to be_nil } + it { expect(subject.response_headers).to be_nil } + it { expect(subject.response_body).to be_nil } + end + + context 'with exception string and response hash' do + let(:exception) { 'custom message' } + let(:response) { { status: 400 } } + + it { expect(subject.wrapped_exception).to be_nil } + it { expect(subject.response).to eq(response) } + it { expect(subject.message).to eq('custom message') } + it { expect(subject.inspect).to eq('#400}>') } + it { expect(subject.response_status).to eq(400) } + it { expect(subject.response_headers).to be_nil } + it { expect(subject.response_body).to be_nil } + end + + context 'with exception and response object' do + let(:exception) { RuntimeError.new('test') } + let(:body) { { test: 'test' } } + let(:headers) { { 'Content-Type' => 'application/json' } } + let(:response) { Faraday::Response.new(status: 400, response_headers: headers, response_body: body) } + + it { expect(subject.wrapped_exception).to eq(exception) } + it { expect(subject.response).to eq(response) } + it { expect(subject.message).to eq(exception.message) } + it { expect(subject.backtrace).to eq(exception.backtrace) } + it { expect(subject.response_status).to eq(400) } + it { expect(subject.response_headers).to eq(headers) } + it { expect(subject.response_body).to eq(body) } end end end diff --git a/spec/faraday/middleware_registry_spec.rb b/spec/faraday/middleware_registry_spec.rb new file mode 100644 index 000000000..a8fa7cc79 --- /dev/null +++ b/spec/faraday/middleware_registry_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.describe Faraday::MiddlewareRegistry do + before do + stub_const('CustomMiddleware', custom_middleware_klass) + end + let(:custom_middleware_klass) { Class.new(Faraday::Middleware) } + let(:dummy) { Class.new { extend Faraday::MiddlewareRegistry } } + + after { dummy.unregister_middleware(:custom) } + + it 'allows to register with constant' do + dummy.register_middleware(custom: custom_middleware_klass) + expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass) + end + + it 'allows to register with symbol' do + dummy.register_middleware(custom: :CustomMiddleware) + expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass) + end + + it 'allows to register with string' do + dummy.register_middleware(custom: 'CustomMiddleware') + expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass) + end + + it 'allows to register with Proc' do + dummy.register_middleware(custom: -> { custom_middleware_klass }) + expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass) + end +end diff --git a/spec/faraday/middleware_spec.rb b/spec/faraday/middleware_spec.rb index 0ae9aab33..da1d36858 100644 --- a/spec/faraday/middleware_spec.rb +++ b/spec/faraday/middleware_spec.rb @@ -2,25 +2,212 @@ RSpec.describe Faraday::Middleware do subject { described_class.new(app) } + let(:app) { double } + + describe 'options' do + context 'when options are passed to the middleware' do + subject { described_class.new(app, options) } + let(:options) { { field: 'value' } } + + it 'accepts options when initialized' do + expect(subject.options[:field]).to eq('value') + end + end + end + + describe '#on_request' do + subject do + Class.new(described_class) do + def on_request(env) + # do nothing + end + end.new(app) + end + + it 'is called by #call' do + expect(app).to receive(:call).and_return(app) + expect(app).to receive(:on_complete) + is_expected.to receive(:call).and_call_original + is_expected.to receive(:on_request) + subject.call(double) + end + end + + describe '#on_error' do + subject do + Class.new(described_class) do + def on_error(error) + # do nothing + end + end.new(app) + end + + it 'is called by #call' do + expect(app).to receive(:call).and_raise(Faraday::ConnectionFailed) + is_expected.to receive(:call).and_call_original + is_expected.to receive(:on_error) + + expect { subject.call(double) }.to raise_error(Faraday::ConnectionFailed) + end + end describe '#close' do context "with app that doesn't support \#close" do - let(:app) { double } - it 'should issue warning' do - expect(subject).to receive(:warn) + is_expected.to receive(:warn) subject.close end end context "with app that supports \#close" do - let(:app) { double } - it 'should issue warning' do expect(app).to receive(:close) - expect(subject).to_not receive(:warn) + is_expected.to_not receive(:warn) subject.close end end end + + describe '::default_options' do + let(:subclass_no_options) { FaradayMiddlewareSubclasses::SubclassNoOptions } + let(:subclass_one_option) { FaradayMiddlewareSubclasses::SubclassOneOption } + let(:subclass_two_options) { FaradayMiddlewareSubclasses::SubclassTwoOptions } + + def build_conn(resp_middleware) + Faraday.new do |c| + c.adapter :test do |stub| + stub.get('/success') { [200, {}, 'ok'] } + end + c.response resp_middleware + end + end + + RSpec.shared_context 'reset @default_options' do + before(:each) do + FaradayMiddlewareSubclasses::SubclassNoOptions.instance_variable_set(:@default_options, nil) + FaradayMiddlewareSubclasses::SubclassOneOption.instance_variable_set(:@default_options, nil) + FaradayMiddlewareSubclasses::SubclassTwoOptions.instance_variable_set(:@default_options, nil) + Faraday::Middleware.instance_variable_set(:@default_options, nil) + end + end + + after(:all) do + FaradayMiddlewareSubclasses::SubclassNoOptions.instance_variable_set(:@default_options, nil) + FaradayMiddlewareSubclasses::SubclassOneOption.instance_variable_set(:@default_options, nil) + FaradayMiddlewareSubclasses::SubclassTwoOptions.instance_variable_set(:@default_options, nil) + Faraday::Middleware.instance_variable_set(:@default_options, nil) + end + + context 'with subclass DEFAULT_OPTIONS defined' do + include_context 'reset @default_options' + + context 'and without application options configured' do + let(:resp1) { build_conn(:one_option).get('/success') } + + it 'has only subclass defaults' do + expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS) + expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS) + expect(subclass_one_option.default_options).to eq(subclass_one_option::DEFAULT_OPTIONS) + expect(subclass_two_options.default_options).to eq(subclass_two_options::DEFAULT_OPTIONS) + end + + it { expect(resp1.body).to eq('ok') } + end + + context "and with one application's options changed" do + let(:resp2) { build_conn(:two_options).get('/success') } + + before(:each) do + FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false } + end + + it 'only updates default options of target subclass' do + expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS) + expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS) + expect(subclass_one_option.default_options).to eq(subclass_one_option::DEFAULT_OPTIONS) + expect(subclass_two_options.default_options).to eq({ some_option: false, some_other_option: false }) + end + + it { expect(resp2.body).to eq('ok') } + end + + context "and with two applications' options changed" do + let(:resp1) { build_conn(:one_option).get('/success') } + let(:resp2) { build_conn(:two_options).get('/success') } + + before(:each) do + FaradayMiddlewareSubclasses::SubclassOneOption.default_options = { some_other_option: true } + FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false } + end + + it 'updates subclasses and parent independent of each other' do + expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS) + expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS) + expect(subclass_one_option.default_options).to eq({ some_other_option: true }) + expect(subclass_two_options.default_options).to eq({ some_option: false, some_other_option: false }) + end + + it { expect(resp1.body).to eq('ok') } + it { expect(resp2.body).to eq('ok') } + end + end + + context 'with FARADAY::MIDDLEWARE DEFAULT_OPTIONS and with Subclass DEFAULT_OPTIONS' do + before(:each) do + stub_const('Faraday::Middleware::DEFAULT_OPTIONS', { its_magic: false }) + end + + # Must stub Faraday::Middleware::DEFAULT_OPTIONS before resetting default options + include_context 'reset @default_options' + + context 'and without application options configured' do + let(:resp1) { build_conn(:one_option).get('/success') } + + it 'has only subclass defaults' do + expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS) + expect(FaradayMiddlewareSubclasses::SubclassNoOptions.default_options).to eq({ its_magic: false }) + expect(FaradayMiddlewareSubclasses::SubclassOneOption.default_options).to eq({ its_magic: false, some_other_option: false }) + expect(FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options).to eq({ its_magic: false, some_option: true, some_other_option: false }) + end + + it { expect(resp1.body).to eq('ok') } + end + + context "and with two applications' options changed" do + let(:resp1) { build_conn(:one_option).get('/success') } + let(:resp2) { build_conn(:two_options).get('/success') } + + before(:each) do + FaradayMiddlewareSubclasses::SubclassOneOption.default_options = { some_other_option: true } + FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false } + end + + it 'updates subclasses and parent independent of each other' do + expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS) + expect(FaradayMiddlewareSubclasses::SubclassNoOptions.default_options).to eq({ its_magic: false }) + expect(FaradayMiddlewareSubclasses::SubclassOneOption.default_options).to eq({ its_magic: false, some_other_option: true }) + expect(FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options).to eq({ its_magic: false, some_option: false, some_other_option: false }) + end + + it { expect(resp1.body).to eq('ok') } + it { expect(resp2.body).to eq('ok') } + end + end + + describe 'default_options input validation' do + include_context 'reset @default_options' + + it 'raises error if Faraday::Middleware option does not exist' do + expect { Faraday::Middleware.default_options = { something_special: true } }.to raise_error(Faraday::InitializationError) do |e| + expect(e.message).to eq('Invalid options provided. Keys not found in Faraday::Middleware::DEFAULT_OPTIONS: something_special') + end + end + + it 'raises error if subclass option does not exist' do + expect { subclass_one_option.default_options = { this_is_a_typo: true } }.to raise_error(Faraday::InitializationError) do |e| + expect(e.message).to eq('Invalid options provided. Keys not found in FaradayMiddlewareSubclasses::SubclassOneOption::DEFAULT_OPTIONS: this_is_a_typo') + end + end + end + end end diff --git a/spec/faraday/options/env_spec.rb b/spec/faraday/options/env_spec.rb index 04a4b5e88..006bd5fb3 100644 --- a/spec/faraday/options/env_spec.rb +++ b/spec/faraday/options/env_spec.rb @@ -27,14 +27,20 @@ expect(ssl.fetch(:verify, true)).to be_falsey end + it 'handle verify_hostname when fetching' do + ssl = Faraday::SSLOptions.new + ssl.verify_hostname = true + expect(ssl.fetch(:verify_hostname, false)).to be_truthy + end + it 'retains custom members' do env[:foo] = 'custom 1' - env[:bar] = :custom_2 + env[:bar] = :custom2 env2 = Faraday::Env.from(env) env2[:baz] = 'custom 3' expect(env2[:foo]).to eq('custom 1') - expect(env2[:bar]).to eq(:custom_2) + expect(env2[:bar]).to eq(:custom2) expect(env[:baz]).to be_nil end diff --git a/spec/faraday/options/options_spec.rb b/spec/faraday/options/options_spec.rb index 9758eccf5..fc0b117a8 100644 --- a/spec/faraday/options/options_spec.rb +++ b/spec/faraday/options/options_spec.rb @@ -2,7 +2,7 @@ RSpec.describe Faraday::Options do SubOptions = Class.new(Faraday::Options.new(:sub_a, :sub_b)) - class ParentOptions < Faraday::Options.new(:a, :b, :c) + ParentOptions = Faraday::Options.new(:a, :b, :c) do options c: SubOptions end diff --git a/spec/faraday/options/proxy_options_spec.rb b/spec/faraday/options/proxy_options_spec.rb index a64ef0e27..749247d1f 100644 --- a/spec/faraday/options/proxy_options_spec.rb +++ b/spec/faraday/options/proxy_options_spec.rb @@ -14,6 +14,13 @@ expect(options.inspect).to match('# empty string + options = Faraday::ProxyOptions.from proxy_string + expect(options).to be_a_kind_of(Faraday::ProxyOptions) + expect(options.inspect).to eq('#') + end end it 'allows hash access' do diff --git a/spec/faraday/params_encoders/flat_spec.rb b/spec/faraday/params_encoders/flat_spec.rb index fe746d635..115342e53 100644 --- a/spec/faraday/params_encoders/flat_spec.rb +++ b/spec/faraday/params_encoders/flat_spec.rb @@ -31,4 +31,12 @@ params = { a: [] } expect(subject.encode(params)).to eq('a=') end + + it 'encodes unsorted when asked' do + params = { b: false, a: true } + expect(subject.encode(params)).to eq('a=true&b=false') + Faraday::FlatParamsEncoder.sort_params = false + expect(subject.encode(params)).to eq('b=false&a=true') + Faraday::FlatParamsEncoder.sort_params = true + end end diff --git a/spec/faraday/params_encoders/nested_spec.rb b/spec/faraday/params_encoders/nested_spec.rb index 6909f42f0..83da22dc4 100644 --- a/spec/faraday/params_encoders/nested_spec.rb +++ b/spec/faraday/params_encoders/nested_spec.rb @@ -62,7 +62,8 @@ it 'encodes rack compat' do params = { a: [{ one: '1', two: '2' }, '3', ''] } result = Faraday::Utils.unescape(Faraday::NestedParamsEncoder.encode(params)).split('&') - expected = Rack::Utils.build_nested_query(params).split('&') + escaped = Rack::Utils.build_nested_query(params) + expected = Rack::Utils.unescape(escaped).split('&') expect(result).to match_array(expected) end @@ -94,6 +95,22 @@ expect(subject.encode(params)).to eq('a%5B%5D=true&a%5B%5D=false') end + it 'encodes unsorted when asked' do + params = { b: false, a: true } + expect(subject.encode(params)).to eq('a=true&b=false') + Faraday::NestedParamsEncoder.sort_params = false + expect(subject.encode(params)).to eq('b=false&a=true') + Faraday::NestedParamsEncoder.sort_params = true + end + + it 'encodes arrays indices when asked' do + params = { a: [0, 1, 2] } + expect(subject.encode(params)).to eq('a%5B%5D=0&a%5B%5D=1&a%5B%5D=2') + Faraday::NestedParamsEncoder.array_indices = true + expect(subject.encode(params)).to eq('a%5B0%5D=0&a%5B1%5D=1&a%5B2%5D=2') + Faraday::NestedParamsEncoder.array_indices = false + end + shared_examples 'a wrong decoding' do it do expect { subject.decode(query) }.to raise_error(TypeError) do |e| diff --git a/spec/faraday/rack_builder_spec.rb b/spec/faraday/rack_builder_spec.rb index b1d7981e3..89f17ca96 100644 --- a/spec/faraday/rack_builder_spec.rb +++ b/spec/faraday/rack_builder_spec.rb @@ -12,16 +12,16 @@ def call(env) class Apple < Handler end + class Orange < Handler end - class Banana < Handler - end - class Broken < Faraday::Middleware - dependency 'zomg/i_dont/exist' + class Banana < Handler end subject { conn.builder } + before { Faraday.default_adapter = :test } + after { Faraday.default_adapter = nil } context 'with default stack' do let(:conn) { Faraday::Connection.new } @@ -88,13 +88,6 @@ class Broken < Faraday::Middleware it { expect(subject.handlers).to eq([Apple]) } - it 'allows rebuilding' do - subject.build do |builder| - builder.use(Orange) - end - expect(subject.handlers).to eq([Orange]) - end - it 'allows use' do subject.use(Orange) expect(subject.handlers).to eq([Apple, Orange]) @@ -117,36 +110,6 @@ class Broken < Faraday::Middleware end end - context 'with custom registered middleware' do - let(:conn) { Faraday::Connection.new {} } - - after { Faraday::Middleware.unregister_middleware(:apple) } - - it 'allows to register with constant' do - Faraday::Middleware.register_middleware(apple: Apple) - subject.use(:apple) - expect(subject.handlers).to eq([Apple]) - end - - it 'allows to register with symbol' do - Faraday::Middleware.register_middleware(apple: :Apple) - subject.use(:apple) - expect(subject.handlers).to eq([Apple]) - end - - it 'allows to register with string' do - Faraday::Middleware.register_middleware(apple: 'Apple') - subject.use(:apple) - expect(subject.handlers).to eq([Apple]) - end - - it 'allows to register with Proc' do - Faraday::Middleware.register_middleware(apple: -> { Apple }) - subject.use(:apple) - expect(subject.handlers).to eq([Apple]) - end - end - context 'when having two handlers' do let(:conn) { Faraday::Connection.new {} } @@ -176,21 +139,179 @@ class Broken < Faraday::Middleware end end - context 'when having a handler with broken dependency' do - let(:conn) do - Faraday::Connection.new do |builder| - builder.adapter :test do |stub| - stub.get('/') { |_| [200, {}, ''] } + context 'when adapter is added with named options' do + after { Faraday.default_adapter_options = {} } + let(:conn) { Faraday::Connection.new {} } + + let(:cat_adapter) do + Class.new(Faraday::Adapter) do + attr_accessor :name + + def initialize(app, name:) + super(app) + @name = name end end end - before { subject.use(Broken) } + let(:cat) { subject.adapter.build } + + it 'adds a handler to construct adapter with named options' do + Faraday.default_adapter = cat_adapter + Faraday.default_adapter_options = { name: 'Chloe' } + expect { cat }.to_not output( + /warning: Using the last argument as keyword parameters is deprecated/ + ).to_stderr + expect(cat.name).to eq 'Chloe' + end + end + + context 'when middleware is added with named arguments' do + let(:conn) { Faraday::Connection.new {} } + + let(:dog_middleware) do + Class.new(Faraday::Middleware) do + attr_accessor :name - it 'raises an error while making a request' do - expect { conn.get('/') }.to raise_error(RuntimeError) do |err| - expect(err.message).to eq('missing dependency for Broken: cannot load such file -- zomg/i_dont/exist') + def initialize(app, name:) + super(app) + @name = name + end end end + let(:dog) do + subject.handlers.find { |handler| handler == dog_middleware }.build + end + + it 'adds a handler to construct middleware with options passed to use' do + subject.use dog_middleware, name: 'Rex' + expect { dog }.to_not output( + /warning: Using the last argument as keyword parameters is deprecated/ + ).to_stderr + expect(dog.name).to eq('Rex') + end + end + + context 'when a middleware is added with named arguments' do + let(:conn) { Faraday::Connection.new {} } + + let(:cat_request) do + Class.new(Faraday::Middleware) do + attr_accessor :name + + def initialize(app, name:) + super(app) + @name = name + end + end + end + let(:cat) do + subject.handlers.find { |handler| handler == cat_request }.build + end + + it 'adds a handler to construct request adapter with options passed to request' do + Faraday::Request.register_middleware cat_request: cat_request + subject.request :cat_request, name: 'Felix' + expect { cat }.to_not output( + /warning: Using the last argument as keyword parameters is deprecated/ + ).to_stderr + expect(cat.name).to eq('Felix') + end + end + + context 'when a middleware is added with named arguments' do + let(:conn) { Faraday::Connection.new {} } + + let(:fish_response) do + Class.new(Faraday::Middleware) do + attr_accessor :name + + def initialize(app, name:) + super(app) + @name = name + end + end + end + let(:fish) do + subject.handlers.find { |handler| handler == fish_response }.build + end + + it 'adds a handler to construct response adapter with options passed to response' do + Faraday::Response.register_middleware fish_response: fish_response + subject.response :fish_response, name: 'Bubbles' + expect { fish }.to_not output( + /warning: Using the last argument as keyword parameters is deprecated/ + ).to_stderr + expect(fish.name).to eq('Bubbles') + end + end + + context 'when a plain adapter is added with named arguments' do + let(:conn) { Faraday::Connection.new {} } + + let(:rabbit_adapter) do + Class.new(Faraday::Adapter) do + attr_accessor :name + + def initialize(app, name:) + super(app) + @name = name + end + end + end + let(:rabbit) do + subject.adapter.build + end + + it 'adds a handler to construct adapter with options passed to adapter' do + Faraday::Adapter.register_middleware rabbit_adapter: rabbit_adapter + subject.adapter :rabbit_adapter, name: 'Thumper' + expect { rabbit }.to_not output( + /warning: Using the last argument as keyword parameters is deprecated/ + ).to_stderr + expect(rabbit.name).to eq('Thumper') + end + end + + context 'when handlers are directly added or updated' do + let(:conn) { Faraday::Connection.new {} } + + let(:rock_handler) do + Class.new do + attr_accessor :name + + def initialize(_app, name:) + @name = name + end + end + end + let(:rock) do + subject.handlers.find { |handler| handler == rock_handler }.build + end + + it 'adds a handler to construct adapter with options passed to insert' do + subject.insert 0, rock_handler, name: 'Stony' + expect { rock }.to_not output( + /warning: Using the last argument as keyword parameters is deprecated/ + ).to_stderr + expect(rock.name).to eq('Stony') + end + + it 'adds a handler with options passed to insert_after' do + subject.insert_after 0, rock_handler, name: 'Rocky' + expect { rock }.to_not output( + /warning: Using the last argument as keyword parameters is deprecated/ + ).to_stderr + expect(rock.name).to eq('Rocky') + end + + it 'adds a handler with options passed to swap' do + subject.insert 0, rock_handler, name: 'Flint' + subject.swap 0, rock_handler, name: 'Chert' + expect { rock }.to_not output( + /warning: Using the last argument as keyword parameters is deprecated/ + ).to_stderr + expect(rock.name).to eq('Chert') + end end end diff --git a/spec/faraday/request/authorization_spec.rb b/spec/faraday/request/authorization_spec.rb index 85026d926..437c88ae0 100644 --- a/spec/faraday/request/authorization_spec.rb +++ b/spec/faraday/request/authorization_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Faraday::Request::Authorization do let(:conn) do Faraday.new do |b| - b.request auth_type, *auth_config + b.request :authorization, auth_type, *auth_config b.adapter :test do |stub| stub.get('/auth-echo') do |env| [200, {}, env[:request_headers]['Authorization']] @@ -14,10 +14,10 @@ shared_examples 'does not interfere with existing authentication' do context 'and request already has an authentication header' do - let(:response) { conn.get('/auth-echo', nil, authorization: 'Token token="bar"') } + let(:response) { conn.get('/auth-echo', nil, authorization: 'OAuth oauth_token') } it 'does not interfere with existing authorization' do - expect(response.body).to eq('Token token="bar"') + expect(response.body).to eq('OAuth oauth_token') end end end @@ -25,7 +25,7 @@ let(:response) { conn.get('/auth-echo') } describe 'basic_auth' do - let(:auth_type) { :basic_auth } + let(:auth_type) { :basic } context 'when passed correct params' do let(:auth_config) { %w[aladdin opensesame] } @@ -44,43 +44,73 @@ end end - describe 'token_auth' do - let(:auth_type) { :token_auth } + describe 'authorization' do + let(:auth_type) { :Bearer } - context 'when passed correct params' do - let(:auth_config) { 'quux' } + context 'when passed a string' do + let(:auth_config) { ['custom'] } - it { expect(response.body).to eq('Token token="quux"') } + it { expect(response.body).to eq('Bearer custom') } include_examples 'does not interfere with existing authentication' end - context 'when other values are provided' do - let(:auth_config) { ['baz', foo: 42] } + context 'when passed a proc' do + let(:auth_config) { [-> { 'custom_from_proc' }] } - it { expect(response.body).to match(/^Token /) } - it { expect(response.body).to match(/token="baz"/) } - it { expect(response.body).to match(/foo="42"/) } + it { expect(response.body).to eq('Bearer custom_from_proc') } include_examples 'does not interfere with existing authentication' end - end - - describe 'authorization' do - let(:auth_type) { :authorization } - context 'when passed two strings' do - let(:auth_config) { ['custom', 'abc def'] } + context 'when passed a callable' do + let(:callable) { double('Callable Authorizer', call: 'custom_from_callable') } + let(:auth_config) { [callable] } - it { expect(response.body).to eq('custom abc def') } + it { expect(response.body).to eq('Bearer custom_from_callable') } include_examples 'does not interfere with existing authentication' end - context 'when passed a string and a hash' do - let(:auth_config) { ['baz', foo: 42] } + context 'with an argument' do + let(:response) { conn.get('/auth-echo', nil, 'middle' => 'crunchy surprise') } + + context 'when passed a proc' do + let(:auth_config) { [proc { |env| "proc #{env.request_headers['middle']}" }] } + + it { expect(response.body).to eq('Bearer proc crunchy surprise') } + + include_examples 'does not interfere with existing authentication' + end + + context 'when passed a lambda' do + let(:auth_config) { [->(env) { "lambda #{env.request_headers['middle']}" }] } + + it { expect(response.body).to eq('Bearer lambda crunchy surprise') } + + include_examples 'does not interfere with existing authentication' + end + + context 'when passed a callable with an argument' do + let(:callable) do + Class.new do + def call(env) + "callable #{env.request_headers['middle']}" + end + end.new + end + let(:auth_config) { [callable] } + + it { expect(response.body).to eq('Bearer callable crunchy surprise') } + + include_examples 'does not interfere with existing authentication' + end + end + + context 'when passed too many arguments' do + let(:auth_config) { %w[baz foo] } - it { expect(response.body).to eq('baz foo="42"') } + it { expect { response }.to raise_error(ArgumentError) } include_examples 'does not interfere with existing authentication' end diff --git a/spec/faraday/request/instrumentation_spec.rb b/spec/faraday/request/instrumentation_spec.rb index f8af4c4f5..d207c5568 100644 --- a/spec/faraday/request/instrumentation_spec.rb +++ b/spec/faraday/request/instrumentation_spec.rb @@ -30,13 +30,11 @@ def instrument(name, env) it { expect(options.name).to eq('request.faraday') } it 'defaults to ActiveSupport::Notifications' do - begin - res = options.instrumenter - rescue NameError => e - expect(e.to_s).to match('ActiveSupport') - else - expect(res).to eq(ActiveSupport::Notifications) - end + res = options.instrumenter + rescue NameError => e + expect(e.to_s).to match('ActiveSupport') + else + expect(res).to eq(ActiveSupport::Notifications) end it 'instruments with default name' do diff --git a/spec/faraday/request/json_spec.rb b/spec/faraday/request/json_spec.rb new file mode 100644 index 000000000..44dee7963 --- /dev/null +++ b/spec/faraday/request/json_spec.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +RSpec.describe Faraday::Request::Json do + let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) } + + def process(body, content_type = nil) + env = { body: body, request_headers: Faraday::Utils::Headers.new } + env[:request_headers]['content-type'] = content_type if content_type + middleware.call(Faraday::Env.from(env)).env + end + + def result_body + result[:body] + end + + def result_type + result[:request_headers]['content-type'] + end + + context 'no body' do + let(:result) { process(nil) } + + it "doesn't change body" do + expect(result_body).to be_nil + end + + it "doesn't add content type" do + expect(result_type).to be_nil + end + end + + context 'empty body' do + let(:result) { process('') } + + it "doesn't change body" do + expect(result_body).to be_empty + end + + it "doesn't add content type" do + expect(result_type).to be_nil + end + end + + context 'string body' do + let(:result) { process('{"a":1}') } + + it "doesn't change body" do + expect(result_body).to eq('{"a":1}') + end + + it 'adds content type' do + expect(result_type).to eq('application/json') + end + end + + context 'object body' do + let(:result) { process(a: 1) } + + it 'encodes body' do + expect(result_body).to eq('{"a":1}') + end + + it 'adds content type' do + expect(result_type).to eq('application/json') + end + end + + context 'empty object body' do + let(:result) { process({}) } + + it 'encodes body' do + expect(result_body).to eq('{}') + end + end + + context 'true body' do + let(:result) { process(true) } + + it 'encodes body' do + expect(result_body).to eq('true') + end + + it 'adds content type' do + expect(result_type).to eq('application/json') + end + end + + context 'false body' do + let(:result) { process(false) } + + it 'encodes body' do + expect(result_body).to eq('false') + end + + it 'adds content type' do + expect(result_type).to eq('application/json') + end + end + + context 'object body with json type' do + let(:result) { process({ a: 1 }, 'application/json; charset=utf-8') } + + it 'encodes body' do + expect(result_body).to eq('{"a":1}') + end + + it "doesn't change content type" do + expect(result_type).to eq('application/json; charset=utf-8') + end + end + + context 'object body with vendor json type' do + let(:result) { process({ a: 1 }, 'application/vnd.myapp.v1+json; charset=utf-8') } + + it 'encodes body' do + expect(result_body).to eq('{"a":1}') + end + + it "doesn't change content type" do + expect(result_type).to eq('application/vnd.myapp.v1+json; charset=utf-8') + end + end + + context 'object body with incompatible type' do + let(:result) { process({ a: 1 }, 'application/xml; charset=utf-8') } + + it "doesn't change body" do + expect(result_body).to eq(a: 1) + end + + it "doesn't change content type" do + expect(result_type).to eq('application/xml; charset=utf-8') + end + end + + context 'with encoder' do + let(:encoder) do + double('Encoder').tap do |e| + allow(e).to receive(:dump) { |s, opts| JSON.generate(s, opts) } + end + end + + let(:result) { process(a: 1) } + + context 'when encoder is passed as object' do + let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: encoder }) } + + it 'calls specified JSON encoder\'s dump method' do + expect(encoder).to receive(:dump).with({ a: 1 }) + + result + end + + it 'encodes body' do + expect(result_body).to eq('{"a":1}') + end + + it 'adds content type' do + expect(result_type).to eq('application/json') + end + end + + context 'when encoder is passed as an object-method pair' do + let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: [encoder, :dump] }) } + + it 'calls specified JSON encoder' do + expect(encoder).to receive(:dump).with({ a: 1 }) + + result + end + + it 'encodes body' do + expect(result_body).to eq('{"a":1}') + end + + it 'adds content type' do + expect(result_type).to eq('application/json') + end + end + + context 'when encoder is not passed' do + let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) } + + it 'calls JSON.generate' do + expect(JSON).to receive(:generate).with({ a: 1 }) + + result + end + + it 'encodes body' do + expect(result_body).to eq('{"a":1}') + end + + it 'adds content type' do + expect(result_type).to eq('application/json') + end + end + end +end diff --git a/spec/faraday/request/multipart_spec.rb b/spec/faraday/request/multipart_spec.rb deleted file mode 100644 index 98940b72d..000000000 --- a/spec/faraday/request/multipart_spec.rb +++ /dev/null @@ -1,274 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Request::Multipart do - let(:conn) do - Faraday.new do |b| - b.request :multipart - b.request :url_encoded - b.adapter :test do |stub| - stub.post('/echo') do |env| - posted_as = env[:request_headers]['Content-Type'] - expect(env[:body]).to be_a_kind_of(Faraday::CompositeReadIO) - [200, { 'Content-Type' => posted_as }, env[:body].read] - end - end - end - end - - shared_examples 'a multipart request' do - it 'generates a unique boundary for each request' do - response1 = conn.post('/echo', payload) - response2 = conn.post('/echo', payload) - - b1 = parse_multipart_boundary(response1.headers['Content-Type']) - b2 = parse_multipart_boundary(response2.headers['Content-Type']) - expect(b1).to_not eq(b2) - end - end - - context 'FilePart: when multipart objects in param' do - let(:payload) do - { - a: 1, - b: { - c: Faraday::FilePart.new(__FILE__, 'text/x-ruby', nil, - 'Content-Disposition' => 'form-data; foo=1'), - d: 2 - } - } - end - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_a, body_a = result.part('a') - expect(part_a).to_not be_nil - expect(part_a.filename).to be_nil - expect(body_a).to eq('1') - - part_bc, body_bc = result.part('b[c]') - expect(part_bc).to_not be_nil - expect(part_bc.filename).to eq('multipart_spec.rb') - expect(part_bc.headers['content-disposition']).to eq( - 'form-data; foo=1; name="b[c]"; filename="multipart_spec.rb"' - ) - expect(part_bc.headers['content-type']).to eq('text/x-ruby') - expect(part_bc.headers['content-transfer-encoding']).to eq('binary') - expect(body_bc).to eq(File.read(__FILE__)) - - part_bd, body_bd = result.part('b[d]') - expect(part_bd).to_not be_nil - expect(part_bd.filename).to be_nil - expect(body_bd).to eq('2') - end - end - - context 'FilePart: when providing json and IO content in the same payload' do - let(:io) { StringIO.new('io-content') } - let(:json) do - { - b: 1, - c: 2 - }.to_json - end - - let(:payload) do - { - json: Faraday::ParamPart.new(json, 'application/json'), - io: Faraday::FilePart.new(io, 'application/pdf') - } - end - - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_json, body_json = result.part('json') - expect(part_json).to_not be_nil - expect(part_json.mime).to eq('application/json') - expect(part_json.filename).to be_nil - expect(body_json).to eq(json) - - part_io, body_io = result.part('io') - expect(part_io).to_not be_nil - expect(part_io.mime).to eq('application/pdf') - expect(part_io.filename).to eq('local.path') - expect(body_io).to eq(io.string) - end - end - - context 'FilePart: when multipart objects in array param' do - let(:payload) do - { - a: 1, - b: [{ - c: Faraday::FilePart.new(__FILE__, 'text/x-ruby'), - d: 2 - }] - } - end - - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_a, body_a = result.part('a') - expect(part_a).to_not be_nil - expect(part_a.filename).to be_nil - expect(body_a).to eq('1') - - part_bc, body_bc = result.part('b[][c]') - expect(part_bc).to_not be_nil - expect(part_bc.filename).to eq('multipart_spec.rb') - expect(part_bc.headers['content-disposition']).to eq( - 'form-data; name="b[][c]"; filename="multipart_spec.rb"' - ) - expect(part_bc.headers['content-type']).to eq('text/x-ruby') - expect(part_bc.headers['content-transfer-encoding']).to eq('binary') - expect(body_bc).to eq(File.read(__FILE__)) - - part_bd, body_bd = result.part('b[][d]') - expect(part_bd).to_not be_nil - expect(part_bd.filename).to be_nil - expect(body_bd).to eq('2') - end - end - - context 'UploadIO: when multipart objects in param' do - let(:payload) do - { - a: 1, - b: { - c: Faraday::UploadIO.new(__FILE__, 'text/x-ruby', nil, - 'Content-Disposition' => 'form-data; foo=1'), - d: 2 - } - } - end - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_a, body_a = result.part('a') - expect(part_a).to_not be_nil - expect(part_a.filename).to be_nil - expect(body_a).to eq('1') - - part_bc, body_bc = result.part('b[c]') - expect(part_bc).to_not be_nil - expect(part_bc.filename).to eq('multipart_spec.rb') - expect(part_bc.headers['content-disposition']).to eq( - 'form-data; foo=1; name="b[c]"; filename="multipart_spec.rb"' - ) - expect(part_bc.headers['content-type']).to eq('text/x-ruby') - expect(part_bc.headers['content-transfer-encoding']).to eq('binary') - expect(body_bc).to eq(File.read(__FILE__)) - - part_bd, body_bd = result.part('b[d]') - expect(part_bd).to_not be_nil - expect(part_bd.filename).to be_nil - expect(body_bd).to eq('2') - end - end - - context 'UploadIO: when providing json and IO content in the same payload' do - let(:io) { StringIO.new('io-content') } - let(:json) do - { - b: 1, - c: 2 - }.to_json - end - - let(:payload) do - { - json: Faraday::ParamPart.new(json, 'application/json'), - io: Faraday::UploadIO.new(io, 'application/pdf') - } - end - - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_json, body_json = result.part('json') - expect(part_json).to_not be_nil - expect(part_json.mime).to eq('application/json') - expect(part_json.filename).to be_nil - expect(body_json).to eq(json) - - part_io, body_io = result.part('io') - expect(part_io).to_not be_nil - expect(part_io.mime).to eq('application/pdf') - expect(part_io.filename).to eq('local.path') - expect(body_io).to eq(io.string) - end - end - - context 'UploadIO: when multipart objects in array param' do - let(:payload) do - { - a: 1, - b: [{ - c: Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), - d: 2 - }] - } - end - - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_a, body_a = result.part('a') - expect(part_a).to_not be_nil - expect(part_a.filename).to be_nil - expect(body_a).to eq('1') - - part_bc, body_bc = result.part('b[][c]') - expect(part_bc).to_not be_nil - expect(part_bc.filename).to eq('multipart_spec.rb') - expect(part_bc.headers['content-disposition']).to eq( - 'form-data; name="b[][c]"; filename="multipart_spec.rb"' - ) - expect(part_bc.headers['content-type']).to eq('text/x-ruby') - expect(part_bc.headers['content-transfer-encoding']).to eq('binary') - expect(body_bc).to eq(File.read(__FILE__)) - - part_bd, body_bd = result.part('b[][d]') - expect(part_bd).to_not be_nil - expect(part_bd.filename).to be_nil - expect(body_bd).to eq('2') - end - end -end diff --git a/spec/faraday/request/retry_spec.rb b/spec/faraday/request/retry_spec.rb deleted file mode 100644 index cd3ae9a74..000000000 --- a/spec/faraday/request/retry_spec.rb +++ /dev/null @@ -1,242 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Request::Retry do - let(:calls) { [] } - let(:times_called) { calls.size } - let(:options) { [] } - let(:conn) do - Faraday.new do |b| - b.request :retry, *options - - b.adapter :test do |stub| - %w[get post].each do |method| - stub.send(method, '/unstable') do |env| - calls << env.dup - env[:body] = nil # simulate blanking out response body - callback.call - end - end - end - end - end - - context 'when an unexpected error happens' do - let(:callback) { -> { raise 'boom!' } } - - before { expect { conn.get('/unstable') }.to raise_error(RuntimeError) } - - it { expect(times_called).to eq(1) } - - context 'and this is passed as a custom exception' do - let(:options) { [{ exceptions: StandardError }] } - - it { expect(times_called).to eq(3) } - end - end - - context 'when an expected error happens' do - let(:callback) { -> { raise Errno::ETIMEDOUT } } - - before do - @started = Time.now - expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT) - end - - it { expect(times_called).to eq(3) } - - context 'and legacy max_retry set to 1' do - let(:options) { [1] } - - it { expect(times_called).to eq(2) } - end - - context 'and legacy max_retry set to -9' do - let(:options) { [-9] } - - it { expect(times_called).to eq(1) } - end - - context 'and new max_retry set to 3' do - let(:options) { [{ max: 3 }] } - - it { expect(times_called).to eq(4) } - end - - context 'and new max_retry set to -9' do - let(:options) { [{ max: -9 }] } - - it { expect(times_called).to eq(1) } - end - - context 'and both max_retry and interval are set' do - let(:options) { [{ max: 2, interval: 0.1 }] } - - it { expect(Time.now - @started).to be_within(0.04).of(0.2) } - end - end - - context 'when no exception raised' do - let(:options) { [{ max: 1, retry_statuses: 429 }] } - - before { conn.get('/unstable') } - - context 'and response code is in retry_statuses' do - let(:callback) { -> { [429, {}, ''] } } - - it { expect(times_called).to eq(2) } - end - - context 'and response code is not in retry_statuses' do - let(:callback) { -> { [503, {}, ''] } } - - it { expect(times_called).to eq(1) } - end - end - - describe '#calculate_retry_interval' do - context 'with exponential backoff' do - let(:options) { { max: 5, interval: 0.1, backoff_factor: 2 } } - let(:middleware) { Faraday::Request::Retry.new(nil, options) } - - it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) } - it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) } - it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.4) } - end - - context 'with exponential backoff and max_interval' do - let(:options) { { max: 5, interval: 0.1, backoff_factor: 2, max_interval: 0.3 } } - let(:middleware) { Faraday::Request::Retry.new(nil, options) } - - it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) } - it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) } - it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.3) } - it { expect(middleware.send(:calculate_retry_interval, 2)).to eq(0.3) } - end - - context 'with exponential backoff and interval_randomness' do - let(:options) { { max: 2, interval: 0.1, interval_randomness: 0.05 } } - let(:middleware) { Faraday::Request::Retry.new(nil, options) } - - it { expect(middleware.send(:calculate_retry_interval, 2)).to be_between(0.1, 0.15) } - end - end - - context 'when method is not idempotent' do - let(:callback) { -> { raise Errno::ETIMEDOUT } } - - before { expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) } - - it { expect(times_called).to eq(1) } - end - - describe 'retry_if option' do - let(:callback) { -> { raise Errno::ETIMEDOUT } } - let(:options) { [{ retry_if: @check }] } - - it 'retries if retry_if block always returns true' do - body = { foo: :bar } - @check = ->(_, _) { true } - expect { conn.post('/unstable', body) }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(3) - expect(calls.all? { |env| env[:body] == body }).to be_truthy - end - - it 'does not retry if retry_if block returns false checking env' do - @check = ->(env, _) { env[:method] != :post } - expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(1) - end - - it 'does not retry if retry_if block returns false checking exception' do - @check = ->(_, exception) { !exception.is_a?(Errno::ETIMEDOUT) } - expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(1) - end - - it 'FilePart: should rewind files on retry' do - io = StringIO.new('Test data') - filepart = Faraday::FilePart.new(io, 'application/octet/stream') - - rewound = 0 - rewind = -> { rewound += 1 } - - @check = ->(_, _) { true } - allow(filepart).to receive(:rewind, &rewind) - expect { conn.post('/unstable', file: filepart) }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(3) - expect(rewound).to eq(2) - end - - it 'UploadIO: should rewind files on retry' do - io = StringIO.new('Test data') - upload_io = Faraday::UploadIO.new(io, 'application/octet/stream') - - rewound = 0 - rewind = -> { rewound += 1 } - - @check = ->(_, _) { true } - allow(upload_io).to receive(:rewind, &rewind) - expect { conn.post('/unstable', file: upload_io) }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(3) - expect(rewound).to eq(2) - end - - context 'when explicitly specifying methods to retry' do - let(:options) { [{ retry_if: @check, methods: [:post] }] } - - it 'does not call retry_if for specified methods' do - @check = ->(_, _) { raise 'this should have never been called' } - expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(3) - end - end - - context 'with empty list of methods to retry' do - let(:options) { [{ retry_if: @check, methods: [] }] } - - it 'calls retry_if for all methods' do - @check = ->(_, _) { calls.size < 2 } - expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(2) - end - end - end - - describe 'retry_after header support' do - let(:callback) { -> { [504, headers, ''] } } - let(:elapsed) { Time.now - @started } - - before do - @started = Time.now - conn.get('/unstable') - end - - context 'when retry_after bigger than interval' do - let(:headers) { { 'Retry-After' => '0.5' } } - let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] } - - it { expect(elapsed).to be > 0.5 } - end - - context 'when retry_after smaller than interval' do - let(:headers) { { 'Retry-After' => '0.1' } } - let(:options) { [{ max: 1, interval: 0.2, retry_statuses: 504 }] } - - it { expect(elapsed).to be > 0.2 } - end - - context 'when retry_after is a timestamp' do - let(:headers) { { 'Retry-After' => (Time.now.utc + 2).strftime('%a, %d %b %Y %H:%M:%S GMT') } } - let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] } - - it { expect(elapsed).to be > 1 } - end - - context 'when retry_after is bigger than max_interval' do - let(:headers) { { 'Retry-After' => (Time.now.utc + 20).strftime('%a, %d %b %Y %H:%M:%S GMT') } } - let(:options) { [{ max: 2, interval: 0.1, max_interval: 5, retry_statuses: 504 }] } - - it { expect(times_called).to eq(1) } - end - end -end diff --git a/spec/faraday/request/url_encoded_spec.rb b/spec/faraday/request/url_encoded_spec.rb index b6380cbb2..bdd9e0ac0 100644 --- a/spec/faraday/request/url_encoded_spec.rb +++ b/spec/faraday/request/url_encoded_spec.rb @@ -1,14 +1,19 @@ # frozen_string_literal: true +require 'stringio' + RSpec.describe Faraday::Request::UrlEncoded do let(:conn) do Faraday.new do |b| - b.request :multipart b.request :url_encoded b.adapter :test do |stub| stub.post('/echo') do |env| posted_as = env[:request_headers]['Content-Type'] - [200, { 'Content-Type' => posted_as }, env[:body]] + body = env[:body] + if body.respond_to?(:read) + body = body.read + end + [200, { 'Content-Type' => posted_as }, body] end end end @@ -67,4 +72,22 @@ response = conn.post('/echo', 'a' => { 'b' => { 'c' => ['d'] } }) expect(response.body).to eq('a%5Bb%5D%5Bc%5D%5B%5D=d') end + + it 'works with files' do + response = conn.post('/echo', StringIO.new('str=apple')) + expect(response.body).to eq('str=apple') + end + + context 'customising default_space_encoding' do + around do |example| + Faraday::Utils.default_space_encoding = '%20' + example.run + Faraday::Utils.default_space_encoding = nil + end + + it 'uses the custom character to encode spaces' do + response = conn.post('/echo', str: 'apple banana') + expect(response.body).to eq('str=apple%20banana') + end + end end diff --git a/spec/faraday/request_spec.rb b/spec/faraday/request_spec.rb index 6b4287da1..fbf85b56f 100644 --- a/spec/faraday/request_spec.rb +++ b/spec/faraday/request_spec.rb @@ -2,24 +2,25 @@ RSpec.describe Faraday::Request do let(:conn) do - Faraday.new(url: 'http://sushi.com/api', + Faraday.new(url: 'http://httpbingo.org/api', headers: { 'Mime-Version' => '1.0' }, request: { oauth: { consumer_key: 'anonymous' } }) end - let(:method) { :get } + let(:http_method) { :get } let(:block) { nil } - subject { conn.build_request(method, &block) } + subject { conn.build_request(http_method, &block) } context 'when nothing particular is configured' do - it { expect(subject.method).to eq(:get) } + it { expect(subject.http_method).to eq(:get) } it { expect(subject.to_env(conn).ssl.verify).to be_falsey } + it { expect(subject.to_env(conn).ssl.verify_hostname).to be_falsey } end - context 'when method is post' do - let(:method) { :post } + context 'when HTTP method is post' do + let(:http_method) { :post } - it { expect(subject.method).to eq(:post) } + it { expect(subject.http_method).to eq(:post) } end context 'when setting the url on setup with a URI' do @@ -27,7 +28,7 @@ it { expect(subject.path).to eq(URI.parse('foo.json')) } it { expect(subject.params).to eq('a' => '1') } - it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') } + it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1') } end context 'when setting the url on setup with a string path and params' do @@ -35,7 +36,7 @@ it { expect(subject.path).to eq('foo.json') } it { expect(subject.params).to eq('a' => 1) } - it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') } + it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1') } end context 'when setting the url on setup with a path including params' do @@ -43,7 +44,7 @@ it { expect(subject.path).to eq('foo.json') } it { expect(subject.params).to eq('a' => '1', 'b' => '2') } - it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1&b=2') } + it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1&b=2') } end context 'when setting a header on setup with []= syntax' do diff --git a/spec/faraday/response/json_spec.rb b/spec/faraday/response/json_spec.rb new file mode 100644 index 000000000..e6cbda39b --- /dev/null +++ b/spec/faraday/response/json_spec.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +RSpec.describe Faraday::Response::Json, type: :response do + let(:options) { {} } + let(:headers) { {} } + let(:middleware) do + described_class.new(lambda { |env| + Faraday::Response.new(env) + }, **options) + end + + def process(body, content_type = 'application/json', options = {}) + env = { + body: body, request: options, + request_headers: Faraday::Utils::Headers.new, + response_headers: Faraday::Utils::Headers.new(headers) + } + env[:response_headers]['content-type'] = content_type if content_type + yield(env) if block_given? + middleware.call(Faraday::Env.from(env)) + end + + context 'no type matching' do + it "doesn't change nil body" do + expect(process(nil).body).to be_nil + end + + it 'nullifies empty body' do + expect(process('').body).to be_nil + end + + it 'parses json body' do + response = process('{"a":1}') + expect(response.body).to eq('a' => 1) + expect(response.env[:raw_body]).to be_nil + end + end + + context 'with preserving raw' do + let(:options) { { preserve_raw: true } } + + it 'parses json body' do + response = process('{"a":1}') + expect(response.body).to eq('a' => 1) + expect(response.env[:raw_body]).to eq('{"a":1}') + end + end + + context 'with default regexp type matching' do + it 'parses json body of correct type' do + response = process('{"a":1}', 'application/x-json') + expect(response.body).to eq('a' => 1) + end + + it 'ignores json body of incorrect type' do + response = process('{"a":1}', 'text/json-xml') + expect(response.body).to eq('{"a":1}') + end + end + + context 'with array type matching' do + let(:options) { { content_type: %w[a/b c/d] } } + + it 'parses json body of correct type' do + expect(process('{"a":1}', 'a/b').body).to be_a(Hash) + expect(process('{"a":1}', 'c/d').body).to be_a(Hash) + end + + it 'ignores json body of incorrect type' do + expect(process('{"a":1}', 'a/d').body).not_to be_a(Hash) + end + end + + it 'chokes on invalid json' do + expect { process('{!') }.to raise_error(Faraday::ParsingError) + end + + it 'includes the response on the ParsingError instance' do + process('{') { |env| env[:response] = Faraday::Response.new } + raise 'Parsing should have failed.' + rescue Faraday::ParsingError => e + expect(e.response).to be_a(Faraday::Response) + end + + context 'HEAD responses' do + it "nullifies the body if it's only one space" do + response = process(' ') + expect(response.body).to be_nil + end + + it "nullifies the body if it's two spaces" do + response = process(' ') + expect(response.body).to be_nil + end + end + + context 'JSON options' do + let(:body) { '{"a": 1}' } + let(:result) { { a: 1 } } + let(:options) do + { + parser_options: { + symbolize_names: true + } + } + end + + it 'passes relevant options to JSON parse' do + expect(::JSON).to receive(:parse) + .with(body, options[:parser_options]) + .and_return(result) + + response = process(body) + expect(response.body).to eq(result) + end + end + + context 'with decoder' do + let(:decoder) do + double('Decoder').tap do |e| + allow(e).to receive(:load) { |s, opts| JSON.parse(s, opts) } + end + end + + let(:body) { '{"a": 1}' } + let(:result) { { a: 1 } } + + context 'when decoder is passed as object' do + let(:options) do + { + parser_options: { + decoder: decoder, + option: :option_value, + symbolize_names: true + } + } + end + + it 'passes relevant options to specified decoder\'s load method' do + expect(decoder).to receive(:load) + .with(body, { option: :option_value, symbolize_names: true }) + .and_return(result) + + response = process(body) + expect(response.body).to eq(result) + end + end + + context 'when decoder is passed as an object-method pair' do + let(:options) do + { + parser_options: { + decoder: [decoder, :load], + option: :option_value, + symbolize_names: true + } + } + end + + it 'passes relevant options to specified decoder\'s method' do + expect(decoder).to receive(:load) + .with(body, { option: :option_value, symbolize_names: true }) + .and_return(result) + + response = process(body) + expect(response.body).to eq(result) + end + end + + context 'when decoder is not passed' do + let(:options) do + { + parser_options: { + symbolize_names: true + } + } + end + + it 'passes relevant options to JSON parse' do + expect(JSON).to receive(:parse) + .with(body, { symbolize_names: true }) + .and_return(result) + + response = process(body) + expect(response.body).to eq(result) + end + + it 'passes relevant options to JSON parse even when nil responds to :load' do + original_allow_message_expectations_on_nil = RSpec::Mocks.configuration.allow_message_expectations_on_nil + RSpec::Mocks.configuration.allow_message_expectations_on_nil = true + allow(nil).to receive(:respond_to?) + .with(:load) + .and_return(true) + + expect(JSON).to receive(:parse) + .with(body, { symbolize_names: true }) + .and_return(result) + + response = process(body) + expect(response.body).to eq(result) + ensure + RSpec::Mocks.configuration.allow_message_expectations_on_nil = original_allow_message_expectations_on_nil + end + end + end +end diff --git a/spec/faraday/response/logger_spec.rb b/spec/faraday/response/logger_spec.rb index 10eeff5f4..4fd1e11c4 100644 --- a/spec/faraday/response/logger_spec.rb +++ b/spec/faraday/response/logger_spec.rb @@ -25,6 +25,7 @@ stubs.get('/filtered_headers') { [200, { 'Content-Type' => 'text/html' }, 'headers response'] } stubs.get('/filtered_params') { [200, { 'Content-Type' => 'text/html' }, 'params response'] } stubs.get('/filtered_url') { [200, { 'Content-Type' => 'text/html' }, 'url response'] } + stubs.get('/connection_failed') { raise Faraday::ConnectionFailed, 'Failed to open TCP connection' } end end end @@ -64,6 +65,15 @@ expect(formatter).to receive(:response).with(an_instance_of(Faraday::Env)) conn.get '/hello' end + + context 'when no route' do + it 'delegates logging to the formatter' do + expect(formatter).to receive(:request).with(an_instance_of(Faraday::Env)) + expect(formatter).to receive(:exception).with(an_instance_of(Faraday::Adapter::Test::Stubs::NotFound)) + + expect { conn.get '/noroute' }.to raise_error(Faraday::Adapter::Test::Stubs::NotFound) + end + end end context 'with custom formatter' do @@ -94,6 +104,16 @@ def response(_env) expect(string_io.string).to match('GET http:/hello') end + it 'logs status' do + conn.get '/hello', nil, accept: 'text/html' + expect(string_io.string).to match('Status 200') + end + + it 'does not log error message by default' do + expect { conn.get '/noroute' }.to raise_error(Faraday::Adapter::Test::Stubs::NotFound) + expect(string_io.string).not_to match(%(no stubbed request for get http:/noroute)) + end + it 'logs request headers by default' do conn.get '/hello', nil, accept: 'text/html' expect(string_io.string).to match(%(Accept: "text/html)) @@ -188,6 +208,24 @@ def response(_env) end end + context 'when logging errors' do + let(:logger_options) { { errors: true } } + + it 'logs error message' do + expect { conn.get '/noroute' }.to raise_error(Faraday::Adapter::Test::Stubs::NotFound) + expect(string_io.string).to match(%(no stubbed request for get http:/noroute)) + end + end + + context 'when logging headers and errors' do + let(:logger_options) { { headers: true, errors: true } } + + it 'logs error message' do + expect { conn.get '/connection_failed' }.to raise_error(Faraday::ConnectionFailed) + expect(string_io.string).to match(%(Failed to open TCP connection)) + end + end + context 'when using log_level' do let(:logger_options) { { bodies: true, log_level: :debug } } diff --git a/spec/faraday/response/middleware_spec.rb b/spec/faraday/response/middleware_spec.rb deleted file mode 100644 index 3c3adf276..000000000 --- a/spec/faraday/response/middleware_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Response::Middleware do - let(:conn) do - Faraday.new do |b| - b.use custom_middleware - b.adapter :test do |stub| - stub.get('ok') { [200, { 'Content-Type' => 'text/html' }, ''] } - stub.get('not_modified') { [304, nil, nil] } - stub.get('no_content') { [204, nil, nil] } - end - end - end - - context 'with a custom ResponseMiddleware' do - let(:custom_middleware) do - Class.new(Faraday::Response::Middleware) do - def parse(body) - body.upcase - end - end - end - - it 'parses the response' do - expect(conn.get('ok').body).to eq('') - end - end - - context 'with a custom ResponseMiddleware but empty response' do - let(:custom_middleware) do - Class.new(Faraday::Response::Middleware) do - def parse(_body) - raise 'this should not be called' - end - end - end - - it 'raises exception for 200 responses' do - expect { conn.get('ok') }.to raise_error(StandardError) - end - - it 'doesn\'t call the middleware for 204 responses' do - expect_any_instance_of(custom_middleware).not_to receive(:parse) - expect(conn.get('no_content').body).to be_nil - end - - it 'doesn\'t call the middleware for 304 responses' do - expect_any_instance_of(custom_middleware).not_to receive(:parse) - expect(conn.get('not_modified').body).to be_nil - end - end -end diff --git a/spec/faraday/response/raise_error_spec.rb b/spec/faraday/response/raise_error_spec.rb index c8331b6e8..65aba59db 100644 --- a/spec/faraday/response/raise_error_spec.rb +++ b/spec/faraday/response/raise_error_spec.rb @@ -11,8 +11,10 @@ stub.get('forbidden') { [403, { 'X-Reason' => 'because' }, 'keep looking'] } stub.get('not-found') { [404, { 'X-Reason' => 'because' }, 'keep looking'] } stub.get('proxy-error') { [407, { 'X-Reason' => 'because' }, 'keep looking'] } + stub.get('request-timeout') { [408, { 'X-Reason' => 'because' }, 'keep looking'] } stub.get('conflict') { [409, { 'X-Reason' => 'because' }, 'keep looking'] } stub.get('unprocessable-entity') { [422, { 'X-Reason' => 'because' }, 'keep looking'] } + stub.get('too-many-requests') { [429, { 'X-Reason' => 'because' }, 'keep looking'] } stub.get('4xx') { [499, { 'X-Reason' => 'because' }, 'keep looking'] } stub.get('nil-status') { [nil, { 'X-Reason' => 'nil' }, 'fail'] } stub.get('server-error') { [500, { 'X-Error' => 'bailout' }, 'fail'] } @@ -29,6 +31,9 @@ expect(ex.message).to eq('the server responded with status 400') expect(ex.response[:headers]['X-Reason']).to eq('because') expect(ex.response[:status]).to eq(400) + expect(ex.response_status).to eq(400) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') end end @@ -37,6 +42,9 @@ expect(ex.message).to eq('the server responded with status 401') expect(ex.response[:headers]['X-Reason']).to eq('because') expect(ex.response[:status]).to eq(401) + expect(ex.response_status).to eq(401) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') end end @@ -45,6 +53,9 @@ expect(ex.message).to eq('the server responded with status 403') expect(ex.response[:headers]['X-Reason']).to eq('because') expect(ex.response[:status]).to eq(403) + expect(ex.response_status).to eq(403) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') end end @@ -53,6 +64,9 @@ expect(ex.message).to eq('the server responded with status 404') expect(ex.response[:headers]['X-Reason']).to eq('because') expect(ex.response[:status]).to eq(404) + expect(ex.response_status).to eq(404) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') end end @@ -61,6 +75,20 @@ expect(ex.message).to eq('407 "Proxy Authentication Required"') expect(ex.response[:headers]['X-Reason']).to eq('because') expect(ex.response[:status]).to eq(407) + expect(ex.response_status).to eq(407) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') + end + end + + it 'raises Faraday::RequestTimeoutError for 408 responses' do + expect { conn.get('request-timeout') }.to raise_error(Faraday::RequestTimeoutError) do |ex| + expect(ex.message).to eq('the server responded with status 408') + expect(ex.response[:headers]['X-Reason']).to eq('because') + expect(ex.response[:status]).to eq(408) + expect(ex.response_status).to eq(408) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') end end @@ -69,6 +97,9 @@ expect(ex.message).to eq('the server responded with status 409') expect(ex.response[:headers]['X-Reason']).to eq('because') expect(ex.response[:status]).to eq(409) + expect(ex.response_status).to eq(409) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') end end @@ -77,6 +108,20 @@ expect(ex.message).to eq('the server responded with status 422') expect(ex.response[:headers]['X-Reason']).to eq('because') expect(ex.response[:status]).to eq(422) + expect(ex.response_status).to eq(422) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') + end + end + + it 'raises Faraday::TooManyRequestsError for 429 responses' do + expect { conn.get('too-many-requests') }.to raise_error(Faraday::TooManyRequestsError) do |ex| + expect(ex.message).to eq('the server responded with status 429') + expect(ex.response[:headers]['X-Reason']).to eq('because') + expect(ex.response[:status]).to eq(429) + expect(ex.response_status).to eq(429) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') end end @@ -85,6 +130,9 @@ expect(ex.message).to eq('http status could not be derived from the server response') expect(ex.response[:headers]['X-Reason']).to eq('nil') expect(ex.response[:status]).to be_nil + expect(ex.response_status).to be_nil + expect(ex.response_body).to eq('fail') + expect(ex.response_headers['X-Reason']).to eq('nil') end end @@ -93,6 +141,9 @@ expect(ex.message).to eq('the server responded with status 499') expect(ex.response[:headers]['X-Reason']).to eq('because') expect(ex.response[:status]).to eq(499) + expect(ex.response_status).to eq(499) + expect(ex.response_body).to eq('keep looking') + expect(ex.response_headers['X-Reason']).to eq('because') end end @@ -101,6 +152,104 @@ expect(ex.message).to eq('the server responded with status 500') expect(ex.response[:headers]['X-Error']).to eq('bailout') expect(ex.response[:status]).to eq(500) + expect(ex.response_status).to eq(500) + expect(ex.response_body).to eq('fail') + expect(ex.response_headers['X-Error']).to eq('bailout') + end + end + + describe 'request info' do + let(:conn) do + Faraday.new do |b| + b.response :raise_error, **middleware_options + b.adapter :test do |stub| + stub.post(url, request_body, request_headers) do + [400, { 'X-Reason' => 'because' }, 'keep looking'] + end + end + end + end + let(:middleware_options) { {} } + let(:request_body) { JSON.generate({ 'item' => 'sth' }) } + let(:request_headers) { { 'Authorization' => 'Basic 123' } } + let(:url_path) { 'request' } + let(:query_params) { 'full=true' } + let(:url) { "#{url_path}?#{query_params}" } + + subject(:perform_request) do + conn.post url do |req| + req.headers['Authorization'] = 'Basic 123' + req.body = request_body + end + end + + it 'returns the request info in the exception' do + expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex| + expect(ex.response[:request][:method]).to eq(:post) + expect(ex.response[:request][:url]).to eq(URI("http:/#{url}")) + expect(ex.response[:request][:url_path]).to eq("/#{url_path}") + expect(ex.response[:request][:params]).to eq({ 'full' => 'true' }) + expect(ex.response[:request][:headers]).to match(a_hash_including(request_headers)) + expect(ex.response[:request][:body]).to eq(request_body) + end + end + + describe 'DEFAULT_OPTION: include_request' do + before(:each) do + Faraday::Response::RaiseError.instance_variable_set(:@default_options, nil) + Faraday::Middleware.instance_variable_set(:@default_options, nil) + end + + after(:all) do + Faraday::Response::RaiseError.instance_variable_set(:@default_options, nil) + Faraday::Middleware.instance_variable_set(:@default_options, nil) + end + + context 'when RaiseError DEFAULT_OPTION (include_request: true) is used' do + it 'includes request info in the exception' do + expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex| + expect(ex.response.keys).to contain_exactly( + :status, + :headers, + :body, + :request + ) + end + end + end + + context 'when application sets default_options `include_request: false`' do + before(:each) do + Faraday::Response::RaiseError.default_options = { include_request: false } + end + + context 'and when include_request option is omitted' do + it 'does not include request info in the exception' do + expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex| + expect(ex.response.keys).to contain_exactly( + :status, + :headers, + :body + ) + end + end + end + + context 'and when include_request option is explicitly set for instance' do + let(:middleware_options) { { include_request: true } } + + it 'includes request info in the exception' do + expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex| + expect(ex.response.keys).to contain_exactly( + :status, + :headers, + :body, + :request + ) + end + end + end + end end end end diff --git a/spec/faraday/response_spec.rb b/spec/faraday/response_spec.rb index 171594701..e3e2c2378 100644 --- a/spec/faraday/response_spec.rb +++ b/spec/faraday/response_spec.rb @@ -4,7 +4,7 @@ subject { Faraday::Response.new(env) } let(:env) do - Faraday::Env.from(status: 404, body: 'yikes', + Faraday::Env.from(status: 404, body: 'yikes', url: Faraday::Utils.URI('https://lostisland.github.io/faraday'), response_headers: { 'Content-Type' => 'text/plain' }) end @@ -30,6 +30,7 @@ it { expect(hash[:status]).to eq(subject.status) } it { expect(hash[:response_headers]).to eq(subject.headers) } it { expect(hash[:body]).to eq(subject.body) } + it { expect(hash[:url]).to eq(subject.env.url) } end describe 'marshal serialization support' do @@ -45,6 +46,7 @@ it { expect(loaded.env[:body]).to eq(env[:body]) } it { expect(loaded.env[:response_headers]).to eq(env[:response_headers]) } it { expect(loaded.env[:status]).to eq(env[:status]) } + it { expect(loaded.env[:url]).to eq(env[:url]) } end describe '#on_complete' do diff --git a/spec/faraday/utils/headers_spec.rb b/spec/faraday/utils/headers_spec.rb index ffe0c3313..238bfd990 100644 --- a/spec/faraday/utils/headers_spec.rb +++ b/spec/faraday/utils/headers_spec.rb @@ -56,27 +56,54 @@ it { expect(subject.delete('content-type')).to be_nil } end - describe '#parse' do - before { subject.parse(headers) } + describe '#dig' do + before { subject['Content-Type'] = 'application/json' } + + it { expect(subject&.dig('Content-Type')).to eq('application/json') } + it { expect(subject&.dig('CONTENT-TYPE')).to eq('application/json') } + it { expect(subject&.dig(:content_type)).to eq('application/json') } + it { expect(subject&.dig('invalid')).to be_nil } + end + describe '#parse' do context 'when response headers leave http status line out' do let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" } + before { subject.parse(headers) } + it { expect(subject.keys).to eq(%w[Content-Type]) } it { expect(subject['Content-Type']).to eq('text/html') } it { expect(subject['content-type']).to eq('text/html') } end context 'when response headers values include a colon' do - let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://sushi.com/\r\n\r\n" } + let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://httpbingo.org/\r\n\r\n" } - it { expect(subject['location']).to eq('http://sushi.com/') } + before { subject.parse(headers) } + + it { expect(subject['location']).to eq('http://httpbingo.org/') } end context 'when response headers include a blank line' do let(:headers) { "HTTP/1.1 200 OK\r\n\r\nContent-Type: text/html\r\n\r\n" } + before { subject.parse(headers) } + it { expect(subject['content-type']).to eq('text/html') } end + + context 'when response headers include already stored keys' do + let(:headers) { "HTTP/1.1 200 OK\r\nX-Numbers: 123\r\n\r\n" } + + before do + h = subject + h[:x_numbers] = 8 + h.parse(headers) + end + + it do + expect(subject[:x_numbers]).to eq('8, 123') + end + end end end diff --git a/spec/faraday/utils_spec.rb b/spec/faraday/utils_spec.rb index c3da7462c..bf7499eb8 100644 --- a/spec/faraday/utils_spec.rb +++ b/spec/faraday/utils_spec.rb @@ -4,7 +4,7 @@ describe 'headers parsing' do let(:multi_response_headers) do "HTTP/1.x 500 OK\r\nContent-Type: text/html; charset=UTF-8\r\n" \ - "HTTP/1.x 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n" + "HTTP/1.x 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n" end it 'parse headers for aggregated responses' do @@ -53,4 +53,66 @@ expect(headers).not_to have_key('authorization') end end + + describe '.deep_merge!' do + let(:connection_options) { Faraday::ConnectionOptions.new } + let(:url) do + { + url: 'http://example.com/abc', + headers: { 'Mime-Version' => '1.0' }, + request: { oauth: { consumer_key: 'anonymous' } }, + ssl: { version: '2' } + } + end + + it 'recursively merges the headers' do + connection_options.headers = { user_agent: 'My Agent 1.0' } + deep_merge = Faraday::Utils.deep_merge!(connection_options, url) + + expect(deep_merge.headers).to eq('Mime-Version' => '1.0', user_agent: 'My Agent 1.0') + end + + context 'when a target hash has an Options Struct value' do + let(:request) do + { + params_encoder: nil, + proxy: nil, + bind: nil, + timeout: nil, + open_timeout: nil, + read_timeout: nil, + write_timeout: nil, + boundary: nil, + oauth: { consumer_key: 'anonymous' }, + context: nil, + on_data: nil + } + end + let(:ssl) do + { + verify: nil, + ca_file: nil, + ca_path: nil, + verify_mode: nil, + cert_store: nil, + client_cert: nil, + client_key: nil, + certificate: nil, + private_key: nil, + verify_depth: nil, + version: '2', + min_version: nil, + max_version: nil, + verify_hostname: nil + } + end + + it 'does not overwrite an Options Struct value' do + deep_merge = Faraday::Utils.deep_merge!(connection_options, url) + + expect(deep_merge.request.to_h).to eq(request) + expect(deep_merge.ssl.to_h).to eq(ssl) + end + end + end end diff --git a/spec/faraday_spec.rb b/spec/faraday_spec.rb index 8b603ebbd..c3583f184 100644 --- a/spec/faraday_spec.rb +++ b/spec/faraday_spec.rb @@ -18,10 +18,16 @@ end it 'uses method_missing on Faraday if there is no proxyable method' do - expect { Faraday.this_method_does_not_exist }.to raise_error( - NoMethodError, - "undefined method `this_method_does_not_exist' for Faraday:Module" - ) + expected_message = + if RUBY_VERSION >= '3.4' + "undefined method 'this_method_does_not_exist' for module Faraday" + elsif RUBY_VERSION >= '3.3' + "undefined method `this_method_does_not_exist' for module Faraday" + else + "undefined method `this_method_does_not_exist' for Faraday:Module" + end + + expect { Faraday.this_method_does_not_exist }.to raise_error(NoMethodError, expected_message) end it 'proxied methods can be accessed' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5fcddd5b0..1b80ea248 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,14 +29,15 @@ minimum_coverage_by_file 26 end -# Ensure all /lib files are loaded -# so they will be included in the test coverage report. -Dir['./lib/**/*.rb'].sort.each { |file| require file } - require 'faraday' require 'pry' -Dir['./spec/support/**/*.rb'].sort.each { |f| require f } +# Ensure all /lib files are loaded +# so they will be included in the test coverage report. +Dir['./lib/**/*.rb'].each { |file| require file } + +# Load all Rspec support files +Dir['./spec/support/**/*.rb'].each { |file| require file } RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate diff --git a/spec/support/fake_safe_buffer.rb b/spec/support/fake_safe_buffer.rb index 62a56aa58..69afd6ea9 100644 --- a/spec/support/fake_safe_buffer.rb +++ b/spec/support/fake_safe_buffer.rb @@ -8,7 +8,7 @@ def to_s def gsub(regex) string.gsub(regex) do - match, = $&, '' =~ /a/ + match, = Regexp.last_match(0), '' =~ /a/ # rubocop:disable Performance/StringInclude yield(match) end end diff --git a/spec/support/faraday_middleware_subclasses.rb b/spec/support/faraday_middleware_subclasses.rb new file mode 100644 index 000000000..4e63f61a1 --- /dev/null +++ b/spec/support/faraday_middleware_subclasses.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module FaradayMiddlewareSubclasses + class SubclassNoOptions < Faraday::Middleware + end + + class SubclassOneOption < Faraday::Middleware + DEFAULT_OPTIONS = { some_other_option: false }.freeze + end + + class SubclassTwoOptions < Faraday::Middleware + DEFAULT_OPTIONS = { some_option: true, some_other_option: false }.freeze + end +end + +Faraday::Response.register_middleware(no_options: FaradayMiddlewareSubclasses::SubclassNoOptions) +Faraday::Response.register_middleware(one_option: FaradayMiddlewareSubclasses::SubclassOneOption) +Faraday::Response.register_middleware(two_options: FaradayMiddlewareSubclasses::SubclassTwoOptions) diff --git a/spec/support/helper_methods.rb b/spec/support/helper_methods.rb index d63bd59f9..0f5d4f5a5 100644 --- a/spec/support/helper_methods.rb +++ b/spec/support/helper_methods.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'multipart_parser/reader' - module Faraday module HelperMethods def self.included(base) @@ -86,41 +84,6 @@ def capture_warnings end end - def multipart_file - Faraday::FilePart.new(__FILE__, 'text/x-ruby') - end - - # parse boundary out of a Content-Type header like: - # Content-Type: multipart/form-data; boundary=gc0p4Jq0M2Yt08jU534c0p - def parse_multipart_boundary(ctype) - MultipartParser::Reader.extract_boundary_value(ctype) - end - - # parse a multipart MIME message, returning a hash of any multipart errors - def parse_multipart(boundary, body) - reader = MultipartParser::Reader.new(boundary) - result = { errors: [], parts: [] } - def result.part(name) - hash = self[:parts].detect { |h| h[:part].name == name } - [hash[:part], hash[:body].join] - end - - reader.on_part do |part| - result[:parts] << thispart = { - part: part, - body: [] - } - part.on_data do |chunk| - thispart[:body] << chunk - end - end - reader.on_error do |msg| - result[:errors] << msg - end - reader.write(body) - result - end - def method_with_body?(method) self.class.method_with_body?(method) end diff --git a/spec/support/shared_examples/adapter.rb b/spec/support/shared_examples/adapter.rb index 33f261182..625690883 100644 --- a/spec/support/shared_examples/adapter.rb +++ b/spec/support/shared_examples/adapter.rb @@ -33,20 +33,21 @@ let(:protocol) { ssl_mode? ? 'https' : 'http' } let(:remote) { "#{protocol}://example.com" } + let(:stub_remote) { remote } let(:conn) do conn_options[:ssl] ||= {} - conn_options[:ssl][:ca_file] ||= ENV['SSL_FILE'] + conn_options[:ssl][:ca_file] ||= ENV.fetch('SSL_FILE', nil) + conn_options[:ssl][:verify_hostname] ||= ENV['SSL_VERIFY_HOSTNAME'] == 'yes' Faraday.new(remote, conn_options) do |conn| - conn.request :multipart conn.request :url_encoded conn.response :raise_error conn.adapter described_class, *adapter_options end end - let!(:request_stub) { stub_request(http_method, remote) } + let!(:request_stub) { stub_request(http_method, stub_remote) } after do expect(request_stub).to have_been_requested unless request_stub.disabled? diff --git a/spec/support/shared_examples/request_method.rb b/spec/support/shared_examples/request_method.rb index 8e2828a21..afa337677 100644 --- a/spec/support/shared_examples/request_method.rb +++ b/spec/support/shared_examples/request_method.rb @@ -1,5 +1,19 @@ # frozen_string_literal: true +shared_examples 'proxy examples' do + it 'handles requests with proxy' do + res = conn.public_send(http_method, '/') + + expect(res.status).to eq(200) + end + + it 'handles proxy failures' do + request_stub.to_return(status: 407) + + expect { conn.public_send(http_method, '/') }.to raise_error(Faraday::ProxyAuthError) + end +end + shared_examples 'a request method' do |http_method| let(:query_or_body) { method_with_body?(http_method) ? :body : :query } let(:response) { conn.public_send(http_method, '/') } @@ -13,8 +27,8 @@ end it 'handles headers with multiple values' do - request_stub.to_return(headers: { 'Set-Cookie' => 'one, two' }) - expect(response.headers['set-cookie']).to eq('one, two') + request_stub.to_return(headers: { 'Set-Cookie' => 'name=value' }) + expect(response.headers['set-cookie']).to eq('name=value') end it 'retrieves the response headers' do @@ -65,7 +79,7 @@ on_feature :request_body_on_query_methods do it 'sends request body' do - request_stub.with(Hash[:body, 'test']) + request_stub.with({ body: 'test' }) res = if query_or_body == :body conn.public_send(http_method, '/', 'test') else @@ -79,7 +93,7 @@ it 'sends url encoded parameters' do payload = { name: 'zack' } - request_stub.with(Hash[query_or_body, payload]) + request_stub.with({ query_or_body => payload }) res = conn.public_send(http_method, '/', payload) if query_or_body == :query expect(res.env.request_body).to be_nil @@ -90,7 +104,7 @@ it 'sends url encoded nested parameters' do payload = { name: { first: 'zack' } } - request_stub.with(Hash[query_or_body, payload]) + request_stub.with({ query_or_body => payload }) conn.public_send(http_method, '/', payload) end @@ -112,19 +126,6 @@ expect { conn.public_send(http_method, '/') }.to raise_error(exc) end - # Can't send files on get, head and delete methods - if method_with_body?(http_method) - it 'sends files' do - payload = { uploaded_file: multipart_file } - request_stub.with(headers: { 'Content-Type' => %r{\Amultipart/form-data} }) do |request| - # WebMock does not support matching body for multipart/form-data requests yet :( - # https://github.com/bblimke/webmock/issues/623 - request.body =~ /RubyMultipartPost/ - end - conn.public_send(http_method, '/', payload) - end - end - on_feature :reason_phrase_parse do it 'parses the reason phrase' do request_stub.to_return(status: [200, 'OK']) @@ -152,12 +153,19 @@ let(:streamed) { [] } context 'when response is empty' do - it do + it 'handles streaming' do + env = nil conn.public_send(http_method, '/') do |req| - req.options.on_data = proc { |*args| streamed << args } + req.options.on_data = proc do |chunk, size, block_env| + streamed << [chunk, size] + env ||= block_env + end end expect(streamed).to eq([['', 0]]) + # TODO: enable this after updating all existing adapters to the new streaming API + # expect(env).to be_a(Faraday::Env) + # expect(env.status).to eq(200) end end @@ -165,12 +173,19 @@ before { request_stub.to_return(body: big_string) } it 'handles streaming' do + env = nil response = conn.public_send(http_method, '/') do |req| - req.options.on_data = proc { |*args| streamed << args } + req.options.on_data = proc do |chunk, size, block_env| + streamed << [chunk, size] + env ||= block_env + end end expect(response.body).to eq('') check_streaming_response(streamed, chunk_size: 16 * 1024) + # TODO: enable this after updating all existing adapters to the new streaming API + # expect(env).to be_a(Faraday::Env) + # expect(env.status).to eq(200) end end end @@ -185,11 +200,11 @@ @payload2 = { b: '2' } request_stub - .with(Hash[query_or_body, @payload1]) + .with({ query_or_body => @payload1 }) .to_return(body: @payload1.to_json) stub_request(http_method, remote) - .with(Hash[query_or_body, @payload2]) + .with({ query_or_body => @payload2 }) .to_return(body: @payload2.to_json) conn.in_parallel do @@ -218,17 +233,31 @@ end end - it 'handles requests with proxy' do - conn_options[:proxy] = 'http://google.co.uk' + context 'when a proxy is provided as option' do + before do + conn_options[:proxy] = 'http://env-proxy.com:80' + end - res = conn.public_send(http_method, '/') - expect(res.status).to eq(200) + include_examples 'proxy examples' end - it 'handles proxy failures' do - conn_options[:proxy] = 'http://google.co.uk' - request_stub.to_return(status: 407) + context 'when http_proxy env variable is set' do + let(:proxy_url) { 'http://env-proxy.com:80' } - expect { conn.public_send(http_method, '/') }.to raise_error(Faraday::ProxyAuthError) + around do |example| + with_env 'http_proxy' => proxy_url do + example.run + end + end + + include_examples 'proxy examples' + + context 'when the env proxy is ignored' do + around do |example| + with_env_proxy_disabled(&example) + end + + include_examples 'proxy examples' + end end end diff --git a/spec/support/webmock_rack_app.rb b/spec/support/webmock_rack_app.rb deleted file mode 100644 index a3212c71c..000000000 --- a/spec/support/webmock_rack_app.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -# Rack app used to test the Rack adapter. -# Uses Webmock to check if requests are registered, in which case it returns -# the registered response. -class WebmockRackApp - def call(env) - req_signature = WebMock::RequestSignature.new( - req_method(env), - req_uri(env), - body: req_body(env), - headers: req_headers(env) - ) - - WebMock::RequestRegistry - .instance - .requested_signatures - .put(req_signature) - - process_response(req_signature) - end - - def req_method(env) - env['REQUEST_METHOD'].downcase.to_sym - end - - def req_uri(env) - scheme = env['rack.url_scheme'] - host = env['SERVER_NAME'] - port = env['SERVER_PORT'] - path = env['PATH_INFO'] - query = env['QUERY_STRING'] - - url = +"#{scheme}://#{host}:#{port}#{path}" - url += "?#{query}" if query - - uri = WebMock::Util::URI.heuristic_parse(url) - uri.path = uri.normalized_path.gsub('[^:]//', '/') - uri - end - - def req_headers(env) - http_headers = env.select { |k, _| k.start_with?('HTTP_') } - .map { |k, v| [k[5..-1], v] } - .to_h - - special_headers = Faraday::Adapter::Rack::SPECIAL_HEADERS - http_headers.merge(env.select { |k, _| special_headers.include?(k) }) - end - - def req_body(env) - env['rack.input'].read - end - - def process_response(req_signature) - res = WebMock::StubRegistry.instance.response_for_request(req_signature) - - if res.nil? && req_signature.uri.host == 'localhost' - raise Faraday::ConnectionFailed, 'Trying to connect to localhost' - end - - raise WebMock::NetConnectNotAllowedError, req_signature unless res - - raise Faraday::TimeoutError if res.should_timeout - - [res.status[0], res.headers || {}, [res.body || '']] - end -end