[
  {
    "path": ".editorconfig",
    "content": "[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".github/scripts/generate_docs.sh",
    "content": "#!/usr/bin/env sh\n\nDOCS_PATH=\"docs\"\nTAGS=$(git tag -l)\nDEFAULT_VERSION=$(git tag -l | sort -V | tail -n 1)\nDEFAULT_VERSION=$(echo $DEFAULT_VERSION | awk '{gsub(/^v/, \"\"); print}')\n\nif [ -z \"$DEFAULT_VERSION\" ]; then\n  echo \"Not fount default version\"\n  exit 1\nfi\n\n# Clean up\nrm -rf $DOCS_PATH\nmkdir -p $DOCS_PATH\n\n# Generate master docs\nCOMMIT_DATE=$(git log -1 --format=%ci)\nMASTER_COMMIT_HASH=$(git rev-parse --short HEAD)\nCOMMIT_STATUS=\"[#${MASTER_COMMIT_HASH}](${GH_REF}/commit/${MASTER_COMMIT_HASH})\"\nsed -i -e \"s/latest commit/$(echo ${COMMIT_STATUS} | sed -e \"s/\\//\\\\\\\\\\//g\") (${COMMIT_DATE})/\" README.md\ncrystal docs --output=\"${DOCS_PATH}/master\" --project-version=\"master-dev\" --json-config-url=\"/halite/version.json\"\ngit reset --hard\n\nversion_gt () {\n  test \"$(printf '%s\\n' \"$@\" | sort -V | head -n 1)\" != \"$1\"\n}\n\necho \"{\\\"versions\\\": [\" > docs/version.json\necho \"{\\\"name\\\": \\\"master-dev\\\", \\\"url\\\": \\\"/halite/master/\\\", \\\"released\\\": false}\" >> docs/version.json\n\n# Generate version docs\nfor TAG in $(git tag -l | sort -r -V); do\n  NAME=$(echo $TAG | awk '{gsub(/^v/, \"\"); print}')\n\n  # Crystal version 1.0 complie version must great than 0.12.0.\n  if version_gt $NAME \"0.11.0\"; then\n    git checkout -b $NAME $TAG\n\n    echo \",{\\\"name\\\": \\\"$NAME\\\", \\\"url\\\": \\\"/halite/$NAME/\\\"}\" >> docs/version.json\n\n    COMMIT_STATUS=\"[${TAG}](${GH_REF}/blob/master/CHANGELOG.md)\"\n    sed -i -e \"s/latest commit/$(echo ${COMMIT_STATUS} | sed -e \"s/\\//\\\\\\\\\\//g\")/\" README.md\n    crystal docs --output=\"${DOCS_PATH}/${NAME}\" --project-version=\"${NAME}\" --json-config-url=\"/halite/version.json\"\n    git reset --hard\n    git checkout master\n    git branch -d $NAME\n  fi\ndone\n\necho \"]}\" >> docs/version.json\n\necho \"<html>\n<header>\n  <meta http-equiv='Refresh' content='0; url='${GH_URL}/${DEFAULT_VERSION}/' />\n  <script language='javascript' type='text/javascript'>\n    window.location.href='${GH_URL}/${DEFAULT_VERSION}/';\n  </script>\n</header>\n<body>\n<p><a href='${GH_URL}/${DEFAULT_VERSION}/'>Redirect to ${DEFAULT_VERSION}</a></p>\n</body>\n</html>\" > \"${DOCS_PATH}/index.html\"\n"
  },
  {
    "path": ".github/workflows/api-document.yml",
    "content": "name: Deploy API documents\non:\n  push:\n    paths-ignore:\n      - \"benchmarks/**\"\n    branches:\n      - \"master\"\n    tags:\n      - \"v*\"\n\nenv:\n  DOCS_PATH: docs\n  GH_REF: https://github.com/icyleaf/halite\n  GH_URL: https://icyleaf.github.io/halite\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@master\n      with:\n        # fetch all tags and branches\n        fetch-depth: 0\n    - uses: oprypin/install-crystal@v1\n    - name: Generate\n      id: generate\n      run: |\n        chmod +x .github/scripts/generate_docs.sh\n        ./.github/scripts/generate_docs.sh\n    - name: Deploy\n      uses: peaceiris/actions-gh-pages@v3\n      with:\n        github_token: ${{ secrets.GITHUB_TOKEN }}\n        publish_dir: ${{ env.DOCS_PATH }}\n\n\n"
  },
  {
    "path": ".github/workflows/linux-ci.yml",
    "content": "name: Linux CI\non:\n  push:\n    paths-ignore:\n      - \"benchmarks/**\"\n    branches:\n      - \"master\"\n  pull_request:\n    branches: \"*\"\n\njobs:\n  specs:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        crystal: [ '1.0.0', 'latest', 'nightly' ]\n    name: Crystal ${{ matrix.crystal }} tests\n    steps:\n    - uses: actions/checkout@master\n    - uses: oprypin/install-crystal@v1\n      with:\n        crystal: ${{ matrix.crystal }}\n    - name: Install dependencies\n      run: shards install\n    - name: Run tests\n      run: crystal spec --error-on-warnings --error-trace\n    - name: Run code format check\n      run: |\n        if ! crystal tool format --check; then\n          crystal tool format\n          git diff\n          exit 1\n        fi\n"
  },
  {
    "path": ".github/workflows/release-version.yml",
    "content": "name: Deploy new release\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@master\n    - name: Create Release\n      id: create_release\n      uses: actions/create-release@v1\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        tag_name: ${{ github.ref }}\n        release_name: Release ${{ github.ref }}\n        draft: false\n        prerelease: false\n\n"
  },
  {
    "path": ".gitignore",
    "content": "docs/\nlib/\nbin/\nlogs/\n.shards/\n\n# Local test file\nmain.cr\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in application that uses them\nshard.lock\n.history/\ncache/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)\nand this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n> List all changes before release a new version.\n\n### Todo\n\n- [ ] Rewrite HTTP Connection\n  - [ ] New Connection for Halite\n  - [x] Proxy support\n  - [ ] Reuse connection\n\n## [0.12.1] (2021-11-04)\n\n### Fixed\n\n- Reduce alloc too many memory. [#108](https://github.com/icyleaf/halite/pull/108) (thanks @[wolfgang371](https://github.com/wolfgang371))\n\n## [0.12.0] (2021-03-24)\n\n### Fixed\n\n- Compatibility with Crystal 1.0.\n\n## [0.11.0] (2021-02-18)\n\n> Finally, the major version was out! Happy new year!\n\n### Changed\n\n- **[breaking changing]** Drop file logging in favor of Crystal's [Log](https://crystal-lang.org/api/0.36.1/Log.html). (removed `.logging(file: )`, use `.logging(for: )` instead)  [#101](https://github.com/icyleaf/halite/pull/101) (thanks @[oprypin](https://github.com/oprypin))\n- Pre-read `TZ` environment value to convert timestamp's timezone during logging output. [#102](https://github.com/icyleaf/halite/pull/102)\n- Crystal 0.34.x support.\n\n## [0.10.9] (2021-02-01)\n\n### Fixed\n\n- `timeout` fail to match argument type. [#97](https://github.com/icyleaf/halite/issues/97) (thanks @[oprypin](https://github.com/oprypin))\n- Compatibility with Crystal 0.36.0.\n\n## [0.10.8] (2020-12-22)\n\n### Fixed\n\n- Resolve path of endpoint ending without slash. [#94](https://github.com/icyleaf/halite/issues/94) (thanks @[mipmip](https://github.com/mipmip))\n\n## [0.10.7] (2020-12-08)\n### Fixed\n\n- Fix initial status_message. [#91](https://github.com/icyleaf/halite/issues/91) (thanks @[oprypin](https://github.com/oprypin))\n\n## [0.10.6] (2020-11-24)\n### Fixed\n\n- Improve resolve of URI. [#88](https://github.com/icyleaf/halite/issues/88) (thanks @[oprypin](https://github.com/oprypin))\n\n## [0.10.5] (2020-04-15)\n\n### Fixed\n\n- Compatibility with Crystal 0.34.0.\n\n## [0.10.4] (2019-09-26)\n\n### Fixed\n\n- Compatibility with Crystal 0.31.0.\n\n## [0.10.3] (2019-08-12)\n\n### Fixed\n\n- Compatibility with Crystal 0.30.0.\n\n## [0.10.2] (2019-06-24)\n\n### Fixed\n\n- Fixed Basic Auth creates bad headers in crystal 0.29.0. [#73](https://github.com/icyleaf/halite/pull/73) (thanks @[kalinon](https://github.com/kalinon))\n- Fixed use one shared options in multiple instanced `Halite::Client`. [#72](https://github.com/icyleaf/halite/issues/72) (thanks @[qszhu](https://github.com/qszhu))\n\n## [0.10.1] (2019-05-28)\n\n### Fixed\n\n- Fixed duplica query and backslash when reuse client. [#67](https://github.com/icyleaf/halite/pull/67), [#68](https://github.com/icyleaf/halite/issues/68) (thanks @[watzon](https://github.com/watzon))\n- Fixed no effect to call `logging(true)` method in Crystal 0.28. [#69](https://github.com/icyleaf/halite/issues/69)\n\n## [0.10.0] (2019-05-20)\n\n### Added\n\n- Add `endpoint` chainable method, also add it as configuration option to reuse client. [#66](https://github.com/icyleaf/halite/pull/66)\n\n## [0.9.2] (2019-05-20)\n\n### Fixed\n\n- Compatibility with Crystal 0.28.0\n\n### Changed\n\n- Drop Crystal 0.25.x, 0.26.x, 0.27.x support.\n\n## [0.9.1] (2019-01-14)\n\n> Minor typo fix (same as v0.9.0)\n\n### Fixed\n\n- Correct version both in `shard.yml` and `version.cr`. (thanks @[matthewmcgarvey](https://github.com/matthewmcgarvey))\n- Update basic auth example in `README.md`. (thanks @[matthewmcgarvey](https://github.com/matthewmcgarvey))\n\n## [0.9.0] (2018-12-21)\n\n> New features with performance improved.\n\n### Added\n\n- Add streaming requests (feature to store binary data chunk by chunk) [#53](https://github.com/icyleaf/halite/pull/53)\n- Add `user_agent` to Chainable methods. [#55](https://github.com/icyleaf/halite/pull/55)\n\n### Fixed\n\n- Fix overwrite the value with default headers when use `merge` or  `merge!` method in `Halite::Options`. [#54](https://github.com/icyleaf/halite/pull/54)\n\n### Changed\n\n- Remove default headers in `Halite::Options`.\n- Move header `User-Agent` to `Halite::Request`.\n- Change header `Connection` from \"keep-alive\" to \"close\" to `Halite::Request`.\n- Remove header `Accept`.\n\n## [0.8.0] (2018-11-30)\n\n> Compatibility with Crystal 0.27 and serious bugfix.\n\n### Changed\n\n- **[breaking changing]** Rename `logger` to `logging`, `with_logger` to `with_logging`. [#52](https://github.com/icyleaf/halite/pull/52)\n- **[breaking changing]** Remove `logging` argument in `Halite::Options.new` and `Halite::Client.new`. [#51](https://github.com/icyleaf/halite/pull/51)\n- **[breaking changing]** Remove `logging?` method in `Halite::Options`, use `logging` method instead. [#51](https://github.com/icyleaf/halite/pull/51)\n- Change `logging` behavior check if features is exists any class of superclasses is `Halite::Logging` instead of given a Bool type.\n- Rename prefix `X-Cache` to `X-Halite-Cache` in cache feature.\n\n### Added\n\n- Allow `timeout` method passed single `read` or `connect` method.\n- Add `merge!` and `dup` methods in `Halite::Options`. [#51](https://github.com/icyleaf/halite/pull/51)\n\n### Fixed\n\n- Fix duplice add \"Content-Type\" into header during request. [#50](https://github.com/icyleaf/halite/pull/50)\n- Fix non overwrite value of headers use `Halite::Options.merge` method. [#50](https://github.com/icyleaf/halite/pull/50)\n- Fix always overwrite and return merged option in a instanced class(session mode), see updated note in [Session](https://github.com/icyleaf/halite#sessions).\n\n### Tested\n\n- Compatibility with Crystal 0.27\n- Add specs with Crystal 0.25, 0.26 and 0.27 in Circle CI.\n\n## [0.7.5] (2018-10-31)\n\n### Changed\n\n- **[breaking changing]** Rename argument name `ssl` to `tls` in `Halite::Client`/`Halite::Options`/`Halite::Chainable`.\n\n### Fixed\n\n- Fix new a `Halite::Client` instance with empty block return `Nil`. [#44](https://github.com/icyleaf/halite/issues/44)\n\n## [0.7.4] (2018-10-30)\n\n### Fixed\n\n- Fix typos in document and comments. [#43](https://github.com/icyleaf/halite/issues/43) (thanks @[GloverDonovan](https://github.com/GloverDonovan))\n\n## [0.7.3] (2018-10-18)\n\n### Fixed\n\n- Fix json payloads with sub hash/array/namedtupled. [#41](https://github.com/icyleaf/halite/issues/41) (thanks @[fusillicode](https://github.com/fusillicode))\n\n## [0.7.2] (2018-09-14)\n\n> Minor bugfix :bug:\n\n### Changed\n\n- **[breaking changing]** Renamed `#to_h` to `#to_flat_h` to avoid confict in `HTTP::Params` extension. [#39](https://github.com/icyleaf/halite/issues/39)\n\n### Fixed\n\n- Fix cast from NamedTuple(work: String) to Halite::Options::Type failed with params/json/form. [#38](https://github.com/icyleaf/halite/issues/38)\n\n## [0.7.1] (2018-09-04)\n\n### Changed\n\n- Return empty hash for an empty named tuple.\n\n### Fixed\n\n- Fix send cookie during requesting in session mode. (thanks @[megatux](https://github.com/megatux))\n- Fix pass current options instead of instance variable.\n- Fix move named tuple extension to src path.\n\n## [0.7.0] (2018-09-03)\n\n> Features support :tada:\n\n### Changed\n\n- **[breaking changing]** Change instance `Halite::Client` with block behavior. [#33](https://github.com/icyleaf/halite/issues/33)\n- **[breaking changing]** Renamed argument name `adapter` to `format` in `#logger` chainable method.\n- Move logger into features.\n\n### Added\n\n- Add features (aka middleware) support, you can create monitor or interceptor. [#29](https://github.com/icyleaf/halite/issues/29)\n- Add cache feature. [#24](https://github.com/icyleaf/halite/issues/24)\n- Add `#logging` in chainable method.\n\n### Fixed\n\n- Add misisng `#request` method with headers, params, form, json, raw, ssl arguments.\n- Fix do not overwrite default headers with exists one by using `Halite::Options.merge`.\n- Fix append response to history only with redirect uri. (thanks @[j8r](https://github.com/j8r))\n- Typo and correct words in README. (thanks @[megatux](https://github.com/megatux))\n\n## [0.6.0] (2018-08-24)\n\n> Improve performance with :see_no_evil:\n\n### Changed\n\n- **[breaking changing]** Set `logger` to nil when instance a `Halite::Options`, it throws a `Halite::Error` exception if enable `logging`.\n- Change `Halite::Options` accepts argument inside. no effect for users. [#27](https://github.com/icyleaf/halite/pull/27)\n- Wrap all exception class into a module, better for reading document.\n\n### Fixed\n\n- Fix always return `#` with `#full_path` if fragment not exists in `Halite::Request`.\n- Fix always overwrite with default headers with `#merge` in `Halite::Options`\n\n### Tested\n\n- Compatibility with Crystal 0.26\n\n## [0.5.0] (2018-07-03)\n\n### Changed\n\n- New logger system and json logger support, see [#19](https://github.com/icyleaf/halite/pull/19).\n- **[breaking changing]** Change verb request behavior:\n  - `get`, `head` only accepts `#params` argument.\n  - `post`, `put`, `delete`, `patch`, `options` accepts `#params`, `#form`, `#json` and `#raw` arguments.\n\n### Added\n\n- Add request [#raw](https://github.com/icyleaf/halite/#raw-string) string support. [#20](https://github.com/icyleaf/halite/issues/20) (thanks @[wirrareka](https://github.com/wirrareka))\n\n## [0.4.0] (2018-06-27)\n\n### Changed\n\n- **[breaking changing]** Remove `#mime_type` duplicate with `#content_type` in `Halite::Response`.\n- Change write log file use append mode by default, it could be change by param.\n- Change logger formatter to easy identify category(request/response).\n\n### Added\n\n- Add [#links](https://github.com/icyleaf/halite/#link-headers) to `Halite::Response` to fetch link headers.\n- Add [#raise_for_status](https://github.com/icyleaf/halite/#raise-for-status-code) to `Halite::Response`.\n- Support multiple files upload. [#14](https://github.com/icyleaf/halite/issues/14) (thanks @[BenDietze](https://github.com/BenDietze))\n- Add `#to_raw` to `Halite::Response` to dump a raw of response. [#15](https://github.com/icyleaf/halite/issues/15) (thanks @[BenDietze](https://github.com/BenDietze))\n- Support `OPTIONS` method (crystal 0.25.0+)\n- Append write log to a file section to README.\n\n### Fixed\n\n- Stripped the filename in a `multipart/form-data` body. [#16](https://github.com/icyleaf/halite/issues/16) (thanks @[BenDietze](https://github.com/BenDietze))\n- Fix `#domain` in `Halite::Request` with subdomain. [#17](https://github.com/icyleaf/halite/pull/17) (thanks @[007lva](https://github.com/007lva))\n- Create missing directories when use path to write log to a file.\n\n## [0.3.2] (2018-06-19)\n\n### Fixed\n\nCompatibility with Crystal 0.25\n\n## [0.3.1] (2017-12-13)\n\n### Added\n\n- Set `Options.default_headers` to be public method.\n- Accept tuples options in `Options.new`.\n- Accept `follow`/`follow_strict` in `Options.new`.\n- Accept options block in `Options.new`.\n- Add logger during request and response (see [usage](README.md#logging)).\n- Alias method `Options.read_timeout` to `Options::Timeout.read`.\n- Alias method `Options.read_timeout=` to `Options::Timeout.read=`.\n- Alias method `Options.connect_timeout` to `Options::Timeout.connect`.\n- Alias method `Options.connect_timeout` to `Options::Timeout.connect=`.\n- Alias method `Options.follow=` to `Options::Timeout.follow.hops=`.\n- Alias method `Options.follow_strict` to `Options::Timeout.follow.strict`.\n- Alias method `Options.follow_strict=` to `Options::Timeout.follow.strict=`.\n\n### Fixed\n\n- Fix store **Set-Cookies** in response and set **Cookies** in request in better way.\n- Fix cant not set connect/read timeout in `Options.new`.\n- Fix cant not overwrite default headers in `Options.new`.\n- Fix `Options.clear!` was not clear everything and restore default headers.\n\n## [0.2.0] (2017-11-28)\n\n### Changed\n\n- `HTTP::Headers#to_h` return string with each key if it contains one in array. ([commit#e057c47c](https://github.com/icyleaf/halite/commit/e057c47c4b587b27b2bae6871a1968299ce348f5))\n\n### Added\n\n- Add `Response#mime_type` method.\n- Add `Response#history` method to support full history of redirections. ([#8](https://github.com/icyleaf/halite/issues/8))\n- Add `Response#parse` method that it better body parser of response with json and write custom adapter for MIME type. ([#9](https://github.com/icyleaf/halite/issues/9))\n\n### Fixed\n\n- Fix issue to first char of redirect uri is not slash(/). ([#11](https://github.com/icyleaf/halite/issues/11))\n- Fix raise unsafe verbs in strict mode.\n\n## [0.1.5] (2017-10-11)\n\n### Changed\n\n- Only store cookies in Sessions shards. ([#7](https://github.com/icyleaf/halite/issues/7))\n\n### Added\n\n- Add `TLS/SSL` support (based on [HTTP::Client.new(uri : URI, tls = nil)](https://crystal-lang.org/api/0.23.1/HTTP/Client.html#new%28uri%3AURI%2Ctls%3Dnil%29-class-method)).\n- Add `UnsupportedMethodError/UnsupportedSchemeError` exceptions.\n\n### Fixed\n\n- Timeout with redirection. ([#7](https://github.com/icyleaf/halite/issues/7))\n- Compatibility with Crystal 0.24.0 (unreleased)\n\n## [0.1.3] (2017-10-09)\n\n### Changed\n\n- Always instance a new Options with each request in chainable methods.\n\n### Added\n\n- Add `accept` method.\n\n### Fixed\n\n- Fix `follow`(redirect uri) with full uri and relative path.\n- Fix always overwrite request headers with default values.\n- Fix always shard same options in any new call. (it only valid in chainable methods)\n\n## 0.1.2 (2017-09-18)\n\n- First beta version.\n\n[Unreleased]: https://github.com/icyleaf/halite/compare/v0.12.1...HEAD\n[0.12.1]: https://github.com/icyleaf/halite/compare/v0.12.0...v0.12.1\n[0.12.0]: https://github.com/icyleaf/halite/compare/v0.11.0...v0.12.0\n[0.11.0]: https://github.com/icyleaf/halite/compare/v0.10.9...v0.11.0\n[0.10.9]: https://github.com/icyleaf/halite/compare/v0.10.8...v0.10.9\n[0.10.8]: https://github.com/icyleaf/halite/compare/v0.10.7...v0.10.8\n[0.10.7]: https://github.com/icyleaf/halite/compare/v0.10.6...v0.10.7\n[0.10.6]: https://github.com/icyleaf/halite/compare/v0.10.5...v0.10.6\n[0.10.5]: https://github.com/icyleaf/halite/compare/v0.10.4...v0.10.5\n[0.10.4]: https://github.com/icyleaf/halite/compare/v0.10.3...v0.10.4\n[0.10.3]: https://github.com/icyleaf/halite/compare/v0.10.2...v0.10.3\n[0.10.2]: https://github.com/icyleaf/halite/compare/v0.10.1...v0.10.2\n[0.10.1]: https://github.com/icyleaf/halite/compare/v0.10.0...v0.10.1\n[0.10.0]: https://github.com/icyleaf/halite/compare/v0.9.2...v0.10.0\n[0.9.2]: https://github.com/icyleaf/halite/compare/v0.9.1...v0.9.2\n[0.9.1]: https://github.com/icyleaf/halite/compare/v0.9.0...v0.9.1\n[0.9.0]: https://github.com/icyleaf/halite/compare/v0.8.0...v0.9.0\n[0.8.0]: https://github.com/icyleaf/halite/compare/v0.7.5...v0.8.0\n[0.7.5]: https://github.com/icyleaf/halite/compare/v0.7.4...v0.7.5\n[0.7.4]: https://github.com/icyleaf/halite/compare/v0.7.3...v0.7.4\n[0.7.3]: https://github.com/icyleaf/halite/compare/v0.7.2...v0.7.3\n[0.7.2]: https://github.com/icyleaf/halite/compare/v0.7.1...v0.7.2\n[0.7.1]: https://github.com/icyleaf/halite/compare/v0.7.0...v0.7.1\n[0.7.0]: https://github.com/icyleaf/halite/compare/v0.6.0...v0.7.0\n[0.6.0]: https://github.com/icyleaf/halite/compare/v0.5.0...v0.6.0\n[0.5.0]: https://github.com/icyleaf/halite/compare/v0.4.0...v0.5.0\n[0.4.0]: https://github.com/icyleaf/halite/compare/v0.3.2...v0.4.0\n[0.3.2]: https://github.com/icyleaf/halite/compare/v0.3.1...v0.3.2\n[0.3.1]: https://github.com/icyleaf/halite/compare/v0.2.0...v0.3.1\n[0.2.0]: https://github.com/icyleaf/halite/compare/v0.1.5...v0.2.0\n[0.1.5]: https://github.com/icyleaf/halite/compare/v0.1.3...v0.1.5\n[0.1.3]: https://github.com/icyleaf/halite/compare/v0.1.2...v0.1.3\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at icyleaf.cn@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017-present icyleaf\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![halite-logo](https://github.com/icyleaf/halite/raw/master/halite-logo-small.png)\n\n# Halite\n\n[![Language](https://img.shields.io/badge/language-crystal-776791.svg)](https://github.com/crystal-lang/crystal)\n[![Tag](https://img.shields.io/github/tag/icyleaf/halite.svg)](https://github.com/icyleaf/halite/blob/master/CHANGELOG.md)\n[![Source](https://img.shields.io/badge/source-github-brightgreen.svg)](https://github.com/icyleaf/halite/)\n[![Document](https://img.shields.io/badge/document-api-brightgreen.svg)](https://icyleaf.github.io/halite/)\n[![Build Status](https://github.com/icyleaf/halite/workflows/Linux%20CI/badge.svg)](https://github.com/icyleaf/halite/actions?query=workflow%3A%22Linux+CI%22)\n\nHTTP Requests with a chainable REST API, built-in sessions and middleware written by [Crystal](https://crystal-lang.org/).\nInspired from the **awesome** Ruby's [HTTP](https://github.com/httprb/http)/[RESTClient](https://github.com/rest-client/rest-client)\nand Python's [requests](https://github.com/requests/requests).\n\nBuild in Crystal version `>= 1.0.0`, this document valid with latest commit.\n\n## Index\n\n<!-- TOC -->\n\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Making Requests](#making-requests)\n  - [Passing Parameters](#passing-parameters)\n    - [Query string parameters](#query-string-parameters)\n    - [Form data](#form-data)\n    - [File uploads (via form data)](#file-uploads-via-form-data)\n    - [JSON data](#json-data)\n    - [Raw String](#raw-string)\n  - [Passing advanced options](#passing-advanced-options)\n    - [Auth](#auth)\n    - [User Agent](#user-agent)\n    - [Headers](#headers)\n    - [Cookies](#cookies)\n    - [Redirects and History](#redirects-and-history)\n    - [Timeout](#timeout)\n  - [HTTPS](#https)\n  - [Response Handling](#response-handling)\n    - [Response Content](#response-content)\n    - [JSON Content](#json-content)\n    - [Parsing Content](#parsing-content)\n    - [Binary Data](#binary-data)\n  - [Error Handling](#error-handling)\n    - [Raise for status code](#raise-for-status-code)\n- [Middleware](#middleware)\n  - [Write a simple feature](#write-a-simple-feature)\n  - [Write a interceptor](#write-a-interceptor)\n- [Advanced Usage](#advanced-usage)\n  - [Configuring](#configuring)\n  - [Endpoint](#endpoint)\n  - [Sessions](#sessions)\n  - [Streaming Requests](#streaming-requests)\n  - [Logging](#logging)\n  - [Local Cache](#local-cache)\n  - [Link Headers](#link-headers)\n<!-- /TOC -->\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  halite:\n    github: icyleaf/halite\n```\n\n## Usage\n\n```crystal\nrequire \"halite\"\n```\n\n### Making Requests\n\nMake a GET request:\n\n```crystal\n# Direct get url\nHalite.get(\"http://httpbin.org/get\")\n\n# Support NamedTuple as query params\nHalite.get(\"http://httpbin.org/get\", params: {\n  language: \"crystal\",\n  shard: \"halite\"\n})\n\n# Also support Hash as query params\nHalite.get(\"http://httpbin.org/get\", headers: {\n    \"Private-Token\" => \"T0k3n\"\n  }, params: {\n    \"language\" => \"crystal\",\n    \"shard\" => \"halite\"\n  })\n\n# And support chainable\nHalite.header(private_token: \"T0k3n\")\n      .get(\"http://httpbin.org/get\", params: {\n        \"language\" => \"crystal\",\n        \"shard\" => \"halite\"\n      })\n```\n\nSee also all [chainable methods](https://icyleaf.github.io/halite/Halite/Chainable.html).\n\nMany other HTTP methods are available as well:\n\n- `get`\n- `head`\n- `post`\n- `put`\n- `delete`\n- `patch`\n- `options`\n\n### Passing Parameters\n\n#### Query string parameters\n\nUse the `params` argument to add query string parameters to requests:\n\n```crystal\nHalite.get(\"http://httpbin.org/get\", params: { \"firstname\" => \"Olen\", \"lastname\" => \"Rosenbaum\" })\n```\n\n#### Form data\n\nUse the `form` argument to pass data serialized as form encoded:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", form: { \"firstname\" => \"Olen\", \"lastname\" => \"Rosenbaum\" })\n```\n\n#### File uploads (via form data)\n\nTo upload files as if form data, construct the form as follows:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", form: {\n  \"username\" => \"Quincy\",\n  \"avatar\" => File.open(\"/Users/icyleaf/quincy_avatar.png\")\n})\n```\n\nIt is possible to upload multiple files:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", form: {\n  photos: [\n    File.open(\"/Users/icyleaf/photo1.png\"),\n    File.open(\"/Users/icyleaf/photo2.png\")\n  ],\n  album_name: \"samples\"\n})\n```\n\nOr pass the name with `[]`:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", form: {\n  \"photos[]\" => [\n    File.open(\"/Users/icyleaf/photo1.png\"),\n    File.open(\"/Users/icyleaf/photo2.png\")\n  ],\n  \"album_name\" => \"samples\"\n})\n```\n\nMultiple files can also be uploaded using both ways above, it depend on web server.\n\n#### JSON data\n\nUse the `json` argument to pass data serialized as body encoded:\n\n```crystal\nHalite.post(\"http://httpbin.org/post\", json: { \"firstname\" => \"Olen\", \"lastname\" => \"Rosenbaum\" })\n```\n\n#### Raw String\n\nUse the `raw` argument to pass raw string as body and set the `Content-Type` manually:\n\n```crystal\n# Set content-type to \"text/plain\" by default\nHalite.post(\"http://httpbin.org/post\", raw: \"name=Peter+Lee&address=%23123+Happy+Ave&language=C%2B%2B\")\n\n# Set content-type manually\nHalite.post(\"http://httpbin.org/post\",\n  headers: {\n    \"content-type\" => \"application/json\"\n  },\n  raw: %Q{{\"name\":\"Peter Lee\",\"address\":\"23123 Happy Ave\",\"language\":\"C++\"}}\n)\n```\n\n### Passing advanced options\n\n#### Auth\n\nUse the `#basic_auth` method to perform [HTTP Basic Authentication](http://tools.ietf.org/html/rfc2617) using a username and password:\n\n```crystal\nHalite.basic_auth(user: \"user\", pass: \"p@ss\").get(\"http://httpbin.org/get\")\n\n# We can pass a raw authorization header using the auth method:\nHalite.auth(\"Bearer dXNlcjpwQHNz\").get(\"http://httpbin.org/get\")\n```\n\n#### User Agent\n\nUse the `#user_agent` method to overwrite default one:\n\n```crystal\nHalite.user_agent(\"Crystal Client\").get(\"http://httpbin.org/user-agent\")\n```\n\n#### Headers\n\nHere are two way to passing headers data:\n\n##### 1. Use the `#headers` method\n\n```crystal\nHalite.headers(private_token: \"T0k3n\").get(\"http://httpbin.org/get\")\n\n# Also support Hash or NamedTuple\nHalite.headers({ \"private_token\" => \"T0k3n\" }).get(\"http://httpbin.org/get\")\n\n# Or\nHalite.headers({ private_token: \"T0k3n\" }).get(\"http://httpbin.org/get\")\n```\n\n##### 2. Use the `headers` argument in the available request method:\n\n```crystal\nHalite.get(\"http://httpbin.org/anything\" , headers: { private_token: \"T0k3n\" })\n\nHalite.post(\"http://httpbin.org/anything\" , headers: { private_token: \"T0k3n\" })\n```\n\n#### Cookies\n\n##### Passing cookies in requests\n\nThe `Halite.cookies` option can be used to configure cookies for a given request:\n\n```crystal\nHalite.cookies(session_cookie: \"6abaef100b77808ceb7fe26a3bcff1d0\")\n      .get(\"http://httpbin.org/headers\")\n```\n\n##### Get cookies in requests\n\nTo obtain the cookies(cookie jar) for a given response, call the `#cookies` method:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/cookies?set?session_cookie=6abaef100b77808ceb7fe26a3bcff1d0\")\npp r.cookies\n# => #<HTTP::Cookies:0x10dbed980 @cookies={\"session_cookie\" =>#<HTTP::Cookie:0x10ec20f00 @domain=nil, @expires=nil, @extension=nil, @http_only=false, @name=\"session_cookie\", @path=\"/\", @secure=false, @value=\"6abaef100b77808ceb7fe26a3bcff1d0\">}>\n```\n\n#### Redirects and History\n\n##### Automatically following redirects\n\nThe `Halite.follow` method can be used for automatically following redirects(Max up to 5 times):\n\n```crystal\n# Set the cookie and redirect to http://httpbin.org/cookies\nHalite.follow\n      .get(\"http://httpbin.org/cookies/set/name/foo\")\n```\n\n##### Limiting number of redirects\n\nAs above, set over 5 times, it will raise a `Halite::TooManyRedirectsError`, but you can change less if you can:\n\n```crystal\nHalite.follow(2)\n      .get(\"http://httpbin.org/relative-redirect/5\")\n```\n\n##### Disabling unsafe redirects\n\nIt only redirects with `GET`, `HEAD` request and returns a `300`, `301`, `302` by default, otherwise it will raise a `Halite::StateError`.\nWe can disable it to set `:strict` to `false` if we want any method(verb) requests, in which case the `GET` method(verb) will be used for\nthat redirect:\n\n```crystal\nHalite.follow(strict: false)\n      .post(\"http://httpbin.org/relative-redirect/5\")\n```\n\n##### History\n\n`Response#history` property list contains the `Response` objects that were created in order to complete the request.\nThe list is ordered from the oldest to most recent response.\n\n```crystal\nr = Halite.follow\n          .get(\"http://httpbin.org/redirect/3\")\n\nr.uri\n# => http://httpbin.org/get\n\nr.status_code\n# => 200\n\nr.history\n# => [\n#      #<Halite::Response HTTP/1.1 302 FOUND {\"Location\" => \"/relative-redirect/2\" ...>,\n#      #<Halite::Response HTTP/1.1 302 FOUND {\"Location\" => \"/relative-redirect/1\" ...>,\n#      #<Halite::Response HTTP/1.1 302 FOUND {\"Location\" => \"/get\" ...>,\n#      #<Halite::Response HTTP/1.1 200 OK    {\"Content-Type\" => \"application/json\" ...>\n#    ]\n```\n\n**NOTE**: It contains the `Response` object if you use `history` and HTTP was not a `30x`, For example:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/get\")\nr.history.size # => 0\n\nr = Halite.follow\n          .get(\"http://httpbin.org/get\")\nr.history.size # => 1\n```\n\n#### Timeout\n\nBy default, the Halite does not enforce timeout on a request.\nWe can enable per operation timeouts by configuring them through the chaining API.\n\nThe `connect` timeout is the number of seconds Halite will wait for our client to establish a connection to a remote server call on the socket.\n\nOnce our client has connected to the server and sent the HTTP request,\nthe `read` timeout is the number of seconds the client will wait for the server to send a response.\n\n```crystal\n# Separate set connect and read timeout\nHalite.timeout(connect: 3.0, read: 2.minutes)\n      .get(\"http://httpbin.org/anything\")\n\n# Boath set connect and read timeout\n# The timeout value will be applied to both the connect and the read timeouts.\nHalite.timeout(5)\n      .get(\"http://httpbin.org/anything\")\n```\n\n### HTTPS\n\nThe Halite supports HTTPS via Crystal's built-in OpenSSL module. All you have to do in order to use HTTPS is pass in an https://-prefixed URL.\n\nTo use client certificates, you can pass in a custom `OpenSSL::SSL::Context::Client` object containing the certificates you wish to use:\n\n```crystal\ntls = OpenSSL::SSL::Context::Client.new\ntls.ca_certificates = File.expand_path(\"~/client.crt\")\ntls.private_key = File.expand_path(\"~/client.key\")\n\nHalite.get(\"https://httpbin.org/anything\", tls: tls)\n```\n\n### Response Handling\n\nAfter an HTTP request, `Halite::Response` object have several useful methods. (Also see the [API documentation](https://icyleaf.github.io/halite/Halite/Response.html)).\n\n- **#body**: The response body.\n- **#body_io**: The response body io only available in streaming requests.\n- **#status_code**: The HTTP status code.\n- **#content_type**: The content type of the response.\n- **#content_length**: The content length of the response.\n- **#cookies**: A `HTTP::Cookies` set by server.\n- **#headers**: A `HTTP::Headers` of the response.\n- **#links**: A list of `Halite::HeaderLink` set from headers.\n- **#parse**: (return value depends on MIME type) parse the body using a parser defined for the `#content_type`.\n- **#to_a**: Return a `Hash` of status code, response headers and body as a string.\n- **#to_raw**: Return a raw of response as a string.\n- **#to_s**: Return response body as a string.\n- **#version**: The HTTP version.\n\n#### Response Content\n\nWe can read the content of the server's response by call `#body`:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/user-agent\")\nr.body\n# => {\"user-agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36\"}\n```\n\nThe `gzip` and `deflate` transfer-encodings are automatically decoded for you.\nAnd requests will automatically decode content from the server. Most unicode charsets are seamlessly decoded.\n\n#### JSON Content\n\nThere’s also a built-in a JSON adapter, in case you’re dealing with JSON data:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/user-agent\")\nr.parse(\"json\")\nr.parse # simplily by default\n# => {\n# =>   \"user-agent\" => \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36\"\n# => }\n```\n\n#### Parsing Content\n\n`Halite::Response` has a MIME type adapter system that you can use a decoder to parse the content,\nwe can inherit `Halite::MimeTypes::Adapter` to make our adapter:\n\n```crystal\n# Define a MIME type adapter\nclass YAMLAdapter < Halite::MimeType::Adapter\n  def decode(string)\n    YAML.parse(string)\n  end\n\n  def encode(obj)\n    obj.to_yaml\n  end\nend\n\n# Register to Halite to invoke\nHalite::MimeType.register YAMLAdapter.new, \"application/x-yaml\", \"yaml\", \"yml\"\n\n# Test it!\nr = Halite.get \"https://raw.githubusercontent.com/icyleaf/halite/master/shard.yml\"\nr.parse(\"yaml\") # or \"yml\"\n# => {\"name\" => \"halite\", \"version\" => \"0.4.0\", \"authors\" => [\"icyleaf <icyleaf.cn@gmail.com>\"], \"crystal\" => \"0.25.0\", \"license\" => \"MIT\"}\n```\n\n#### Binary Data\n\nStore binary data (eg, `application/octet-stream`) to file, you can use [streaming requests](#streaming-requests):\n\n```crystal\nHalite.get(\"https://github.com/icyleaf/halite/archive/master.zip\") do |response|\n  filename = response.filename || \"halite-master.zip\"\n  File.open(filename, \"w\") do |file|\n    IO.copy(response.body_io, file)\n  end\nend\n```\n\n### Error Handling\n\n- For any status code, a `Halite::Response` will be returned.\n- If request timeout, a `Halite::TimeoutError` will be raised.\n- If a request exceeds the configured number of maximum redirections, a `Halite::TooManyRedirectsError` will raised.\n- If request uri is http and configured tls context, a `Halite::RequestError` will raised.\n- If request uri is invalid, a `Halite::ConnectionError`/`Halite::UnsupportedMethodError`/`Halite::UnsupportedSchemeError` will raised.\n\n#### Raise for status code\n\nIf we made a bad request(a 4xx client error or a 5xx server error response), we can raise with `Halite::Response.raise_for_status`.\n\nBut, since our `status_code` was not `4xx` or `5xx`, it returns `nil` when we call it:\n\n```crystal\nurls = [\n  \"https://httpbin.org/status/404\",\n  \"https://httpbin.org/status/500?foo=bar\",\n  \"https://httpbin.org/status/200\",\n]\n\nurls.each do |url|\n  r = Halite.get url\n  begin\n    r.raise_for_status\n    p r.body\n  rescue ex : Halite::ClientError | Halite::ServerError\n    p \"[#{ex.status_code}] #{ex.status_message} (#{ex.class})\"\n  end\nend\n\n# => \"[404] not found error with url: https://httpbin.org/status/404 (Halite::Exception::ClientError)\"\n# => \"[500] internal server error error with url: https://httpbin.org/status/500?foo=bar (Halite::Exception::ServerError)\"\n# => \"\"\n```\n\n## Middleware\n\nHalite now has middleware (a.k.a features) support providing a simple way to plug in intermediate custom logic\nin your HTTP client, allowing you to monitor outgoing requests, incoming responses, and use it as an interceptor.\n\nAvailable features:\n\n- [Logging](#logging) (Yes, logging is based on feature, cool, aha!)\n- [Local Cache](#local-cache) (local storage, speed up in development)\n\n### Write a simple feature\n\nLet's implement simple middleware that prints each request:\n\n```crystal\nclass RequestMonister < Halite::Feature\n  @label : String\n  def initialize(**options)\n    @label = options.fetch(:label, \"\")\n  end\n\n  def request(request) : Halite::Request\n    puts @label\n    puts request.verb\n    puts request.uri\n    puts request.body\n\n    request\n  end\n\n  Halite.register_feature \"request_monster\", self\nend\n```\n\nThen use it in Halite:\n\n```crystal\nHalite.use(\"request_monster\", label: \"testing\")\n      .post(\"http://httpbin.org/post\", form: {name: \"foo\"})\n\n# Or configure to client\nclient = Halite::Client.new do\n  use \"request_monster\", label: \"testing\"\nend\n\nclient.post(\"http://httpbin.org/post\", form: {name: \"foo\"})\n\n# => testing\n# => POST\n# => http://httpbin.org/post\n# => name=foo\n```\n\n### Write a interceptor\n\nHalite's killer feature is the **interceptor**, Use `Halite::Feature::Chain` to process with two result:\n\n- `next`: perform and run next interceptor\n- `return`: perform and return\n\nSo, you can intercept and turn to the following registered features.\n\n```crystal\nclass AlwaysNotFound < Halite::Feature\n  def intercept(chain)\n    response = chain.perform\n    response = Halite::Response.new(chain.request.uri, 404, response.body, response.headers)\n    chain.next(response)\n  end\n\n  Halite.register_feature \"404\", self\nend\n\nclass PoweredBy < Halite::Feature\n  def intercept(chain)\n    if response = chain.response\n      response.headers[\"X-Powered-By\"] = \"Halite\"\n      chain.return(response)\n    else\n      chain\n    end\n  end\n\n  Halite.register_feature \"powered_by\", self\nend\n\nr = Halite.use(\"404\").use(\"powered_by\").get(\"http://httpbin.org/user-agent\")\nr.status_code               # => 404\nr.headers[\"X-Powered-By\"]   # => Halite\nr.body                      # => {\"user-agent\":\"Halite/0.6.0\"}\n```\n\nFor more implementation details about the feature layer, see the [Feature](https://github.com/icyleaf/halite/blob/master/src/halite/feature.cr#L2) class and [examples](https://github.com/icyleaf/halite/tree/master/src/halite/features) and [specs](https://github.com/icyleaf/halite/blob/master/spec/spec_helper.cr#L23).\n\n## Advanced Usage\n\n### Configuring\n\nHalite provides a traditional way to instance client, and you can configure any chainable methods with block:\n\n```crystal\nclient = Halite::Client.new do\n  # Set basic auth\n  basic_auth \"username\", \"password\"\n\n  # Enable logging\n  logging true\n\n  # Set timeout\n  timeout 10.seconds\n\n  # Set user agent\n  headers user_agent: \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36\"\nend\n\n# You also can configure in this way\nclient.accept(\"application/json\")\n\nr = client.get(\"http://httpbin.org/get\")\n```\n\n### Endpoint\n\nNo more given endpoint per request, use `endpoint` will make the request URI shorter, you can set it in flexible way:\n\n```crystal\nclient = Halite::Client.new do\n  endpoint \"https://gitlab.org/api/v4\"\n  user_agent \"Halite\"\nend\n\nclient.get(\"users\")       # GET https://gitlab.org/api/v4/users\n\n# You can override the path by using an absolute path\nclient.get(\"/users\")      # GET https://gitlab.org/users\n```\n\n### Sessions\n\nAs like [requests.Session()](http://docs.python-requests.org/en/master/user/advanced/#session-objects), Halite built-in session by default.\n\nLet's persist some cookies across requests:\n\n```crystal\nclient = Halite::Client.new\nclient.get(\"http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0\")\nclient.get(\"http://httpbin.org/cookies\")\n# => 2018-06-25 18:41:05 +08:00 | request | GET    | http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0\n# => 2018-06-25 18:41:06 +08:00 | response | 302    | http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0 | text/html\n# => <!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n# => <title>Redirecting...</title>\n# => <h1>Redirecting...</h1>\n# => <p>You should be redirected automatically to target URL: <a href=\"/cookies\">/cookies</a>.  If not click the link.\n# => 2018-06-25 18:41:06 +08:00 | request | GET    | http://httpbin.org/cookies\n# => 2018-06-25 18:41:07 +08:00 | response | 200    | http://httpbin.org/cookies | application/json\n# => {\"cookies\":{\"private_token\":\"6abaef100b77808ceb7fe26a3bcff1d0\"}}\n```\n\nAll it support with [chainable methods](https://icyleaf.github.io/halite/Halite/Chainable.html) in the other examples list in [requests.Session](http://docs.python-requests.org/en/master/user/advanced/#session-objects).\n\nNote, however, that chainable methods will not be persisted across requests, even if using a session. This example will only send the cookies or headers with the first request, but not the second:\n\n```crystal\nclient = Halite::Client.new\nr = client.cookies(\"username\": \"foobar\").get(\"http://httpbin.org/cookies\")\nr.body # => {\"cookies\":{\"username\":\"foobar\"}}\n\nr = client.get(\"http://httpbin.org/cookies\")\nr.body # => {\"cookies\":{}}\n```\n\nIf you want to manually add cookies, headers (even features etc) to your session, use the methods start with `with_` in `Halite::Options`\nto manipulate them:\n\n```crystal\nr = client.get(\"http://httpbin.org/cookies\")\nr.body # => {\"cookies\":{}}\n\nclient.options.with_cookie(\"username\": \"foobar\")\nr = client.get(\"http://httpbin.org/cookies\")\nr.body # => {\"cookies\":{\"username\":\"foobar\"}}\n```\n\n### Streaming Requests\n\nSimilar to [HTTP::Client](https://crystal-lang.org/api/0.36.1/HTTP/Client.html#streaming) usage with a block,\nyou can easily use same way, but Halite returns a `Halite::Response` object:\n\n```crystal\nr = Halite.get(\"http://httpbin.org/stream/5\") do |response|\n  response.status_code                  # => 200\n  response.body_io.each_line do |line|\n    puts JSON.parse(line)               # => {\"url\" => \"http://httpbin.org/stream/5\", \"args\" => {}, \"headers\" => {\"Host\" => \"httpbin.org\", \"Connection\" => \"close\", \"User-Agent\" => \"Halite/0.8.0\", \"Accept\" => \"*/*\", \"Accept-Encoding\" => \"gzip, deflate\"}, \"id\" => 0_i64}\n  end\nend\n```\n\n> **Warning**:\n>\n> `body_io` is avaiabled as an `IO` and not reentrant safe. Might throws a \"Nil assertion failed\" exception if there is no data in the `IO`\n(such like `head` requests). Calling this method multiple times causes some of the received data being lost.\n>\n> One more thing, use streaming requests the response will always [enable redirect](#redirects-and-history) automatically.\n\n### Logging\n\nHalite does not enable logging on each request and response too.\nWe can enable per operation logging by configuring them through the chaining API.\n\nBy default, Halite will logging all outgoing HTTP requests and their responses(without binary stream) to `STDOUT` on DEBUG level.\nYou can configuring the following options:\n\n- `logging`: Instance your `Halite::Logging::Abstract`, check [Use the custom logging](#use-the-custom-logging).\n- `format`: Output format, built-in `common` and `json`, you can write your own.\n- `file`: Write to file with path, works with `format`.\n- `filemode`: Write file mode, works with `format`, by default is `a`. (append to bottom, create it if file is not exist)\n- `skip_request_body`: By default is `false`.\n- `skip_response_body`: By default is `false`.\n- `skip_benchmark`: Display elapsed time, by default is `false`.\n- `colorize`: Enable colorize in terminal, only apply in `common` format, by default is `true`.\n\n> **NOTE**: `format` (`file` and `filemode`) and `logging` are conflict, you can not use both.\n\nLet's try with it:\n\n```crystal\n# Logging json request\nHalite.logging\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# => 2018-06-25 18:33:14 +08:00 | request  | GET    | http://httpbin.org/get?name=foobar\n# => 2018-06-25 18:33:15 +08:00 | response | 200    | http://httpbin.org/get?name=foobar | 381.32ms | application/json\n# => {\"args\":{\"name\":\"foobar\"},\"headers\":{\"Accept\":\"*/*\",\"Accept-Encoding\":\"gzip, deflate\",\"Connection\":\"close\",\"Host\":\"httpbin.org\",\"User-Agent\":\"Halite/0.3.2\"},\"origin\":\"60.206.194.34\",\"url\":\"http://httpbin.org/get?name=foobar\"}\n\n# Logging image request\nHalite.logging\n      .get(\"http://httpbin.org/image/png\")\n\n# => 2018-06-25 18:34:15 +08:00 | request  | GET    | http://httpbin.org/image/png\n# => 2018-06-25 18:34:15 +08:00 | response | 200    | http://httpbin.org/image/png | image/png\n\n# Logging with options\nHalite.logging(skip_request_body: true, skip_response_body: true)\n      .post(\"http://httpbin.org/get\", form: {image: File.open(\"halite-logo.png\")})\n\n# => 2018-08-28 14:33:19 +08:00 | request  | POST   | http://httpbin.org/post\n# => 2018-08-28 14:33:21 +08:00 | response | 200    | http://httpbin.org/post | 1.61s | application/json\n```\n\n#### JSON-formatted logging\n\nIt has JSON formatted for developer friendly logging.\n\n```\nHalite.logging(format: \"json\")\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n```\n\n#### Write to a log file\n\n```crystal\n# Write plain text to a log file\nLog.setup(\"halite.file\", backend: Log::IOBackend.new(File.open(\"/tmp/halite.log\", \"a\")))\nHalite.logging(for: \"halite.file\", skip_benchmark: true, colorize: false)\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# Write json data to a log file\nLog.setup(\"halite.file\", backend: Log::IOBackend.new(File.open(\"/tmp/halite.log\", \"a\")))\nHalite.logging(format: \"json\", for: \"halite.file\")\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# Redirect *all* logging from Halite to a file:\nLog.setup(\"halite\", backend: Log::IOBackend.new(File.open(\"/tmp/halite.log\", \"a\")))\n```\n\n#### Use the custom logging\n\nCreating the custom logging by integration `Halite::Logging::Abstract` abstract class.\nHere has two methods must be implement: `#request` and `#response`.\n\n```crystal\nclass CustomLogging < Halite::Logging::Abstract\n  def request(request)\n    @logger.info { \"| >> | %s | %s %s\" % [request.verb, request.uri, request.body] }\n  end\n\n  def response(response)\n    @logger.info { \"| << | %s | %s %s\" % [response.status_code, response.uri, response.content_type] }\n  end\nend\n\n# Add to adapter list (optional)\nHalite::Logging.register \"custom\", CustomLogging.new\n\nHalite.logging(logging: CustomLogging.new)\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# We can also call it use format name if you added it.\nHalite.logging(format: \"custom\")\n      .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n\n# => 2017-12-13 16:40:13 +08:00 | >> | GET | http://httpbin.org/get?name=foobar\n# => 2017-12-13 16:40:15 +08:00 | << | 200 | http://httpbin.org/get?name=foobar application/json\n```\n\n### Local Cache\n\nLocal cache feature is caching responses easily with Halite through an chainable method that is simple and elegant\nyet powerful. Its aim is to focus on the HTTP part of caching and do not worrying about how stuff stored, api rate limiting\neven works without network(offline).\n\nIt has the following options:\n\n- `file`: Load cache from file. it conflict with `path` and `expires`.\n- `path`: The path of cache, default is \"/tmp/halite/cache/\"\n- `expires`: The expires time of cache, default is never expires.\n- `debug`: The debug mode of cache, default is `true`\n\nWith debug mode, cached response it always included some headers information:\n\n- `X-Halite-Cached-From`: Cache source (cache or file)\n- `X-Halite-Cached-Key`: Cache key with verb, uri and body (return with cache, not `file` passed)\n- `X-Halite-Cached-At`:  Cache created time\n- `X-Halite-Cached-Expires-At`: Cache expired time (return with cache, not `file` passed)\n\n```crystal\nHalite.use(\"cache\").get \"http://httpbin.org/anything\"     # request a HTTP\nr = Halite.use(\"cache\").get \"http://httpbin.org/anything\" # request from local storage\nr.headers                                                 # => {..., \"X-Halite-Cached-At\" => \"2018-08-30 10:41:14 UTC\", \"X-Halite-Cached-By\" => \"Halite\", \"X-Halite-Cached-Expires-At\" => \"2018-08-30 10:41:19 UTC\", \"X-Halite-Cached-Key\" => \"2bb155e6c8c47627da3d91834eb4249a\"}}\n```\n\n### Link Headers\n\nMany HTTP APIs feature [Link headers](https://tools.ietf.org/html/rfc5988). GitHub uses\nthese for [pagination](https://developer.github.com/v3/#pagination) in their API, for example:\n\n```crystal\nr = Halite.get \"https://api.github.com/users/icyleaf/repos?page=1&per_page=2\"\nr.links\n# => {\"next\" =>\n# =>   Halite::HeaderLink(\n# =>    @params={},\n# =>    @rel=\"next\",\n# =>    @target=\"https://api.github.com/user/17814/repos?page=2&per_page=2\"),\n# =>  \"last\" =>\n# =>   Halite::HeaderLink(\n# =>    @params={},\n# =>    @rel=\"last\",\n# =>    @target=\"https://api.github.com/user/17814/repos?page=41&per_page=2\")}\n\nr.links[\"next\"]\n# => \"https://api.github.com/user/17814/repos?page=2&per_page=2\"\n\nr.links[\"next\"].params\n# => {}\n```\n\n## Help and Discussion\n\nYou can browse the API documents:\n\nhttps://icyleaf.github.io/halite/\n\nYou can browse the all chainable methods:\n\nhttps://icyleaf.github.io/halite/Halite/Chainable.html\n\nYou can browse the Changelog:\n\nhttps://github.com/icyleaf/halite/blob/master/CHANGELOG.md\n\nIf you have found a bug, please create a issue here:\n\nhttps://github.com/icyleaf/halite/issues/new\n\n## How to Contribute\n\nYour contributions are always welcome! Please submit a pull request or create an issue to add a new question, bug or feature to the list.\n\nAll [Contributors](https://github.com/icyleaf/halite/graphs/contributors) are on the wall.\n\n## You may also like\n\n- [totem](https://github.com/icyleaf/totem) - Load and parse a configuration file or string in JSON, YAML, dotenv formats.\n- [markd](https://github.com/icyleaf/markd) - Yet another markdown parser built for speed, Compliant to CommonMark specification.\n- [poncho](https://github.com/icyleaf/poncho) - A .env parser/loader improved for performance.\n- [popcorn](https://github.com/icyleaf/popcorn) - Easy and Safe casting from one type to another.\n- [fast-crystal](https://github.com/icyleaf/fast-crystal) - 💨 Writing Fast Crystal 😍 -- Collect Common Crystal idioms.\n\n## License\n\n[MIT License](https://github.com/icyleaf/halite/blob/master/LICENSE) © icyleaf\n"
  },
  {
    "path": "shard.yml",
    "content": "name: halite\nversion: 0.12.1\n\nauthors:\n  - icyleaf <icyleaf.cn@gmail.com>\n\ncrystal: \">= 0.36.1, < 2.0.0\"\n\nlicense: MIT\n"
  },
  {
    "path": "spec/fixtures/cache_file.json",
    "content": "{\"name\":\"foo3\"}\n"
  },
  {
    "path": "spec/halite/client_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"../support/mock_server\"\n\ndescribe Halite::Client do\n  # It accepts all chainable methods, see spec/halite_spec.cr\n\n  describe \"#initialize\" do\n    it \"should initial with nothing\" do\n      client = Halite::Client.new\n      client.should be_a(Halite::Client)\n    end\n\n    it \"should initial with options\" do\n      client = Halite::Client.new(headers: {\n        user_agent: \"Spec\",\n      })\n\n      client.should be_a(Halite::Client)\n      client.options.headers[\"User-Agent\"].should eq(\"Spec\")\n    end\n\n    it \"should initial with block\" do\n      client = Halite::Client.new do\n        headers(private_token: \"token\")\n        timeout(read: 2.minutes, connect: 40)\n      end\n\n      client.should be_a(Halite::Client)\n      client.options.headers.should be_a(HTTP::Headers)\n      client.options.headers[\"Private-Token\"].should eq(\"token\")\n      client.options.timeout.connect.should eq(40)\n      client.options.timeout.read.should eq(120)\n    end\n\n    it \"should initial with empty block\" do\n      client = Halite::Client.new { }\n      client.should be_a(Halite::Client)\n    end\n  end\n\n  describe \"#endpoint\" do\n    it \"should set String from arguments\" do\n      client = Halite::Client.new(endpoint: SERVER.endpoint)\n      response = client.get(\"/\")\n      response.status_code.should eq(200)\n    end\n\n    it \"should set String from block\" do\n      client = Halite::Client.new do\n        endpoint SERVER.endpoint\n      end\n\n      response = client.get(\"\")\n      response.status_code.should eq(200)\n    end\n\n    it \"should not overwrite the path of uri if path is empty string\" do\n      client = Halite::Client.new do\n        endpoint \"#{SERVER.endpoint}/redirect-301\"\n      end\n\n      response = client.get(\"\")\n      response.status_code.should eq(301)\n    end\n\n    it \"should always overwrite the path of uri if path starts with '/' char\" do\n      client = Halite::Client.new do\n        endpoint \"#{SERVER.endpoint}/redirect-301\"\n      end\n\n      response = client.accept(\"application/json\").get(\"/\")\n      response.status_code.should eq(200)\n      response.to_s.should match(/json/)\n    end\n\n    it \"should use default uri by each requests\" do\n      client = Halite::Client.new do\n        endpoint SERVER.endpoint\n      end\n\n      response = client.get(\"anything\", params: {\"foo\" => \"bar\"})\n      response.parse[\"url\"].should eq(\"/anything?foo=bar\")\n\n      # try again to make sure endpoint was exists.\n      response = client.get(\"anything\", params: {\"foo\" => \"bar\"})\n      response.parse[\"url\"].should eq(\"/anything?foo=bar\")\n    end\n\n    it \"should resolves uri\" do\n      client = Halite::Client.new do\n        endpoint SERVER.endpoint\n      end\n\n      response = Halite.get(\"#{SERVER.endpoint}/params\", params: {foo: \"bar\"})\n      response.status_code.should eq(200)\n      response.to_s.should eq(\"Params!\")\n    end\n  end\n\n  describe \"#request\" do\n    %w[get post put delete head patch options].each do |verb|\n      it \"should easy to #{verb} request\" do\n        response = Halite::Client.new.request(verb, SERVER.endpoint)\n        response.status_code.should eq(200)\n      end\n\n      it \"should easy to #{verb} request with hash or namedtuple\" do\n        response = Halite::Client.new.request(verb, SERVER.endpoint, params: {name: \"foo\"})\n        response.status_code.should eq(200)\n      end\n\n      it \"should easy to #{verb} request with options\" do\n        response = Halite::Client.new.request(verb, SERVER.endpoint)\n        response.status_code.should eq(200)\n      end\n\n      it \"should easy to #{verb} streaming request\" do\n        data = [] of JSON::Any\n        Halite::Client.new.request(verb, SERVER.api(\"stream?n=2\")) do |response|\n          response.status_code.should eq 200\n          response.headers[\"Transfer-Encoding\"].should eq \"chunked\"\n\n          if verb != \"head\"\n            while content = response.body_io.gets\n              data << JSON.parse(content)\n            end\n          else\n            expect_raises NilAssertionError do\n              response.body_io\n            end\n          end\n        end\n\n        if verb != \"head\"\n          data.size.should eq 2\n          data.first.as_h[\"verb\"].should eq verb.upcase\n        else\n          data.size.should eq 0\n        end\n      end\n    end\n  end\n\n  describe \"#sessions\" do\n    it \"should store and send cookies\" do\n      client = Halite::Client.new\n\n      # get Set-Cookies from server\n      r = client.get SERVER.api(\"cookies\")\n      r.headers[\"Set-Cookie\"].should eq(\"foo=bar\")\n\n      r.cookies.size.should eq(1)\n      r.cookies[\"foo\"].value.should eq(\"bar\")\n\n      # request with stored cookies\n      r = client.get SERVER.api(\"get-cookies\")\n      r.headers.has_key?(\"Set-Cookie\").should be_false\n      r.cookies.size.zero?.should be_true\n      r.parse(\"json\").as_h[\"foo\"].should eq(\"bar\")\n    end\n  end\n\n  describe \"#multiple\" do\n    it \"should use independent and share options\" do\n      client1 = Halite::Client.new do\n        endpoint SERVER.api(\"/user_agent\")\n        user_agent \"foo\"\n      end\n\n      client2 = Halite::Client.new do\n        endpoint SERVER.api(\"/\")\n        user_agent \"bar\"\n      end\n\n      r1 = client1.get(\"\")\n      r1.to_s.should eq(\"foo\")\n\n      r2 = client2.post(\"\")\n      r2.to_s.should eq(\"<!doctype html><body>Mock Server is running.</body></html>\")\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/error_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe Halite::Exception do\n  describe \"#APIError\" do\n    it \"should initial without arguments\" do\n      error = Halite::APIError.new\n      error.message.should be_nil\n      error.status_code.should be_nil\n      error.status_message.not_nil!.should eq \"unknown error\"\n    end\n\n    it \"should initial with message only\" do\n      message = \"foobar\"\n\n      error = Halite::APIError.new(message)\n      error.message.not_nil!.should eq message\n      error.status_code.should be_nil\n      error.status_message.not_nil!.should eq \"foobar error\"\n    end\n\n    it \"should initial with message and status_code\" do\n      message = \"foobar\"\n      status_code = 400\n\n      error = Halite::APIError.new(message, status_code)\n      error.message.not_nil!.should eq message\n      error.status_code.not_nil!.should eq status_code\n      error.status_message.not_nil!.should eq \"bad request error\"\n    end\n\n    it \"should initial with full arguments\" do\n      message = \"foobar\"\n      status_code = 400\n      uri = URI.parse(\"https://www.example.com/get/foobar\")\n\n      error = Halite::APIError.new(message, status_code, uri)\n      error.message.not_nil!.should eq message\n      error.status_code.not_nil!.should eq status_code\n      error.uri.not_nil!.should eq uri\n      error.status_message.not_nil!.should eq \"bad request error with url: #{uri}\"\n    end\n\n    it \"should initial without message\" do\n      status_code = 400\n      uri = URI.parse(\"https://www.example.com/get/foobar\")\n\n      error = Halite::APIError.new(nil, status_code, uri)\n      error.message.not_nil!.should eq \"#{status_code} bad request error with url: #{uri}\"\n      error.status_code.not_nil!.should eq status_code\n      error.uri.not_nil!.should eq uri\n      error.status_message.not_nil!.should eq \"bad request error with url: #{uri}\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/ext/http_headers_encode_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe HTTP::Headers do\n  describe \"#encode\" do\n    it \"should accepts Hash(String, _)\" do\n      HTTP::Headers.encode({\n        \"foo\"    => \"bar\",\n        \"number\" => 1,\n        \"bool\"   => false,\n        \"array\"  => [\"1\", \"2\", \"false\"],\n      }).size.should eq 4\n    end\n\n    it \"should accepts NamedTuple\" do\n      HTTP::Headers.encode({\n        foo:    \"bar\",\n        number: 1,\n        bool:   false,\n        array:  [\"1\", \"2\", \"false\"],\n      }).size.should eq 4\n    end\n\n    it \"should accepts tuples as params\" do\n      HTTP::Headers.encode(foo: \"bar\", name: [\"foo\", \"bar\"]).size.should eq 2\n    end\n\n    it \"should return as HTTP::Params\" do\n      HTTP::Headers.encode({} of String => String).class.should eq HTTP::Headers\n    end\n\n    it \"accepts array to same key\" do\n      h = HTTP::Headers.encode(cookie: [\"a=b\", \"c=d\", \"e=f\"])\n      h[\"Cookie\"].should eq \"a=b,c=d,e=f\"\n    end\n  end\n\n  describe \"#to_flat_h\" do\n    flat_h = HTTP::Headers{\"Accepts\" => [\"application/json\", \"text/html\"], \"Content-Type\" => [\"text/html\"]}.to_flat_h\n    flat_h[\"Accepts\"].should eq([\"application/json\", \"text/html\"])\n    flat_h[\"Content-Type\"].should eq(\"text/html\")\n  end\nend\n"
  },
  {
    "path": "spec/halite/ext/http_params_encode_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe HTTP::Params do\n  describe \"#encode\" do\n    it \"should encode hash to url-encoded query\" do\n      HTTP::Params.encode({\n        \"name\" => \"Lizeth Gusikowski\",\n      }).should eq \"name=Lizeth+Gusikowski\"\n    end\n\n    it \"should encode array in key to url-encoded query\" do\n      HTTP::Params.encode({\n        \"skill\" => [\"ruby\", \"crystal\"],\n      }).should eq \"skill=ruby&skill=crystal\"\n    end\n\n    it \"should encode hash in key to url-encoded query\" do\n      HTTP::Params.encode({\n        \"company\" => {\n          \"name\" => \"Keeling Inc\",\n        },\n      }).should eq \"company%5Bname%5D=Keeling+Inc\"\n    end\n\n    it \"should extract file name to uri-encoded query\" do\n      HTTP::Params.encode({\n        \"avatar\" => File.open(\"halite-logo-small.png\"),\n      }).should eq \"avatar=halite-logo-small.png\"\n    end\n\n    it \"should encode named tupled in key to url-encoded query\" do\n      HTTP::Params.encode({\n        name:    \"Lizeth Gusikowski\",\n        company: {\n          name: \"Keeling Inc\",\n        },\n        skill: [\"ruby\", \"crystal\"],\n      }).should eq \"name=Lizeth+Gusikowski&company=%7Bname%3A+%22Keeling+Inc%22%7D&skill=ruby&skill=crystal\"\n\n      HTTP::Params.encode(\n        name: \"Lizeth Gusikowski\",\n        company: {\n          name: \"Keeling Inc\",\n        },\n        skill: [\"ruby\", \"crystal\"]\n      ).should eq \"name=Lizeth+Gusikowski&company=%7Bname%3A+%22Keeling+Inc%22%7D&skill=ruby&skill=crystal\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/feature_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe Halite::Feature do\n  it \"should a empty feature\" do\n    feature = TestFeatures::Null.new\n    feature.responds_to?(:request).should be_true\n    feature.responds_to?(:response).should be_true\n    feature.responds_to?(:intercept).should be_true\n  end\nend\n"
  },
  {
    "path": "spec/halite/features/cache_spec.cr",
    "content": "require \"../../spec_helper\"\n\nprivate struct CacheStruct\n  getter metadata, body, chain\n\n  def initialize(@body : String, @chain : Halite::Feature::Chain, @metadata : Hash(String, JSON::Any)? = nil)\n  end\nend\n\nprivate def cache_spec(cache, request, response, use_cache = false, wait_time : (Int32 | Time::Span)? = nil)\n  FileUtils.rm_rf(cache.path)\n\n  _chain = Halite::Feature::Chain.new(request, nil, Halite::Options.new) do\n    response\n  end\n\n  if file = cache.file\n    chain = cache.intercept(_chain)\n    body = File.read_lines(file).join(\"\\n\")\n    yield CacheStruct.new(body, chain)\n  else\n    key = Digest::MD5.hexdigest(\"#{request.verb}-#{request.uri}-#{request.body}\")\n    path = File.join(cache.path, key)\n    metadata_file = File.join(path, \"metadata.json\")\n    body_file = File.join(path, \"#{key}.cache\")\n\n    cache.intercept(_chain) if use_cache\n\n    if seconds = wait_time\n      sleep seconds\n    end\n\n    chain = cache.intercept(_chain)\n\n    Dir.exists?(path).should be_true\n    File.file?(metadata_file).should be_true\n    File.file?(body_file).should be_true\n\n    metadata = JSON.parse(File.open(metadata_file)).as_h\n    body = File.read_lines(body_file).join(\"\\n\")\n\n    yield CacheStruct.new(body, chain, metadata)\n\n    FileUtils.rm_rf(cache.path)\n  end\nend\n\ndescribe Halite::Cache do\n  it \"should register a format\" do\n    Halite.has_feature?(\"cache\").should be_true\n    Halite.feature(\"cache\").should eq(Halite::Cache)\n  end\n\n  describe \"getters\" do\n    it \"should default value\" do\n      feature = Halite::Cache.new\n      feature.path.should eq(Halite::Cache::DEFAULT_PATH)\n      feature.expires.should be_nil\n      feature.debug.should be_true\n    end\n\n    it \"should return setter value\" do\n      feature = Halite::Cache.new(path: \"/tmp/cache\", expires: 1.day, debug: false)\n      feature.file.should be_nil\n      feature.path.should eq(\"/tmp/cache\")\n      feature.expires.should eq(1.day)\n      feature.debug.should be_false\n\n      # expires accept Int32/Time::Span but return Time::Span\n      feature = Halite::Cache.new(expires: 60)\n      feature.file.should be_nil\n      feature.path.should eq(Halite::Cache::DEFAULT_PATH)\n      feature.expires.should eq(1.minutes)\n      feature.debug.should be_true\n    end\n  end\n\n  describe \"intercept\" do\n    it \"should cache to local storage\" do\n      body = {name: \"foo\"}.to_json\n      request = Halite::Request.new(\"get\", URI.parse(SERVER.api(\"/anything?q=halite1#result\")), HTTP::Headers{\"Accept\" => \"application/json\"})\n      response = Halite::Response.new(request.uri, 200, body, HTTP::Headers{\"Content-Type\" => \"application/json\", \"Content-Length\" => body.size.to_s})\n      feature = Halite::Cache.new\n      feature.file.should be_nil\n      feature.path.should eq(Halite::Cache::DEFAULT_PATH)\n      feature.expires.should be_nil\n      feature.debug.should be_true\n\n      # First return response on HTTP\n      cache_spec(feature, request, response, use_cache: false) do |result|\n        result.metadata.not_nil![\"status_code\"].should eq(200)\n        result.metadata.not_nil![\"headers\"].as_h[\"Content-Type\"].should eq(\"application/json\")\n        result.metadata.not_nil![\"headers\"].as_h[\"Content-Length\"].should eq(response.body.size.to_s)\n        result.body.should eq(response.body)\n        result.chain.result.should eq(Halite::Feature::Chain::Result::Return)\n        result.chain.response.should eq(response)\n      end\n\n      # Second return response on Cache\n      cache_spec(feature, request, response, use_cache: true) do |result|\n        result.metadata.not_nil![\"status_code\"].should eq(200)\n        result.metadata.not_nil![\"headers\"].as_h[\"Content-Type\"].should eq(\"application/json\")\n        result.metadata.not_nil![\"headers\"].as_h[\"Content-Length\"].should eq(response.body.size.to_s)\n        result.body.should eq(response.body)\n        result.chain.result.should eq(Halite::Feature::Chain::Result::Return)\n\n        result.chain.response.should_not be_nil\n        result.chain.response.not_nil!.headers[\"X-Halite-Cached-From\"].should eq(\"cache\")\n        result.chain.response.not_nil!.headers[\"X-Halite-Cached-Key\"].should_not eq(\"\")\n        result.chain.response.not_nil!.headers[\"X-Halite-Cached-At\"].should_not eq(\"\")\n        result.chain.response.not_nil!.headers[\"X-Halite-Cached-Expires-At\"].should eq(\"None\")\n      end\n    end\n\n    it \"should cache without debug mode\" do\n      body = {name: \"foo1\"}.to_json\n      request = Halite::Request.new(\"get\", URI.parse(SERVER.api(\"/anything?q=halite2#result\")), HTTP::Headers{\"Accept\" => \"application/json\"})\n      response = Halite::Response.new(request.uri, 200, body, HTTP::Headers{\"Content-Type\" => \"application/json\", \"Content-Length\" => body.size.to_s})\n      feature = Halite::Cache.new(debug: false)\n      feature.file.should be_nil\n      feature.path.should eq(Halite::Cache::DEFAULT_PATH)\n      feature.expires.should be_nil\n      feature.debug.should be_false\n\n      cache_spec(feature, request, response, use_cache: true) do |result|\n        result.metadata.not_nil![\"status_code\"].should eq(200)\n        result.metadata.not_nil![\"headers\"].as_h[\"Content-Type\"].should eq(\"application/json\")\n        result.metadata.not_nil![\"headers\"].as_h[\"Content-Length\"].should eq(response.body.size.to_s)\n        result.body.should eq(response.body)\n        result.chain.result.should eq(Halite::Feature::Chain::Result::Return)\n\n        result.chain.response.should_not be_nil\n        result.chain.response.not_nil!.headers.has_key?(\"X-Halite-Cached-From\").should be_false\n        result.chain.response.not_nil!.headers.has_key?(\"X-Halite-Cached-Key\").should be_false\n        result.chain.response.not_nil!.headers.has_key?(\"X-Halite-Cached-At\").should be_false\n      end\n    end\n\n    it \"should return no cache if expired\" do\n      body = {name: \"foo2\"}.to_json\n      request = Halite::Request.new(\"get\", URI.parse(SERVER.api(\"/anything?q=halite3#result\")), HTTP::Headers{\"Accept\" => \"application/json\"})\n      response = Halite::Response.new(request.uri, 200, body, HTTP::Headers{\"Content-Type\" => \"application/json\", \"Content-Length\" => body.size.to_s})\n      feature = Halite::Cache.new(expires: 1.milliseconds)\n      feature.file.should be_nil\n      feature.path.should eq(Halite::Cache::DEFAULT_PATH)\n      feature.expires.should eq(1.milliseconds)\n      feature.debug.should be_true\n\n      cache_spec(feature, request, response, use_cache: true, wait_time: 500.milliseconds) do |result|\n        result.metadata.not_nil![\"status_code\"].should eq(200)\n        result.metadata.not_nil![\"headers\"].as_h[\"Content-Type\"].should eq(\"application/json\")\n        result.metadata.not_nil![\"headers\"].as_h[\"Content-Length\"].should eq(response.body.size.to_s)\n        result.body.should eq(response.body)\n        result.chain.result.should eq(Halite::Feature::Chain::Result::Return)\n\n        result.chain.response.should_not be_nil\n        result.chain.response.not_nil!.headers.has_key?(\"X-Halite-Cached-From\").should be_false\n        result.chain.response.not_nil!.headers.has_key?(\"X-Halite-Cached-Key\").should be_false\n        result.chain.response.not_nil!.headers.has_key?(\"X-Halite-Cached-At\").should be_false\n        result.chain.response.not_nil!.headers.has_key?(\"X-Halite-Cached-Expires-At\").should be_false\n      end\n    end\n\n    it \"should load cache from file\" do\n      file = fixture_path(\"cache_file.json\")\n      body = load_fixture(\"cache_file.json\")\n      request = Halite::Request.new(\"get\", URI.parse(SERVER.api(\"/anything?q=halite4#result\")), HTTP::Headers{\"Accept\" => \"application/json\"})\n      response = Halite::Response.new(request.uri, 200, body, HTTP::Headers{\"Content-Type\" => \"application/json\", \"Content-Length\" => body.size.to_s})\n      feature = Halite::Cache.new(file: file)\n      feature.file.should eq(file)\n      feature.path.should eq(Halite::Cache::DEFAULT_PATH)\n      feature.expires.should be_nil\n      feature.debug.should be_true\n\n      cache_spec(feature, request, response, file) do |result|\n        result.metadata.should be_nil\n        result.body.should eq(response.body)\n        result.chain.result.should eq(Halite::Feature::Chain::Result::Return)\n\n        result.chain.response.should_not be_nil\n        result.chain.response.not_nil!.headers[\"X-Halite-Cached-From\"].should eq(\"file\")\n        result.chain.response.not_nil!.headers.has_key?(\"X-Halite-Cached-Key\").should be_false\n        result.chain.response.not_nil!.headers[\"X-Halite-Cached-At\"].should_not eq(\"\")\n        result.chain.response.not_nil!.headers.has_key?(\"X-Halite-Cached-Expires-At\").should be_false\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/features/logging_spec.cr",
    "content": "require \"../../spec_helper\"\n\nprivate class NulleLogger < Halite::Logging::Abstract\n  def request(request)\n  end\n\n  def response(response)\n  end\nend\n\ndescribe Halite::Logging do\n  describe \"#register\" do\n    it \"should register a format\" do\n      Halite::Logging.register \"null\", NulleLogger\n      Halite::Logging.availables.includes?(\"null\").should be_true\n      Halite::Logging[\"null\"].should eq(NulleLogger)\n    end\n  end\n\n  describe \"#initilize\" do\n    it \"should use common as default logging\" do\n      logging = Halite::Logging.new\n      logging.writer.should be_a(Halite::Logging::Common)\n      logging.writer.skip_request_body.should be_false\n      logging.writer.skip_response_body.should be_false\n      logging.writer.skip_benchmark.should be_false\n      logging.writer.colorize.should be_true\n    end\n\n    it \"should use custom logging\" do\n      logging = Halite::Logging.new(logging: NulleLogger.new)\n      logging.writer.should be_a(NulleLogger)\n      logging.writer.skip_request_body.should be_false\n      logging.writer.skip_response_body.should be_false\n      logging.writer.skip_benchmark.should be_false\n      logging.writer.colorize.should be_true\n    end\n\n    it \"should use File IO\" do\n      with_tempfile(\"halite-features-logging\") do |file|\n        uri = URI.parse(\"https://httpbin.org/get\")\n        Log.setup(\"halite.spec.file\", backend: Log::IOBackend.new(File.open(file, \"w\")))\n        writer = Halite::Logging::Common.new(for: \"halite.spec.file\")\n        logging = Halite::Logging.new(logging: writer)\n        logging.writer.should be_a(Halite::Logging::Common)\n        logging.request(Halite::Request.new(\"get\", uri))\n        logging.response(Halite::Response.new(\n          uri,\n          HTTP::Client::Response.new(status_code: 200, body: \"foobar\", headers: HTTP::Headers.encode({\"Content-Type\" => \"text/plain; charset=utf-8\"}))\n        ))\n\n        # waiting file writes\n        sleep 1\n\n        logs = File.read(file)\n        logs.should contain(\"request\")\n        logs.should contain(\"response\")\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/header_link_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def parse_it(raw : String, uri : URI? = nil)\n  Halite::HeaderLink.parse(raw, uri)\nend\n\ndescribe Halite::HeaderLink do\n  it \"should returns only url\" do\n    links = parse_it(\"http://example.net/bar\")\n    links.should be_a Hash(String, Halite::HeaderLink)\n    links.not_nil![\"http://example.net/bar\"].rel.should eq \"http://example.net/bar\"\n    links.not_nil![\"http://example.net/bar\"].target.should eq \"http://example.net/bar\"\n    links.not_nil![\"http://example.net/bar\"].params.size.should eq 0\n    links.not_nil![\"http://example.net/bar\"].to_s.should eq \"http://example.net/bar\"\n  end\n\n  it \"should returns without 'rel' attribute\" do\n    links = parse_it(\"<http://example.net/foobar>;\")\n    links.should be_a Hash(String, Halite::HeaderLink)\n    links.not_nil![\"http://example.net/foobar\"].rel.should eq \"http://example.net/foobar\"\n    links.not_nil![\"http://example.net/foobar\"].target.should eq \"http://example.net/foobar\"\n\n    links = parse_it(%Q{<http://example.net/foobar>; type=\"text/html\"; })\n    links.should be_a Hash(String, Halite::HeaderLink)\n    links.not_nil![\"http://example.net/foobar\"].rel.should eq \"http://example.net/foobar\"\n    links.not_nil![\"http://example.net/foobar\"].target.should eq \"http://example.net/foobar\"\n    links.not_nil![\"http://example.net/foobar\"].params.size.should eq 1\n    links.not_nil![\"http://example.net/foobar\"].params[\"type\"].should eq \"text/html\"\n  end\n\n  it \"should returns with relative path and none-given uri of response\" do\n    uri = URI.parse(\"http://sub.example.com/foo/bar\")\n    links = parse_it(%Q{</TheBook/chapter2>;rel=\"previous\"}, uri)\n    links.should be_a Hash(String, Halite::HeaderLink)\n    links.not_nil![\"previous\"].rel.should eq \"previous\"\n\n    target = uri.dup\n    target.path = \"/TheBook/chapter2\"\n    links.not_nil![\"previous\"].target.should eq target.to_s\n  end\n\n  it \"should returns and keep the first value with multiple same attributes\" do\n    links = parse_it(%Q{<TheBook/chapter2>; rel=\"foo bar\";title=\"Foo\";rel=\"bar\";title=\"Bar\"})\n    links.not_nil!.has_key?(\"foo bar\").should be_true\n    links.not_nil!.has_key?(\"bar\").should be_false\n    links.not_nil![\"foo bar\"].target.should eq \"TheBook/chapter2\"\n    links.not_nil![\"foo bar\"].params[\"title\"].should eq \"Foo\"\n  end\n\n  it \"should return a list of links\" do\n    hash = parse_it(%Q{<https://api.github.com/user/repos?page=3&per_page=100>; rel=\"next\"; title=\"Next Page\", </>; rel=\"http://example.net/foo\"})\n    hash.should be_a Hash(String, Halite::HeaderLink)\n    if links = hash\n      links.has_key?(\"next\").should be_true\n      links[\"next\"].rel.should eq \"next\"\n      links[\"next\"].target.should eq \"https://api.github.com/user/repos?page=3&per_page=100\"\n      links[\"next\"].params.size.should eq 1\n      links[\"next\"].params[\"title\"].should eq \"Next Page\"\n      links[\"next\"].to_s.should eq \"https://api.github.com/user/repos?page=3&per_page=100\"\n\n      links.has_key?(\"/\").should be_false\n      links[\"http://example.net/foo\"].rel.should eq \"http://example.net/foo\"\n      links[\"http://example.net/foo\"].target.should eq \"http://example.net/foo\"\n      links[\"http://example.net/foo\"].params.size.should eq 0\n      links[\"http://example.net/foo\"].to_s.should eq \"http://example.net/foo\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/mime_type_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"yaml\"\n\nprivate class YAMLAdapter < Halite::MimeType::Adapter\n  def decode(string)\n    YAML.parse string\n  end\n\n  def encode(obj)\n    obj.to_yaml\n  end\nend\n\ndescribe Halite::MimeType do\n  it \"should register an adapter\" do\n    Halite::MimeType[\"yaml\"]?.should be_nil\n    Halite::MimeType[\"yml\"]?.should be_nil\n\n    Halite::MimeType.register YAMLAdapter.new, \"application/x-yaml\", \"yaml\", \"yml\"\n\n    Halite::MimeType[\"yaml\"].should be_a YAMLAdapter\n    Halite::MimeType[\"yml\"].should be_a YAMLAdapter\n  end\nend\n"
  },
  {
    "path": "spec/halite/mime_types/json_spec.cr",
    "content": "require \"../../spec_helper\"\n\nprivate class Foo\nend\n\ndescribe Halite::MimeType::JSON do\n  describe \"#encode\" do\n    it \"should work with to_json class\" do\n      json = Halite::MimeType::JSON.new\n      json.encode({name: \"foo\"}).should eq(%Q{{\"name\":\"foo\"}})\n    end\n  end\n\n  describe \"#decode\" do\n    it \"should work with json string\" do\n      json = Halite::MimeType::JSON.new\n      json.decode(%Q{{\"name\": \"foo\"}}).should be_a(JSON::Any)\n      json.decode(%Q{{\"name\": \"foo\"}}).should eq({\"name\" => \"foo\"})\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/options/follow_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Halite::Follow do\n  describe \"#initialize\" do\n    it \"should work\" do\n      follow = Halite::Follow.new(1, false)\n      follow.hops.should eq(1)\n      follow.strict.should be_false\n    end\n\n    it \"should set one argument\" do\n      follow = Halite::Follow.new(1)\n      follow.hops.should eq(1)\n      follow.strict.should be_true\n\n      follow = Halite::Follow.new(strict: false)\n      follow.hops.should eq(0)\n      follow.strict.should be_false\n    end\n  end\n\n  describe \"setter\" do\n    it \"should work\" do\n      follow = Halite::Follow.new\n      follow.hops = 3\n      follow.hops.should eq(3)\n\n      follow.strict = false\n      follow.strict.should be_false\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/options/timeout_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Halite::Timeout do\n  describe \"#initialize\" do\n    it \"should set with Int32\" do\n      timeout = Halite::Timeout.new(1, 2, 3)\n      timeout.connect.should eq(1.0)\n      timeout.read.should eq(2)\n      timeout.write.should eq(3)\n    end\n\n    it \"should set with Float64\" do\n      timeout = Halite::Timeout.new(1.2, 3.4, 5.6)\n      timeout.connect.should eq(1.2)\n      timeout.read.should eq(3.4)\n      timeout.write.should eq(5.6)\n    end\n\n    it \"should set with Time::Span\" do\n      timeout = Halite::Timeout.new(1.seconds, 1.minutes, 1.hour)\n      timeout.connect.should eq(1.0)\n      timeout.read.should eq(60.0)\n      timeout.write.should eq(3600.0)\n    end\n\n    it \"should set different format\" do\n      timeout = Halite::Timeout.new(1, 1.minutes, 10.0)\n      timeout.connect.should eq(1.0)\n      timeout.read.should eq(60.0)\n      timeout.write.should eq(10.0)\n\n      timeout = Halite::Timeout.new(1.2, 1, Time::Span.new(seconds: 30, nanoseconds: 0))\n      timeout.connect.should eq(1.2)\n      timeout.read.should eq(1.0)\n      timeout.write.should eq(30.0)\n    end\n\n    it \"should set one argument\" do\n      timeout = Halite::Timeout.new(1)\n      timeout.connect.should eq(1.0)\n      timeout.read.should be_nil\n      timeout.write.should be_nil\n\n      timeout = Halite::Timeout.new(connect: 2)\n      timeout.connect.should eq(2.0)\n      timeout.read.should be_nil\n      timeout.write.should be_nil\n\n      timeout = Halite::Timeout.new(read: 3)\n      timeout.connect.should be_nil\n      timeout.read.should eq(3.0)\n\n      timeout = Halite::Timeout.new(write: 3)\n      timeout.connect.should be_nil\n      timeout.read.should be_nil\n      timeout.write.should eq(3.0)\n    end\n  end\n\n  describe \"setter\" do\n    it \"should set with Int32\" do\n      timeout = Halite::Timeout.new\n      timeout.connect = 3\n      timeout.connect.should eq(3.0)\n\n      timeout.read = 12\n      timeout.read.should eq(12.0)\n\n      timeout.write = 1\n      timeout.write.should eq(1.0)\n    end\n\n    it \"should set with Float64\" do\n      timeout = Halite::Timeout.new(1, 2)\n      timeout.connect = 3.0\n      timeout.connect.should eq(3.0)\n\n      timeout.read = 12.0\n      timeout.read.should eq(12.0)\n\n      timeout.write = 1.0\n      timeout.write.should eq(1.0)\n    end\n\n    it \"should set with Time::Span\" do\n      timeout = Halite::Timeout.new(1, 2)\n      timeout.connect = 3.seconds\n      timeout.connect.should eq(3.0)\n\n      timeout.read = 1.minutes\n      timeout.read.should eq(60.0)\n\n      timeout.write = 1.hour\n      timeout.write.should eq(3600.0)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/options_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class SimpleFeature < Halite::Feature\n  def request(request)\n    request\n  end\n\n  def response(response)\n    response\n  end\n\n  Halite.register_feature \"simple\", self\nend\n\nprivate def test_options\n  Halite::Options.new(\n    endpoint: \"https://spec.example.com\",\n    headers: {\n      user_agent: \"spec\",\n    },\n    params: {\"title\" => \"h1\"},\n    form: {\"title\" => \"h2\"},\n    json: {\"title\" => \"h3\"},\n    raw: \"title=h4\",\n    connect_timeout: 1,\n    read_timeout: 3.2,\n    write_timeout: 5,\n    follow: 2,\n    follow_strict: false,\n    tls: OpenSSL::SSL::Context::Client.new,\n    features: {\n      \"logging\" => Halite::Logging.new.as(Halite::Feature),\n    }\n  )\nend\n\ndescribe Halite::Options do\n  describe \"#initialize\" do\n    it \"should initial with nothing\" do\n      options = Halite::Options.new\n      options.should be_a(Halite::Options)\n\n      options.endpoint.should be_nil\n\n      options.headers.empty?.should be_true\n\n      options.cookies.should be_a(HTTP::Cookies)\n      options.cookies.size.should eq(0)\n\n      options.timeout.should be_a(Halite::Timeout)\n      options.timeout.connect.should be_nil\n      options.timeout.read.should be_nil\n      options.timeout.write.should be_nil\n      options.connect_timeout.should be_nil\n      options.read_timeout.should be_nil\n      options.write_timeout.should be_nil\n\n      options.follow.should be_a(Halite::Follow)\n      options.follow.hops.should eq(Halite::Follow::DEFAULT_HOPS)\n      options.follow.strict.should eq(Halite::Follow::STRICT)\n      options.follow_strict.should eq(Halite::Follow::STRICT)\n\n      options.tls.should be_nil\n      options.params.should eq({} of String => Halite::Options::Type)\n      options.form.should eq({} of String => Halite::Options::Type)\n      options.json.should eq({} of String => Halite::Options::Type)\n      options.raw.should be_nil\n    end\n\n    it \"should initial with original\" do\n      options = Halite::Options.new(headers: {\n        \"private_token\" => \"token\",\n      },\n        timeout: Halite::Timeout.new(connect: 3.2),\n        endpoint: \"https://example.com\"\n      )\n\n      options.should be_a(Halite::Options)\n      options.endpoint.should eq(URI.parse(\"https://example.com\"))\n      options.headers.should be_a(HTTP::Headers)\n      options.headers[\"Private-Token\"].should eq(\"token\")\n      options.timeout.connect.should eq(3.2)\n      options.timeout.read.should be_nil\n      options.timeout.write.should be_nil\n    end\n\n    it \"should initial with quick setup\" do\n      endpoint = URI.parse(\"https://example.com\")\n      options = Halite::Options.new(headers: {\n        private_token: \"token\",\n      },\n        connect_timeout: 1.minutes,\n        endpoint: endpoint\n      )\n\n      options.should be_a(Halite::Options)\n      options.endpoint.should eq(endpoint)\n      options.headers.should be_a(HTTP::Headers)\n      options.headers[\"Private-Token\"].should eq(\"token\")\n      options.timeout.connect.should eq(60.0)\n      options.timeout.read.should be_nil\n      options.timeout.write.should be_nil\n    end\n\n    it \"should overwrite default headers\" do\n      options = Halite::Options.new(\n        headers: {\n          user_agent: \"spec\",\n        },\n      )\n\n      options.should be_a(Halite::Options)\n      options.headers[\"User-Agent\"].should eq(\"spec\")\n    end\n  end\n\n  describe \"#merge!\" do\n    it \"should works with Halite::Options\" do\n      old_options = test_options\n      endpoint = old_options.endpoint\n      new_tls = OpenSSL::SSL::Context::Client.new\n      options = old_options.merge!(Halite::Options.new(\n        headers: {\n          user_agent: \"new_spec\",\n        },\n        params: {\"title\" => \"1\"},\n        form: {\"title\" => \"2\"},\n        json: {\"title\" => \"3\"},\n        raw: \"title=4\",\n        connect_timeout: 2,\n        follow: 1,\n        tls: new_tls,\n        features: {\n          \"cache\" => Halite::Cache.new.as(Halite::Feature),\n        }\n      ))\n\n      # TODO: write_timeout\n      old_options.endpoint.should eq(endpoint)\n      old_options.headers.should eq(HTTP::Headers{\"User-Agent\" => \"new_spec\"})\n      old_options.cookies.size.should eq(0)\n      old_options.timeout.connect.should eq(2)\n      old_options.timeout.read.should be_nil\n      old_options.timeout.write.should be_nil\n      old_options.connect_timeout.should eq(2)\n      old_options.read_timeout.should be_nil\n      old_options.write_timeout.should be_nil\n      old_options.follow.hops.should eq(1)\n      old_options.follow.strict.should be_true\n      old_options.params.should eq({\"title\" => \"1\"})\n      old_options.form.should eq({\"title\" => \"2\"})\n      old_options.json.should eq({\"title\" => \"3\"})\n      old_options.raw.should_not be_nil\n      old_options.raw.not_nil!.should eq(\"title=4\")\n      old_options.tls.not_nil!.should eq(new_tls)\n      options.features[\"logging\"].should be_a(Halite::Logging)\n      options.features[\"cache\"].should be_a(Halite::Cache)\n\n      options.endpoint.should eq(endpoint)\n      options.headers.should eq(HTTP::Headers{\"User-Agent\" => \"new_spec\"})\n      options.cookies.size.should eq(0)\n      options.timeout.connect.should eq(2)\n      options.timeout.read.should be_nil\n      options.timeout.write.should be_nil\n      options.connect_timeout.should eq(2)\n      options.read_timeout.should be_nil\n      options.write_timeout.should be_nil\n      options.follow.hops.should eq(1)\n      options.follow.strict.should be_true\n      options.params.should eq({\"title\" => \"1\"})\n      options.form.should eq({\"title\" => \"2\"})\n      options.json.should eq({\"title\" => \"3\"})\n      options.raw.should_not be_nil\n      options.raw.not_nil!.should eq(\"title=4\")\n      options.tls.not_nil!.should eq(new_tls)\n      options.features[\"logging\"].should be_a(Halite::Logging)\n      options.features[\"cache\"].should be_a(Halite::Cache)\n    end\n  end\n\n  describe \"#merge\" do\n    it \"should works with Halite::Options\" do\n      old_options = test_options\n      endpoint = old_options.endpoint\n      new_tls = OpenSSL::SSL::Context::Client.new\n      options = old_options.merge(Halite::Options.new(\n        endpoint: \"https://new.exaple.com\",\n        headers: {\n          user_agent: \"new_spec\",\n        },\n        params: {\"title\" => \"1\"},\n        form: {\"title\" => \"2\"},\n        json: {\"title\" => \"3\"},\n        raw: \"title=4\",\n        connect_timeout: 2,\n        follow: 1,\n        tls: new_tls,\n        features: {\n          \"cache\" => Halite::Cache.new.as(Halite::Feature),\n        }\n      ))\n\n      old_options.endpoint.should eq(endpoint)\n      old_options.headers.should eq(HTTP::Headers{\"User-Agent\" => \"spec\"})\n      old_options.cookies.size.should eq(0)\n      old_options.timeout.connect.should eq(1)\n      old_options.timeout.read.should eq(3.2)\n      old_options.timeout.write.should eq(5.0)\n      old_options.follow.hops.should eq(2)\n      old_options.follow.strict.should be_false\n      old_options.params.should eq({\"title\" => \"h1\"})\n      old_options.form.should eq({\"title\" => \"h2\"})\n      old_options.json.should eq({\"title\" => \"h3\"})\n      old_options.raw.should_not be_nil\n      old_options.raw.not_nil!.should eq(\"title=h4\")\n      old_options.features.size.should eq(1)\n      old_options.features[\"logging\"].should be_a(Halite::Logging)\n      old_options.tls.not_nil!.should_not eq(new_tls)\n\n      options.endpoint.should eq(URI.parse(\"https://new.exaple.com\"))\n      options.headers.should eq(HTTP::Headers{\"User-Agent\" => \"new_spec\"})\n      options.cookies.size.should eq(0)\n      options.timeout.connect.should eq(2)\n      options.timeout.read.should be_nil\n      options.timeout.write.should be_nil\n      options.connect_timeout.should eq(2)\n      options.read_timeout.should be_nil\n      options.write_timeout.should be_nil\n      options.follow.hops.should eq(1)\n      options.follow.strict.should be_true\n      options.params.should eq({\"title\" => \"1\"})\n      options.form.should eq({\"title\" => \"2\"})\n      options.json.should eq({\"title\" => \"3\"})\n      options.raw.should_not be_nil\n      options.raw.not_nil!.should eq(\"title=4\")\n      options.tls.not_nil!.should eq(new_tls)\n      options.features.size.should eq(2)\n      options.features[\"logging\"].should be_a(Halite::Logging)\n      options.features[\"cache\"].should be_a(Halite::Cache)\n    end\n\n    it \"should overwrite exists value of headers from other\" do\n      options = Halite::Options.new(headers: {private_token: \"foo\"})\n      new_options = options.merge(Halite::Options.new(headers: {private_token: \"bar\"}))\n      new_options.headers.should eq(Halite::Options.new(headers: {private_token: \"bar\"}).headers)\n    end\n\n    it \"should merge new headers from other\" do\n      options = Halite::Options.new(headers: {private_token: \"foo\"})\n      new_options = options.merge(Halite::Options.new(headers: {content_type: \"text/html\"}))\n      new_options.headers.should eq(Halite::Options.new(headers: {private_token: \"foo\", content_type: \"text/html\"}).headers)\n    end\n  end\n\n  describe \"#clear!\" do\n    options = test_options\n    options.clear!\n    options.endpoint.should be_nil\n    options.headers.size.should eq(0)\n\n    options.cookies.should be_a(HTTP::Cookies)\n    options.cookies.size.should eq(0)\n\n    options.timeout.should be_a(Halite::Timeout)\n    options.timeout.connect.should be_nil\n    options.timeout.read.should be_nil\n    options.timeout.write.should be_nil\n    options.connect_timeout.should be_nil\n    options.read_timeout.should be_nil\n    options.write_timeout.should be_nil\n\n    options.follow.should be_a(Halite::Follow)\n    options.follow.hops.should eq(Halite::Follow::DEFAULT_HOPS)\n    options.follow.strict.should eq(Halite::Follow::STRICT)\n    options.follow_strict.should eq(Halite::Follow::STRICT)\n\n    options.tls.should be_nil\n    options.params.should eq({} of String => Halite::Options::Type)\n    options.form.should eq({} of String => Halite::Options::Type)\n    options.json.should eq({} of String => Halite::Options::Type)\n    options.raw.should be_nil\n\n    options.features.should eq({} of String => Halite::Feature)\n  end\n\n  describe \"#dup\" do\n    options = test_options\n    new_options = options.dup\n\n    new_options.endpoint = \"https://example.com\"\n    new_options.endpoint.should eq(URI.parse(\"https://example.com\"))\n    options.endpoint.should eq(URI.parse(\"https://spec.example.com\"))\n\n    new_options.headers = HTTP::Headers.new\n    new_options.headers.empty?.should be_true\n    options.headers.size.should eq(1)\n\n    cookies = HTTP::Cookies.new\n    cookies << HTTP::Cookie.new(\"name\", \"foobar\")\n    new_options.cookies = cookies\n    new_options.cookies.size.should eq(1)\n    options.cookies.size.should eq 0\n\n    timeout = Halite::Timeout.new(10, 20)\n    new_options.timeout = timeout\n    new_options.timeout.should eq(timeout)\n    options.timeout.connect.should eq(test_options.timeout.connect)\n    options.timeout.read.should eq(test_options.timeout.read)\n    options.timeout.write.should eq(test_options.timeout.write)\n\n    follow = Halite::Follow.new(6, true)\n    new_options.follow = follow\n    new_options.follow.should eq(follow)\n    options.follow.hops.should eq(test_options.follow.hops)\n    options.follow.strict.should eq(test_options.follow.strict)\n\n    new_options.tls = nil\n    new_options.tls.should be_nil\n    options.tls.should_not be_nil\n\n    data = {\n      \"name\" => \"foo\".as(Halite::Options::Type),\n    }\n\n    new_options.params = data\n    new_options.params.should eq(data)\n    options.params.should eq(test_options.params)\n\n    new_options.form = data\n    new_options.form.should eq(data)\n    options.form.should eq(test_options.form)\n\n    new_options.json = data\n    new_options.json.should eq(data)\n    options.json.should eq(test_options.json)\n\n    new_options.raw = \"foobar\"\n    new_options.raw.not_nil!.should eq(\"foobar\")\n    options.raw.should eq(test_options.raw)\n\n    features = {\"cache\" => Halite::Cache.new.as(Halite::Feature)}\n    new_options.features = features\n    new_options.features.size.should eq(1)\n    new_options.features[\"cache\"].should be_a(Halite::Cache)\n    options.features.size.should eq(1)\n    options.features[\"logging\"].should be_a(Halite::Logging)\n\n    new_options.logging = false\n    new_options.logging.should be_false\n    options.logging.should be_true\n  end\n\n  describe \"#with_endpoint\" do\n    it \"should overwrite String value\" do\n      options = Halite::Options.new\n      options.with_endpoint(\"https://with.example.com\")\n\n      options.endpoint.should eq(URI.parse(\"https://with.example.com\"))\n    end\n\n    it \"should overwrite URI value\" do\n      endpoint = URI.parse(\"https://with.example.com\")\n      options = Halite::Options.new(endpoint: \"https://new.example.com\")\n      options.with_endpoint(endpoint)\n\n      options.endpoint.should eq(endpoint)\n    end\n  end\n\n  describe \"#with_headers\" do\n    it \"should overwrite tupled headers\" do\n      options = Halite::Options.new(headers: {\n        private_token: \"token\",\n      })\n      options = options.with_headers(private_token: \"new\", accept: \"application/json\")\n\n      options.headers[\"Private-Token\"].should eq(\"new\")\n      options.headers[\"Accept\"].should eq(\"application/json\")\n    end\n\n    it \"should overwrite NamedTuped headers\" do\n      options = Halite::Options.new(headers: {\n        private_token: \"token\",\n      })\n      options = options.with_headers(private_token: \"new\", accept: \"application/json\")\n\n      options.headers[\"Private-Token\"].should eq(\"new\")\n      options.headers[\"Accept\"].should eq(\"application/json\")\n    end\n\n    it \"should overwrite Hash headers\" do\n      options = Halite::Options.new(headers: {\n        private_token: \"token\",\n      })\n      options = options.with_headers(private_token: \"new\", accept: \"application/json\")\n\n      options.headers[\"Private-Token\"].should eq(\"new\")\n      options.headers[\"Accept\"].should eq(\"application/json\")\n    end\n  end\n\n  describe \"#with_cookies\" do\n    it \"should overwrite tupled cookies\" do\n      options = Halite::Options.new(cookies: {\n        \"name\" => \"foo\",\n      })\n      options = options.with_cookies(name: \"bar\")\n\n      options.cookies[\"name\"].value.should eq(\"bar\")\n    end\n\n    it \"should overwrite NamedTuple cookies\" do\n      options = Halite::Options.new(cookies: {\n        \"name\" => \"foo\",\n      })\n      options = options.with_cookies({name: \"bar\"})\n\n      options.cookies[\"name\"].value.should eq(\"bar\")\n    end\n\n    it \"should overwrite Hash cookies\" do\n      options = Halite::Options.new(cookies: {\n        \"name\" => \"foo\",\n      })\n      options = options.with_cookies({\"name\" => \"bar\"})\n\n      options.cookies[\"name\"].value.should eq(\"bar\")\n    end\n  end\n\n  describe \"#with_timeout\" do\n    it \"should overwrite timeout\" do\n      options = Halite::Options.new(timeout: Halite::Timeout.new(connect: 1, read: 3, write: 4))\n      options = options.with_timeout(read: 4.minutes, connect: 1.2, write: 10)\n\n      options.timeout.connect.should eq(1.2)\n      options.timeout.read.should eq(4.minutes.to_f)\n      options.timeout.write.should eq(10.0)\n    end\n  end\n\n  describe \"#with_follow\" do\n    it \"should overwrite follow\" do\n      options = Halite::Options.new(follow: Halite::Follow.new(1, true))\n      options = options.with_follow(follow: 5, strict: false)\n\n      options.follow.hops.should eq(5)\n      options.follow.strict.should be_false\n    end\n  end\n\n  describe \"#with_logging\" do\n    it \"should overwrite logging with instance class\" do\n      options = Halite::Options.new.with_logging(logging: SimpleLogger.new)\n      logging = options.features[\"logging\"].as(Halite::Logging)\n      logging.writer.should be_a(SimpleLogger)\n    end\n\n    it \"should overwrite logging with format name\" do\n      Halite::Logging.register \"simple\", SimpleLogger\n\n      options = Halite::Options.new.with_logging(format: \"simple\")\n      logging = options.features[\"logging\"].as(Halite::Logging)\n      logging.writer.should be_a(SimpleLogger)\n    end\n\n    it \"should became a file logging\" do\n      Halite::Logging.register \"simple\", SimpleLogger\n\n      with_tempfile(\"halite_logger\") do |file|\n        Log.setup(\"halite.tempfile\", backend: Log::IOBackend.new(File.open(\"/tmp/halite.log\", \"w\")))\n        options = Halite::Options.new.with_logging(format: \"simple\")\n        logging = options.features[\"logging\"].as(Halite::Logging)\n        logging.writer.should be_a(SimpleLogger)\n      end\n    end\n\n    it \"throws an exception with unregister logging format\" do\n      expect_raises Halite::UnRegisterLoggerFormatError do\n        Halite::Options.new.with_logging(format: \"fake\")\n      end\n    end\n  end\n\n  describe \"#with_features\" do\n    it \"should use a feature\" do\n      options = Halite::Options.new.with_features(\"logging\")\n      logging = options.features[\"logging\"].as(Halite::Logging)\n      logging.writer.should be_a(Halite::Logging::Common)\n    end\n\n    it \"should use a feature with options\" do\n      options = Halite::Options.new.with_features(\"logging\", logging: SimpleLogger.new)\n      logging = options.features[\"logging\"].as(Halite::Logging)\n      logging.writer.should be_a(SimpleLogger)\n    end\n\n    it \"should use multiple features\" do\n      Halite.register_feature \"simple\", SimpleFeature\n\n      options = Halite::Options.new.with_features(\"logging\", \"simple\")\n      logging = options.features[\"logging\"].as(Halite::Logging)\n      logging.writer.should be_a(Halite::Logging::Common)\n\n      simple = options.features[\"simple\"].as(SimpleFeature)\n      simple.should be_a(SimpleFeature)\n    end\n\n    it \"throws an exception with unregister feature\" do\n      expect_raises Halite::UnRegisterFeatureError do\n        Halite::Options.new.with_features(\"fake\")\n      end\n    end\n  end\n\n  describe \"#clear!\" do\n    it \"should clear setted options\" do\n      options = Halite::Options.new(\n        headers: {\n          \"private_token\" => \"token\",\n        },\n        cookies: {\n          \"name\" => \"foo\",\n        },\n        params: {\"name\" => \"foo\"},\n        form: {\"name\" => \"foo\"},\n        json: {\"name\" => \"foo\"},\n        timeout: Halite::Timeout.new(1, 3),\n        follow: Halite::Follow.new(4, false),\n      )\n      options.clear!\n\n      options.headers.empty?.should be_true\n      options.cookies.empty?.should be_true\n      options.params.empty?.should be_true\n      options.form.empty?.should be_true\n      options.json.empty?.should be_true\n\n      options.timeout.connect.nil?.should be_true\n      options.timeout.read.nil?.should be_true\n      options.timeout.write.nil?.should be_true\n\n      options.follow.hops.should eq(Halite::Follow::DEFAULT_HOPS)\n      options.follow.strict.should eq(Halite::Follow::STRICT)\n    end\n  end\n\n  describe \"alias methods\" do\n    context \"endpoint\" do\n      it \"getter\" do\n        options = Halite::Options.new(endpoint: \"https://with.example.com\")\n        options.endpoint.should eq(URI.parse(\"https://with.example.com\"))\n      end\n\n      it \"setter\" do\n        endpoint_string = \"https://with.example.com\"\n        endpoint = URI.parse(endpoint_string)\n        options = Halite::Options.new(endpoint: endpoint_string)\n        options.endpoint.should eq(endpoint)\n\n        options = Halite::Options.new(endpoint: endpoint)\n        options.endpoint.should eq(endpoint)\n\n        options = Halite::Options.new\n        options.endpoint = endpoint_string\n        options.endpoint.should eq(endpoint)\n\n        options = Halite::Options.new\n        options.endpoint = endpoint\n        options.endpoint.should eq(endpoint)\n      end\n    end\n\n    context \"connect_timeout alias to timeout.connect\" do\n      it \"getter\" do\n        options = Halite::Options.new(timeout: Halite::Timeout.new(connect: 34))\n        options.timeout.connect.should eq(34)\n        options.connect_timeout.should eq(34)\n      end\n\n      it \"setter\" do\n        options = Halite::Options.new\n\n        options.timeout.connect = 12\n        options.connect_timeout.should eq(12)\n        options.timeout.connect.should eq(12)\n\n        options.connect_timeout = 21\n        options.connect_timeout.should eq(21)\n        options.timeout.connect.should eq(21)\n      end\n    end\n\n    context \"read_timeout alias to timeout.read\" do\n      it \"getter\" do\n        options = Halite::Options.new(timeout: Halite::Timeout.new(read: 34))\n        options.read_timeout.should eq(34)\n        options.timeout.read.should eq(34)\n      end\n\n      it \"setter\" do\n        options = Halite::Options.new\n\n        options.timeout.read = 12\n        options.read_timeout.should eq(12)\n        options.timeout.read.should eq(12)\n\n        options.read_timeout = 21\n        options.read_timeout.should eq(21)\n        options.timeout.read.should eq(21)\n      end\n    end\n\n    context \"write_timeout alias to timeout.write\" do\n      it \"getter\" do\n        options = Halite::Options.new(timeout: Halite::Timeout.new(write: 56))\n        options.timeout.write.should eq(56)\n        options.write_timeout.should eq(56)\n      end\n\n      it \"setter\" do\n        options = Halite::Options.new\n\n        options.timeout.write = 12\n        options.write_timeout.should eq(12)\n        options.timeout.write.should eq(12)\n\n        options.write_timeout = 21\n        options.write_timeout.should eq(21)\n        options.timeout.write.should eq(21)\n      end\n    end\n\n    context \"only setter for follow alias to follow.hops\" do\n      it \"setter\" do\n        options = Halite::Options.new\n\n        options.follow = 2\n        options.follow.hops.should eq(2)\n      end\n\n      it \"getter\" do\n        options = Halite::Options.new(follow: Halite::Follow.new(3))\n\n        # Can not return integer with follow\n        options.follow.hops.should eq(3)\n      end\n    end\n\n    context \"follow_strict alias to follow.strict\" do\n      it \"setter\" do\n        options = Halite::Options.new\n\n        options.follow_strict = false\n        options.follow.strict.should be_false\n\n        options.follow.strict = true\n        options.follow.strict.should be_true\n      end\n\n      it \"getter\" do\n        options = Halite::Options.new(follow: Halite::Follow.new(strict: false))\n\n        options.follow_strict.should be_false\n        options.follow.strict.should be_false\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/rate_limit_spec.cr",
    "content": "require \"../spec_helper\"\n\n# private def parse_it(raw : String, uri : URI? = nil)\n#   Halite::HeaderLinkParser.parse(raw, uri)\n# end\n\ndescribe Halite::RateLimit do\n  describe \"#parse\" do\n    it \"should works with full arguments\" do\n      headers = HTTP::Headers{\n        \"X-RateLimit-Limit\"     => \"5000\",\n        \"X-RateLimit-Remaining\" => \"4991\",\n        \"X-RateLimit-Reset\"     => \"1613727325\",\n      }\n      subject = Halite::RateLimit.parse(headers)\n      subject.should be_a Halite::RateLimit\n      subject.not_nil!.limit.should eq 5000\n      subject.not_nil!.remaining.should eq 4991\n      subject.not_nil!.reset.should eq 1613727325\n    end\n\n    it \"should works with optional arguments\" do\n      headers = HTTP::Headers{\n        \"X-RateLimit-Limit\" => \"5000\",\n      }\n      subject = Halite::RateLimit.parse(headers)\n      subject.should be_a Halite::RateLimit\n      subject.not_nil!.limit.should eq 5000\n      subject.not_nil!.remaining.should be_nil\n      subject.not_nil!.reset.should be_nil\n    end\n\n    it \"should not works without any headers\" do\n      headers = HTTP::Headers.new\n      subject = Halite::RateLimit.parse(headers)\n      subject.should be_nil\n    end\n  end\n\n  describe \"#new\" do\n    it \"should works with full arguments\" do\n      subject = Halite::RateLimit.new(5000, 4991, 1613727325)\n      subject.limit.should eq 5000\n      subject.remaining.should eq 4991\n      subject.reset.should eq 1613727325\n    end\n\n    it \"should works with optional arguments\" do\n      subject = Halite::RateLimit.new(nil, nil, nil)\n      subject.limit.should be_nil\n      subject.remaining.should be_nil\n      subject.reset.should be_nil\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/redirector_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def request\n  Halite::Request.new(\"head\", URI.parse(\"http://example.com/foo?bar=baz\"))\nend\n\ndef response(uri : URI, status_code = 200, headers = {} of String => String, body = \"\")\n  Halite::Response.new(\n    uri,\n    HTTP::Client::Response.new(status_code: status_code, body: body, headers: HTTP::Headers.encode(headers))\n  )\nend\n\ndef redirector(request, response, strict = true, max_hops = 5)\n  Halite::Redirector.new(request, response, max_hops, strict)\nend\n\ndef simple_response(status_code = Int32, body = \"\", headers = {} of String => String | Array(String))\n  Halite::Response.new(\n    URI.new(\"http://example.com\"),\n    HTTP::Client::Response.new(status_code: status_code, body: body, headers: HTTP::Headers.encode(headers))\n  )\nend\n\ndef redirect_response(status_code, location)\n  simple_response status_code, \"\", {\"Location\" => location}\nend\n\ndescribe Halite::Redirector do\n  describe \"#strict\" do\n    it \"should be true by default\" do\n      redirector(request, response(request.uri)).strict.should eq true\n    end\n  end\n\n  describe \"#max_hops\" do\n    it \"should be 5 by default\" do\n      redirector(request, response(request.uri)).max_hops.should eq 5\n    end\n  end\n\n  describe \"#perform\" do\n    it \"fails with TooManyRedirectsError if max hops reached\" do\n      res = ->(req : Halite::Request) { redirect_response(301, \"#{req.uri}/1\") }\n      expect_raises Halite::TooManyRedirectsError do\n        redirector(request, res.call(request)).perform do |prev_req|\n          redirect_response(301, \"#{prev_req.uri}/1\")\n        end\n      end\n    end\n\n    it \"fails with EndlessRedirectError if endless loop detected\" do\n      res = redirect_response 301, request.uri\n      expect_raises Halite::EndlessRedirectError do\n        redirector(request, res).perform do |_|\n          res\n        end\n      end\n    end\n\n    it \"fails with StateError if there were no Location header\" do\n      res = simple_response 301\n      expect_raises Halite::StateError do\n        redirector(request, res).perform do |_|\n          res\n        end\n      end\n    end\n\n    it \"returns first non-redirect response\" do\n      hops = [\n        redirect_response(301, \"http://example.com/1\"),\n        redirect_response(301, \"http://example.com/2\"),\n        redirect_response(301, \"http://example.com/3\"),\n        simple_response(200, \"foo\"),\n        redirect_response(301, \"http://example.com/4\"),\n        simple_response(200, \"bar\"),\n      ]\n\n      res = redirector(request, hops.shift).perform { hops.shift }\n      res.to_s.should eq \"foo\"\n    end\n\n    context \"following 300/301/302 redirect\" do\n      context \"with strict mode\" do\n        it \"it follows with original verb if it's safe\" do\n          req = Halite::Request.new \"get\", URI.parse(\"http://example.com/foo?bar=baz\")\n          res = redirect_response 300, \"http://example.com/1\"\n\n          redirector(req, res, true).perform do |prev_req|\n            prev_req.verb.should eq \"GET\"\n            simple_response 200\n          end\n        end\n\n        it \"raises StateError if original request was PUT\" do\n          req = Halite::Request.new \"put\", URI.parse(\"http://example.com/foo?bar=baz\")\n          res = redirect_response 300, \"http://example.com/1\"\n          expect_raises Halite::StateError do\n            redirector(req, res, true).perform { |_| simple_response 200 }\n          end\n        end\n\n        it \"raises StateError if original request was POST\" do\n          req = Halite::Request.new \"post\", URI.parse(\"http://example.com/foo?bar=baz\")\n          res = redirect_response 301, \"http://example.com/1\"\n          expect_raises Halite::StateError do\n            redirector(req, res, true).perform { |_| simple_response 200 }\n          end\n        end\n\n        it \"raises StateError if original request was DELETE\" do\n          req = Halite::Request.new \"delete\", URI.parse(\"http://example.com/foo?bar=baz\")\n          res = redirect_response 302, \"http://example.com/1\"\n          expect_raises Halite::StateError do\n            redirector(req, res, true).perform { |_| simple_response 200 }\n          end\n        end\n      end\n\n      context \"without strict mode\" do\n        it \"it follows with original verb if it's safe\" do\n          req = Halite::Request.new \"get\", URI.parse(\"http://example.com/foo?bar=baz\")\n          res = redirect_response 300, \"http://example.com/1\"\n\n          redirector(req, res, false).perform do |prev_req|\n            prev_req.verb.should eq \"GET\"\n            simple_response 200\n          end\n        end\n\n        it \"raises StateError if original request was PUT\" do\n          req = Halite::Request.new \"put\", URI.parse(\"http://example.com/foo?bar=baz\")\n          res = redirect_response 300, \"http://example.com/1\"\n          redirector(req, res, false).perform do |prev_req|\n            prev_req.verb.should eq \"GET\"\n            simple_response 200\n          end\n        end\n\n        it \"raises StateError if original request was POST\" do\n          req = Halite::Request.new \"post\", URI.parse(\"http://example.com/foo?bar=baz\")\n          res = redirect_response 301, \"http://example.com/1\"\n          redirector(req, res, false).perform do |prev_req|\n            prev_req.verb.should eq \"GET\"\n            simple_response 200\n          end\n        end\n\n        it \"raises StateError if original request was DELETE\" do\n          req = Halite::Request.new \"delete\", URI.parse(\"http://example.com/foo?bar=baz\")\n          res = redirect_response 302, \"http://example.com/1\"\n          redirector(req, res, false).perform do |prev_req|\n            prev_req.verb.should eq \"GET\"\n            simple_response 200\n          end\n        end\n      end\n    end\n\n    context \"following 303 redirect\" do\n      it \"follows with HEAD if original request was HEAD\" do\n        req = Halite::Request.new \"head\", URI.parse(\"http://example.com/foo?bar=baz\")\n        res = redirect_response 303, \"http://example.com/1\"\n\n        redirector(req, res).perform do |prev_req|\n          prev_req.verb.should eq \"HEAD\"\n          simple_response 200\n        end\n      end\n\n      it \"follows with GET if original request was GET\" do\n        req = Halite::Request.new \"get\", URI.parse(\"http://example.com/foo?bar=baz\")\n        res = redirect_response 303, \"http://example.com/1\"\n\n        redirector(req, res).perform do |prev_req|\n          prev_req.verb.should eq \"GET\"\n          simple_response 200\n        end\n      end\n\n      it \"follows with GET if original request was neither GET nor HEAD\" do\n        req = Halite::Request.new \"post\", URI.parse(\"http://example.com/foo?bar=baz\")\n        res = redirect_response 303, \"http://example.com/1\"\n\n        redirector(req, res).perform do |prev_req|\n          prev_req.verb.should eq \"GET\"\n          simple_response 200\n        end\n      end\n    end\n\n    context \"following 307 redirect\" do\n      it \"follows with original request's verb\" do\n        req = Halite::Request.new \"post\", URI.parse(\"http://example.com/foo?bar=baz\")\n        res = redirect_response 307, \"http://example.com/1\"\n\n        redirector(req, res).perform do |prev_req|\n          prev_req.verb.should eq \"POST\"\n          simple_response 200\n        end\n      end\n    end\n\n    context \"following 308 redirect\" do\n      it \"follows with original request's verb\" do\n        req = Halite::Request.new \"post\", URI.parse(\"http://example.com/foo?bar=baz\")\n        res = redirect_response 308, \"http://example.com/1\"\n\n        redirector(req, res).perform do |prev_req|\n          prev_req.verb.should eq \"POST\"\n          simple_response 200\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/request_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def request\n  Halite::Request.new(\n    \"get\",\n    URI.parse(\"http://example.com/foo/bar?q=halite#result\"),\n    HTTP::Headers{\"Accept\" => \"text/html\"},\n  )\nend\n\ndescribe Halite::Request do\n  describe \"#verb\" do\n    it \"provides a #verb getter with upcase\" do\n      request.verb.should eq \"GET\"\n    end\n  end\n\n  describe \"#scheme\" do\n    it \"provides a #scheme getter\" do\n      request.scheme.should eq \"http\"\n    end\n  end\n\n  describe \"#headers\" do\n    it \"provides a given headers\" do\n      request.headers[\"Accept\"].should eq \"text/html\"\n    end\n\n    it \"could not set header with key and value\" do\n      request.headers[\"Via\"] = \"Halite\"\n      request.headers[\"Via\"]?.should eq nil\n    end\n  end\n\n  describe \"#domain\" do\n    it \"return `URI` with the scheme, user, password, port and host combined\" do\n      request.domain.to_s.should eq \"http://example.com\"\n    end\n\n    context \"when subdomain and path are the same\" do\n      it \"return `URI` with the scheme, user, password, port and host combined\" do\n        Halite::Request.new(\"get\", URI.parse(\"https://login.example.com/login\")).domain.to_s.should eq \"https://login.example.com\"\n      end\n    end\n  end\n\n  describe \"#full_path\" do\n    it \"provides a full_path\" do\n      request.full_path.should eq \"/foo/bar?q=halite#result\"\n    end\n  end\n\n  describe \"#body\" do\n    it \"provides a body\" do\n      request.body.should eq \"\"\n    end\n  end\n\n  describe \"#redirect\" do\n    it \"should return a new request\" do\n      request = Halite::Request.new(\"GET\", URI.parse(\"http://httpbin.com/redirect/3\"), headers: HTTP::Headers{\"Host\" => \"httpbin.com\"})\n      new_request = request.redirect(\"http://httpbin.com/redirect/2\")\n      new_request.uri.to_s.should eq(\"http://httpbin.com/redirect/2\")\n      new_request.headers.has_key?(\"Host\").should be_false\n      new_request.verb.should eq(\"GET\")\n      request.uri.to_s.should eq(\"http://httpbin.com/redirect/3\")\n      request.verb.should eq(\"GET\")\n      request.headers.has_key?(\"Host\").should be_true\n    end\n\n    it \"should return a new request without Host\" do\n      request = Halite::Request.new(\"POST\", URI.parse(\"http://httpbin.com/redirect/3\"))\n      new_request = request.redirect(\"http://httpbin.com/redirect/2\", \"GET\")\n      new_request.uri.to_s.should eq(\"http://httpbin.com/redirect/2\")\n      new_request.verb.should eq(\"GET\")\n      new_request.headers.has_key?(\"Host\").should be_false\n      request.uri.to_s.should eq(\"http://httpbin.com/redirect/3\")\n      request.verb.should eq(\"POST\")\n      request.headers.has_key?(\"Host\").should be_false\n    end\n  end\n\n  describe \"raises\" do\n    it \"should throws an exception with not allowed request method\" do\n      expect_raises Halite::UnsupportedMethodError, \"Unknown method: TRACE\" do\n        Halite::Request.new(\"trace\", URI.parse(\"http://httpbin.org/get\"))\n      end\n    end\n\n    it \"should throws an exception without scheme part of URI\" do\n      expect_raises Halite::UnsupportedSchemeError, \"Missing scheme: example.com\" do\n        Halite::Request.new(\"get\", URI.parse(\"example.com\"))\n      end\n    end\n\n    it \"should throws an exception with not allowed scheme part of URI\" do\n      expect_raises Halite::UnsupportedSchemeError, \"Unknown scheme: ws\" do\n        Halite::Request.new(\"get\", URI.parse(\"ws://example.com\"))\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite/response_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate URL         = \"http://example.com\"\nprivate STATUS_CODE = 200\nprivate HEADERS     = HTTP::Headers{\"Content-Type\" => \"text/plain; charset=utf-8\"}\nprivate BODY        = \"hello world\"\nprivate COOKIES     = \"foo=bar; domain=example.com\"\n\nprivate def response(url = URL, status_code = STATUS_CODE, headers = HEADERS, body = BODY)\n  Halite::Response.new(\n    URI.parse(url),\n    HTTP::Client::Response.new(status_code: status_code, body: body, headers: headers)\n  )\nend\n\nprivate def empty_response\n  Halite::Response.new(\n    URI.parse(URL),\n    HTTP::Client::Response.new(status_code: 404, body: \"\", headers: HTTP::Headers.new)\n  )\nend\n\ndescribe Halite::Response do\n  describe \"to_a\" do\n    it \"returns a Rack-like array\" do\n      response.to_a.should eq([STATUS_CODE, HEADERS, BODY])\n    end\n  end\n\n  describe \"#content_length\" do\n    it \"should without Content-Length header\" do\n      response.content_length.should be_nil\n    end\n\n    it \"should return content length with number\" do\n      r = response(headers: HTTP::Headers{\"Content-Length\" => \"5\"})\n      r.content_length.should eq 5\n    end\n\n    it \"should return invalid Content-Length\" do\n      r = response(headers: HTTP::Headers{\"Content-Length\" => \"foo\"})\n      expect_raises ArgumentError do\n        r.content_length\n      end\n    end\n  end\n\n  describe \"#cookies\" do\n    it \"should HTTP::Cookies class\" do\n      r = response(headers: HTTP::Headers{\"Set-Cookie\" => COOKIES})\n      r.cookies.class.should eq HTTP::Cookies\n      r.cookies[\"foo\"].class.should eq HTTP::Cookie\n      r.cookies[\"foo\"].value.should eq \"bar\"\n      r.cookies[\"foo\"].domain.should eq \"example.com\"\n    end\n  end\n\n  describe \"#content_type\" do\n    it \"should return nil with empty headers\" do\n      empty_response.content_type.should be_nil\n    end\n\n    it \"should return with string with contains headers\" do\n      response.content_type.should eq \"text/plain\"\n    end\n  end\n\n  describe \"#links\" do\n    # NOTE: more specs in `header_link_spec.cr`.\n    it \"should return nil without Link Header\" do\n      response.links.should eq nil\n    end\n\n    it \"should return a list of links\" do\n      r = response(headers: HTTP::Headers{\"Link\" => %Q{<https://api.github.com/user/repos?page=3&per_page=100>; rel=\"next\"; title=\"Next Page\", </>; rel=\"http://example.net/foo\"}})\n      r.links.should be_a Hash(String, Halite::HeaderLink)\n      if links = r.links\n        links.has_key?(\"next\").should be_true\n        links[\"next\"].rel.should eq \"next\"\n        links[\"next\"].target.should eq \"https://api.github.com/user/repos?page=3&per_page=100\"\n        links[\"next\"].params.size.should eq 1\n        links[\"next\"].params[\"title\"].should eq \"Next Page\"\n        links[\"next\"].to_s.should eq \"https://api.github.com/user/repos?page=3&per_page=100\"\n\n        links.has_key?(\"/\").should be_false\n        links[\"http://example.net/foo\"].rel.should eq \"http://example.net/foo\"\n        links[\"http://example.net/foo\"].target.should eq \"http://example.net/foo\"\n        links[\"http://example.net/foo\"].params.size.should eq 0\n        links[\"http://example.net/foo\"].to_s.should eq \"http://example.net/foo\"\n      end\n    end\n  end\n\n  describe \"#rate_limit\" do\n    # NOTE: more specs in `rate_limit_spec.cr`.\n    it \"should return nil without RateLimit Header\" do\n      response.rate_limit.should eq nil\n    end\n\n    it \"should return rate limit\" do\n      r = response(headers: HTTP::Headers{\n        \"X-RateLimit-Limit\"     => \"5000\",\n        \"X-RateLimit-Remaining\" => \"4991\",\n        \"X-RateLimit-Reset\"     => \"1613727325\",\n      })\n\n      r.rate_limit.should be_a Halite::RateLimit\n      r.rate_limit.not_nil!.limit.should eq 5000\n      r.rate_limit.not_nil!.remaining.should eq 4991\n      r.rate_limit.not_nil!.reset.should eq 1613727325\n    end\n  end\n\n  describe \"#raise_for_status\" do\n    it \"should returns nil when status_code not range in (400..599)\" do\n      response.raise_for_status.should be_nil\n    end\n\n    (400..499).each do |code|\n      it \"throws an Halite::ClientError if status_code is #{code}\" do\n        expect_raises Halite::ClientError do\n          response(status_code: code).raise_for_status\n        end\n      end\n    end\n\n    (500..599).each do |code|\n      it \"throws an Halite::ServerError if status_code is #{code}\" do\n        expect_raises Halite::ServerError do\n          response(status_code: code).raise_for_status\n        end\n      end\n    end\n  end\n\n  describe \"#parse\" do\n    context \"with known content type\" do\n      it \"returns parsed body\" do\n        r = response(headers: HTTP::Headers{\"Content-Type\" => \"application/json;charset=utf-8\"}, body: %q{{\"foo\":\"bar\"}})\n        r.parse.should eq({\"foo\" => \"bar\"})\n      end\n    end\n\n    context \"with empty content type\" do\n      it \"raises Halite::UnRegisterMimeTypeError\" do\n        r = response(headers: HTTP::Headers{\"Content-Type\" => \"\"})\n        expect_raises Halite::Error do\n          r.parse\n        end\n      end\n    end\n\n    context \"without content type\" do\n      it \"raises Halite::UnRegisterMimeTypeError\" do\n        r = response(headers: HTTP::Headers{\"Etag\" => \"123123123\"})\n        expect_raises Halite::Error do\n          r.parse\n        end\n      end\n    end\n\n    context \"with unknown content type\" do\n      it \"raises Halite::UnRegisterMimeTypeError\" do\n        r = response(headers: HTTP::Headers{\"Content-Type\" => \"application/html\"})\n        expect_raises Halite::UnRegisterMimeTypeError do\n          r.parse\n        end\n      end\n    end\n\n    context \"with explicitly given mime type\" do\n      it \"ignores mime_type of response\" do\n        r = response(headers: HTTP::Headers{\"Content-Type\" => \"application/html; charset=utf-8\"}, body: %q{{\"foo\":\"bar\"}})\n        r.parse(\"application/json\").should eq({\"foo\" => \"bar\"})\n      end\n\n      it \"supports MIME type aliases\" do\n        r = response(headers: HTTP::Headers{\"Content-Type\" => \"application/html; charset=utf-8\"}, body: %q{{\"foo\":\"bar\"}})\n        r.parse(\"json\").should eq({\"foo\" => \"bar\"})\n      end\n    end\n  end\n\n  describe \"#inspect\" do\n    it \"returns human-friendly response representation\" do\n      response.inspect.should eq %q{#<Halite::Response HTTP/1.1 200 OK {\"Content-Type\" => \"text/plain; charset=utf-8\"}>}\n    end\n  end\nend\n"
  },
  {
    "path": "spec/halite_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate def without_timezone(&block)\n  with_timezone(nil, &block)\nend\n\nprivate def with_timezone(timezone : String? = nil, &block)\n  current_timezone = ENV[\"TZ\"]?\n  restore_timezone = false\n\n  if current_timezone && timezone.nil?\n    restore_timezone = true\n    ENV.delete(\"TZ\")\n  end\n\n  if timezone\n    restore_timezone = true\n    ENV[\"TZ\"] = timezone.not_nil!\n  end\n\n  block.call\n\n  ENV[\"TZ\"] = current_timezone if restore_timezone\nend\n\ndescribe Halite::Helper do\n  describe \"#timestamp\" do\n    it \"should use utc timezone as default location\" do\n      without_timezone do\n        ENV[\"TZ\"]?.should be_nil\n        t = Time.utc(2021, 2, 10, 22, 5, 13)\n        Halite::Helper.to_rfc3339(t).should eq \"2021-02-10T22:05:13Z\"\n      end\n    end\n\n    it \"should use given timezone\" do\n      without_timezone do\n        ENV[\"TZ\"]?.should be_nil\n        t = Time.utc(2021, 2, 10, 22, 5, 13)\n        timezone = \"Asia/Shanghai\"\n        Halite::Helper.to_rfc3339(t, timezone: timezone).should eq \"2021-02-11T06:05:13+08:00\"\n      end\n    end\n\n    it \"should use `TZ` timezone from ENV\" do\n      timezone = \"Asia/Shanghai\"\n      with_timezone(timezone) do\n        ENV[\"TZ\"].should eq timezone\n        t = Time.utc(2021, 2, 10, 22, 5, 13)\n        Halite::Helper.to_rfc3339(t).should eq \"2021-02-11T06:05:13+08:00\"\n      end\n    end\n\n    it \"should overwrite given timezone\" do\n      timezone = \"Asia/Shanghai\"\n      with_timezone(timezone) do\n        ENV[\"TZ\"].should eq timezone\n        t = Time.utc(2021, 2, 10, 22, 5, 13)\n        Halite::Helper.to_rfc3339(t, timezone: \"Europe/Berlin\").should eq \"2021-02-10T23:05:13+01:00\"\n      end\n    end\n  end\nend\n\ndescribe Halite do\n  describe \".new\" do\n    it \"returns a instance class\" do\n      client = Halite::Client.new\n      client.should be_a(Halite::Client)\n      client.options.should be_a(Halite::Options)\n    end\n  end\n\n  describe \".get\" do\n    context \"loading a simple uri\" do\n      it \"should easy to request\" do\n        response = Halite.get(SERVER.endpoint)\n        response.to_s.should match(/<!doctype html>/)\n      end\n    end\n\n    context \"with query string parameters\" do\n      it \"should easy to request\" do\n        response = Halite.get(SERVER.api(\"params\"), params: {foo: \"bar\"})\n        response.to_s.should eq(\"Params!\")\n      end\n    end\n\n    context \"with query string parameters in the URI and opts hash\" do\n      it \"includes both\" do\n        response = Halite.get(\"#{SERVER.endpoint}/multiple-params?foo=bar\", params: {baz: \"quux\"})\n        response.to_s.should eq(\"More Params!\")\n      end\n    end\n\n    context \"streaming\" do\n      it \"is easy\" do\n        data = [] of JSON::Any\n        Halite.headers(\"via\": \"foo\").get(SERVER.api(\"stream?n=2\")) do |response|\n          response.status_code.should eq 200\n          response.headers[\"Transfer-Encoding\"].should eq \"chunked\"\n          while content = response.body_io.gets\n            data << JSON.parse(content)\n          end\n        end\n\n        data.size.should eq 2\n        data.first.as_h[\"verb\"].should eq \"GET\"\n        data.first.as_h[\"headers\"].as_h[\"Via\"].should eq \"foo\"\n      end\n    end\n\n    context \"with headers\" do\n      it \"is easy\" do\n        response = Halite.accept(\"application/json\").get(SERVER.endpoint)\n        response.to_s.should match(/json/)\n      end\n\n      it \"is auth\" do\n        user = \"halite\"\n        password = \"p@ssword\"\n        secret = Base64.strict_encode(\"#{user}:#{password}\")\n        response = Halite.auth(secret).get(SERVER.api(\"auth\"))\n        response.to_s.should eq(secret)\n      end\n\n      it \"is basic auth\" do\n        user = \"halite\"\n        password = \"p@ssword\"\n        credentials = Base64.strict_encode(\"#{user}:#{password}\")\n        response = Halite.basic_auth(user, password).get(SERVER.api(\"auth\"))\n        response.to_s.should eq(\"Basic #{credentials}\")\n      end\n    end\n\n    context \"loading binary data\" do\n      it \"is a png file\" do\n        response = Halite.get SERVER.api(\"image\")\n        response.headers[\"Content-Type\"].should eq \"image/png\"\n        response.filename.should eq \"logo.png\"\n      end\n\n      it \"with streaming\" do\n        original_path = File.expand_path(\"../../halite-logo.png\", __FILE__)\n        Halite.get SERVER.api(\"image\") do |response|\n          File.open(original_path, \"r\") do |original_file|\n            while byte = response.body_io.read_byte\n              original_file.read_byte.should eq byte\n            end\n          end\n        end\n      end\n    end\n\n    context \"with a large request body\" do\n      [16_000, 16_500, 17_000, 34_000, 68_000].each do |size|\n        [0, rand(0..100), rand(100..1000)].each do |fuzzer|\n          context \"with a #{size} body and #{fuzzer} of fuzzing\" do\n            it \"returns a large body\" do\n              characters = (\"A\"..\"Z\").to_a\n              form = Hash(String, String).new.tap { |obj| (size + fuzzer).times { |i| obj[i.to_s] = characters[i % characters.size] } }\n              response = Halite.post SERVER.api(\"echo-body\"), form: form\n              response_body = HTTP::Params.encode(form)\n\n              response.to_s.should eq(response_body)\n              response.content_length.should eq(response_body.bytesize)\n            end\n          end\n        end\n\n        context \"with `.timeout`\" do\n          [nil, 10, 10.0, 10.seconds].each do |timeout|\n            it \"writes the whole body with #{timeout.inspect}\" do\n              body = \"“\" * 1_000_000\n              response = Halite.timeout(timeout).post SERVER.api(\"echo-body\"), raw: body\n\n              response.to_s.should eq(body)\n              response.content_length.should eq(body.bytesize)\n            end\n          end\n\n          it \"writes the whole body with apiece arguments\" do\n            body = \"“\" * 1_000_000\n            response = Halite.timeout(10, 10.0, 10.seconds).post SERVER.api(\"echo-body\"), raw: body\n\n            response.to_s.should eq(body)\n            response.content_length.should eq(body.bytesize)\n          end\n        end\n      end\n    end\n\n    context \"fetching rate-limit headers\" do\n      it \"should easy to read\" do\n        response = Halite.get(SERVER.api(\"rate-limit\"))\n        response.headers[\"X-RateLimit-Limit\"].should eq \"6000\"\n        response.headers[\"X-RateLimit-Remaining\"].should eq \"5998\"\n        response.headers[\"X-RateLimit-Reset\"].should eq \"1613727325\"\n\n        response.rate_limit.should be_a Halite::RateLimit\n        response.rate_limit.not_nil!.limit.should eq 6000\n        response.rate_limit.not_nil!.remaining.should eq 5998\n        response.rate_limit.not_nil!.reset.should eq 1613727325\n      end\n    end\n  end\n\n  describe \".post\" do\n    context \"loading a simple form data\" do\n      it \"should easy to request with form data\" do\n        response = Halite.post(SERVER.api(\"form\"), form: {example: \"testing-form\"})\n        response.to_s.should contain(\"example: testing-form\")\n      end\n\n      it \"should easy to request with raw string\" do\n        response = Halite.post(SERVER.api(\"form\"), raw: \"example=testing-form\")\n        response.to_s.should contain(\"example: testing-form\")\n      end\n\n      it \"should easy to request with json data\" do\n        response = Halite.post(SERVER.api(\"form\"), json: {\"job\" => {\"title\" => [\"foo\", \"bar\"], \"info\" => {gender: \"male\"}}})\n        response.to_s.should contain(%Q({\"job\":{\"title\":[\"foo\",\"bar\"],\"info\":{\"gender\":\"male\"}}}))\n      end\n    end\n\n    context \"uploading file\" do\n      it \"should easy upload only file\" do\n        response = Halite.post(SERVER.api(\"upload\"), form: {file: File.open(\"./src/halite.cr\")})\n        body = response.parse.as_h\n        params = body[\"params\"].as_h\n        files = body[\"files\"].as_h\n\n        params.size.should eq 0\n\n        files.size.should eq 1\n        files[\"file\"]?.should be_a JSON::Any\n        files[\"file\"].as_h[\"filename\"].should eq \"halite.cr\"\n      end\n\n      it \"should easy upload file with other form data\" do\n        response = Halite.post(SERVER.api(\"upload\"), form: {file: File.open(\"./src/halite.cr\"), \"name\": \"foobar\"})\n        body = response.parse.as_h\n        params = body[\"params\"].as_h\n        files = body[\"files\"].as_h\n\n        params.size.should eq 1\n        params[\"name\"].should eq \"foobar\"\n\n        files.size.should eq 1\n        files[\"file\"]?.should be_a JSON::Any\n        files[\"file\"].as_h[\"filename\"].should eq \"halite.cr\"\n      end\n\n      it \"should easy upload multiple files\" do\n        response = Halite.post(SERVER.api(\"upload\"), form: {avatar: [File.open(\"halite-logo.png\"), File.open(\"halite-logo-small.png\")]})\n        body = response.parse.as_h\n        params = body[\"params\"].as_h\n        files = body[\"files\"].as_h\n\n        params.size.should eq 0\n        files.size.should eq 1\n        files[\"avatar\"]?.should be_a JSON::Any\n        files[\"avatar\"].as_a.size.should eq 2\n        files[\"avatar\"].as_a[0].as_h[\"filename\"].should eq \"halite-logo.png\"\n        files[\"avatar\"].as_a[1].as_h[\"filename\"].should eq \"halite-logo-small.png\"\n      end\n\n      it \"should easy upload multiple files with other form data\" do\n        response = Halite.post(SERVER.api(\"upload\"), form: {\n          avatar: [File.open(\"halite-logo.png\"), File.open(\"halite-logo-small.png\")],\n          name:   \"foobar\",\n        })\n        body = response.parse.as_h\n        params = body[\"params\"].as_h\n        files = body[\"files\"].as_h\n\n        params.size.should eq 1\n        params[\"name\"].should eq \"foobar\"\n\n        files.size.should eq 1\n        files[\"avatar\"]?.should be_a JSON::Any\n        files[\"avatar\"].as_a.size.should eq 2\n        files[\"avatar\"].as_a[0].as_h[\"filename\"].should eq \"halite-logo.png\"\n        files[\"avatar\"].as_a[1].as_h[\"filename\"].should eq \"halite-logo-small.png\"\n      end\n    end\n\n    context \"streaming\" do\n      it \"is easy\" do\n        data = [] of JSON::Any\n        Halite.post(SERVER.api(\"stream?n=2\")) do |response|\n          response.status_code.should eq 200\n          response.headers[\"Transfer-Encoding\"].should eq \"chunked\"\n          while content = response.body_io.gets\n            data << JSON.parse(content)\n          end\n        end\n\n        data.size.should eq 2\n        data.first.as_h[\"verb\"].should eq \"POST\"\n      end\n    end\n  end\n\n  describe \".put\" do\n    it \"should easy to request\" do\n      response = Halite.put SERVER.endpoint\n      response.status_code.should eq(200)\n      response.content_type.should match(/html/)\n    end\n\n    context \"streaming\" do\n      it \"is easy\" do\n        data = [] of JSON::Any\n        Halite.put(SERVER.api(\"stream?n=2\")) do |response|\n          response.status_code.should eq 200\n          response.headers[\"Transfer-Encoding\"].should eq \"chunked\"\n          while content = response.body_io.gets\n            data << JSON.parse(content)\n          end\n        end\n\n        data.size.should eq 2\n        data.first.as_h[\"verb\"].should eq \"PUT\"\n      end\n    end\n  end\n\n  describe \".delete\" do\n    it \"should easy to request\" do\n      response = Halite.delete SERVER.endpoint\n      response.status_code.should eq(200)\n      response.content_type.should match(/html/)\n    end\n\n    context \"streaming\" do\n      it \"is easy\" do\n        data = [] of JSON::Any\n        Halite.delete(SERVER.api(\"stream?n=2\")) do |response|\n          response.status_code.should eq 200\n          response.headers[\"Transfer-Encoding\"].should eq \"chunked\"\n          while content = response.body_io.gets\n            data << JSON.parse(content)\n          end\n        end\n\n        data.size.should eq 2\n        data.first.as_h[\"verb\"].should eq \"DELETE\"\n      end\n    end\n  end\n\n  describe \".patch\" do\n    it \"should easy to request\" do\n      response = Halite.patch SERVER.endpoint\n      response.status_code.should eq(200)\n      response.content_type.should match(/html/)\n    end\n\n    context \"streaming\" do\n      it \"is easy\" do\n        data = [] of JSON::Any\n        Halite.patch(SERVER.api(\"stream?n=2\")) do |response|\n          response.status_code.should eq 200\n          response.headers[\"Transfer-Encoding\"].should eq \"chunked\"\n          while content = response.body_io.gets\n            data << JSON.parse(content)\n          end\n        end\n\n        data.size.should eq 2\n        data.first.as_h[\"verb\"].should eq \"PATCH\"\n      end\n    end\n  end\n\n  describe \".head\" do\n    it \"should easy to request\" do\n      response = Halite.head SERVER.endpoint\n      response.status_code.should eq(200)\n      response.content_type.should match(/html/)\n    end\n  end\n\n  describe \".options\" do\n    it \"should easy to request\" do\n      response = Halite.options SERVER.endpoint\n      response.status_code.should eq(200)\n      response.content_type.should match(/html/)\n    end\n\n    context \"streaming\" do\n      it \"is easy\" do\n        data = [] of JSON::Any\n        Halite.options(SERVER.api(\"stream?n=2\")) do |response|\n          response.status_code.should eq 200\n          response.headers[\"Transfer-Encoding\"].should eq \"chunked\"\n          while content = response.body_io.gets\n            data << JSON.parse(content)\n          end\n        end\n\n        data.size.should eq 2\n        data.first.as_h[\"verb\"].should eq \"OPTIONS\"\n      end\n    end\n  end\n\n  describe \".request\" do\n    %w[get post put delete head patch options].each do |verb|\n      it \"should easy to #{verb} request\" do\n        response = Halite.request(verb, SERVER.endpoint)\n        response.status_code.should eq(200)\n      end\n\n      it \"should easy to #{verb} request with hash or namedtuple\" do\n        response = Halite.request(verb, SERVER.endpoint, params: {name: \"foo\"})\n        response.status_code.should eq(200)\n      end\n\n      it \"should easy to #{verb} request with options\" do\n        response = Halite.request(verb, SERVER.endpoint, Halite::Options.new)\n        response.status_code.should eq(200)\n      end\n\n      it \"should easy to #{verb} streaming request\" do\n        data = [] of JSON::Any\n        Halite.request(verb, SERVER.api(\"stream?n=2\")) do |response|\n          response.status_code.should eq 200\n          response.headers[\"Transfer-Encoding\"].should eq \"chunked\"\n\n          if verb != \"head\"\n            while content = response.body_io.gets\n              data << JSON.parse(content)\n            end\n          else\n            expect_raises NilAssertionError do\n              response.body_io\n            end\n          end\n        end\n\n        if verb != \"head\"\n          data.size.should eq 2\n          data.first.as_h[\"verb\"].should eq verb.upcase\n        else\n          data.size.should eq 0\n        end\n      end\n    end\n\n    it \"throws an exception with non-support method\" do\n      expect_raises Halite::UnsupportedMethodError do\n        Halite.request(\"abc\", SERVER.endpoint)\n      end\n    end\n\n    it \"throws an exception with non-support scheme\" do\n      expect_raises Halite::UnsupportedSchemeError do\n        Halite.request(\"get\", \"ws://example.com/abc\")\n      end\n    end\n\n    it \"throws an exception without scheme\" do\n      expect_raises Halite::UnsupportedSchemeError do\n        Halite.request(\"get\", \"example.com/abc\")\n      end\n    end\n  end\n\n  describe \".follow\" do\n    context \"without redirects\" do\n      it \"should return empty history\" do\n        response = Halite.get(SERVER.api(\"/\"))\n        response.history.size.should eq(0)\n      end\n    end\n\n    context \"with redirects\" do\n      it \"should return one history with non-redirect url\" do\n        response = Halite.follow.get(SERVER.api(\"/\"))\n        response.history.size.should eq(1)\n        response.to_s.should match(/<!doctype html>/)\n      end\n\n      it \"should easy for 301 with full uri\" do\n        response = Halite.follow.get(SERVER.api(\"redirect-301\"))\n        response.history.size.should eq(2)\n        response.to_s.should match(/<!doctype html>/)\n      end\n\n      it \"should easy for 301 with relative path\" do\n        response = Halite.follow.get(SERVER.api(\"redirect-301\"), params: {\"relative_path\" => true})\n        response.history.size.should eq(2)\n        response.to_s.should match(/<!doctype html>/)\n      end\n\n      it \"should easy for 301 with relative path which is not include slash\" do\n        response = Halite.follow.get(SERVER.api(\"redirect-301\"), params: {\"relative_path_without_slash\" => true})\n        response.history.size.should eq(2)\n        response.to_s.should eq(\"hello\")\n      end\n\n      it \"should easy for 302\" do\n        response = Halite.follow.get(SERVER.api(\"redirect-302\"))\n        response.history.size.should eq(2)\n        response.to_s.should match(/<!doctype html>/)\n      end\n\n      it \"should store full history\" do\n        times = 5\n        response = Halite.follow.get(\"#{SERVER.endpoint}/multi-redirect?n=#{times}\")\n        response.history.class.should eq Array(Halite::Response)\n        response.history.size.should eq(times + 1)\n      end\n    end\n  end\n\n  describe \".endpoint\" do\n    it \"sets endpoint with String\" do\n      endpoint = \"https://example.com\"\n      client = Halite.endpoint(endpoint)\n      client.options.endpoint.should eq(URI.parse(endpoint))\n    end\n\n    it \"sets endpoint with String\" do\n      endpoint = URI.parse(\"https://example.com\")\n      client = Halite.endpoint(endpoint)\n      client.options.endpoint.should eq(endpoint)\n    end\n  end\n\n  describe \".auth\" do\n    it \"sets Authorization header to the given value\" do\n      client = Halite.auth(\"abc\")\n      client.options.headers[\"Authorization\"].should eq(\"abc\")\n    end\n  end\n\n  describe \".basic_auth\" do\n    it \"sets Authorization header with proper BasicAuth value\" do\n      client = Halite.basic_auth(user: \"foo\", pass: \"bar\")\n      client.options.headers[\"Authorization\"].should match(%r{^Basic [A-Za-z0-9+/]+=*$})\n    end\n  end\n\n  describe \".user_agent\" do\n    it \"uses default user agent\" do\n      r = Halite.get SERVER.api(\"user_agent\")\n      r.body.should eq(Halite::Request::USER_AGENT)\n    end\n\n    it \"sets user agent\" do\n      user_agent = \"Awesome Halite Client\"\n      client = Halite.user_agent(user_agent)\n      client.options.headers[\"User-Agent\"].should eq(user_agent)\n\n      r = client.get SERVER.api(\"user_agent\")\n      r.body.should eq(user_agent)\n    end\n  end\n\n  describe \".timeout\" do\n    context \"without timeout type\" do\n      it \"sets given timeout options\" do\n        client = Halite.timeout(connect: 12, read: 6, write: 36)\n        client.options.timeout.connect.should eq(12)\n        client.options.timeout.read.should eq(6)\n        client.options.timeout.write.should eq(36)\n      end\n    end\n  end\n\n  describe \".cookies\" do\n    it \"passes correct `Cookie` header\" do\n      response = Halite.cookies(abc: \"def\").get(SERVER.api(\"cookies\"))\n      response.to_s.should eq(\"abc: def\")\n    end\n\n    it \"properly works with cookie jars from response\" do\n      cookies = Halite.get(SERVER.api(\"cookies\")).cookies\n      response = Halite.cookies(cookies).get(SERVER.api(\"cookies\"))\n      response.to_s.should eq(\"foo: bar\")\n    end\n\n    it \"properly merges cookies\" do\n      cookies = Halite.get(SERVER.api(\"cookies\")).cookies\n      response = Halite.cookies(foo: 123, bar: 321).cookies(cookies).get(SERVER.api(\"cookies\"))\n      response.to_s.should contain(\"foo: bar\\nbar: 321\")\n    end\n  end\n\n  describe \".logging\" do\n    it \"should use logging\" do\n      client = Halite.logging\n      client.options.features.has_key?(\"logging\").should be_true\n      client.options.features[\"logging\"].should be_a(Halite::Logging)\n      logging = client.options.features[\"logging\"].as(Halite::Logging)\n      logging.writer.should be_a(Halite::Logging::Common)\n      logging.writer.skip_request_body.should be_false\n      logging.writer.skip_response_body.should be_false\n      logging.writer.skip_benchmark.should be_false\n      logging.writer.colorize.should be_true\n    end\n\n    it \"sets logging with format\" do\n      client = Halite.logging(format: \"json\", skip_response_body: true)\n      client.options.features.has_key?(\"logging\").should be_true\n      client.options.features[\"logging\"].should be_a(Halite::Logging)\n      logging = client.options.features[\"logging\"].as(Halite::Logging)\n      logging.writer.should be_a(Halite::Logging::JSON)\n      logging.writer.skip_request_body.should be_false\n      logging.writer.skip_response_body.should be_true\n      logging.writer.skip_benchmark.should be_false\n      logging.writer.colorize.should be_true\n    end\n\n    it \"sets logging into file\" do\n      with_tempfile(\"halite-spec-logging\") do |file|\n        Log.setup(\"halite.spec.file\", backend: Log::IOBackend.new(File.open(file, \"a\")))\n        client = Halite.logging(for: \"halite.spec.file\", skip_response_body: true)\n        client.options.features.has_key?(\"logging\").should be_true\n        client.options.features[\"logging\"].should be_a(Halite::Logging)\n        logging = client.options.features[\"logging\"].as(Halite::Logging)\n        logging.writer.should be_a(Halite::Logging::Common)\n        logging.writer.skip_request_body.should be_false\n        logging.writer.skip_response_body.should be_true\n        logging.writer.skip_benchmark.should be_false\n        logging.writer.colorize.should be_true\n\n        client.get SERVER.endpoint\n\n        # waiting file writes\n        sleep 1\n\n        logs = File.read(file)\n        logs.should contain(\"request\")\n        logs.should contain(\"response\")\n        logs.should_not contain(\"<!doctype html><body>Mock Server is running.</body></html>\")\n      end\n    end\n\n    it \"sets logging with custom logging\" do\n      client = Halite.logging(logging: SimpleLogger.new(skip_benchmark: true))\n      client.options.features.has_key?(\"logging\").should be_true\n      client.options.features[\"logging\"].should be_a(Halite::Logging)\n      logging = client.options.features[\"logging\"].as(Halite::Logging)\n      logging.writer.should be_a(SimpleLogger)\n      logging.writer.skip_request_body.should be_false\n      logging.writer.skip_response_body.should be_false\n      logging.writer.skip_benchmark.should be_true\n      logging.writer.colorize.should be_true\n    end\n  end\n\n  describe \".use\" do\n    describe \"built-in features\" do\n      it \"sets given feature name\" do\n        client = Halite.use(\"logging\")\n        client.options.features.has_key?(\"logging\").should be_true\n        client.options.features[\"logging\"].should be_a(Halite::Logging)\n        logging = client.options.features[\"logging\"].as(Halite::Logging)\n        logging.writer.should be_a(Halite::Logging::Common)\n        logging.writer.skip_request_body.should be_false\n        logging.writer.skip_response_body.should be_false\n        logging.writer.skip_benchmark.should be_false\n        logging.writer.colorize.should be_true\n      end\n\n      it \"sets logging with logging\" do\n        client = Halite.use(\"logging\", logging: Halite::Logging::JSON.new(skip_request_body: true, colorize: false))\n        client.options.features.has_key?(\"logging\").should be_true\n        client.options.features[\"logging\"].should be_a(Halite::Logging)\n        logging = client.options.features[\"logging\"].as(Halite::Logging)\n        logging.writer.should be_a(Halite::Logging::JSON)\n        logging.writer.skip_request_body.should be_true\n        logging.writer.skip_response_body.should be_false\n        logging.writer.skip_benchmark.should be_false\n        logging.writer.colorize.should be_false\n\n        # Restore\n        Colorize.on_tty_only!\n      end\n    end\n\n    describe \"custom features\" do\n      it \"should modify the headers of request and response\" do\n        response = Halite.use(\"append_headers\").get(SERVER.api(\"/anything?a=b\"))\n        response.headers[\"X-Powered-By\"].should eq(\"Halite\")\n        response.parse.as_h[\"headers\"][\"X-API-Limit\"].should eq(\"60\")\n      end\n\n      it \"should mock response with interceptor\" do\n        response = Halite.use(\"mock\").get(SERVER.api(\"/anything?a=b\"))\n        response.status_code.should eq(400)\n        response.body.should eq(\"mock\")\n      end\n\n      describe \"enable multiple interceptors\" do\n        it \"should call next intercept\" do\n          response = Halite.use(\"404\").use(\"powered_by\").get(SERVER.api(\"/anything?a=b\"))\n          response.status_code.should eq(404)\n          response.headers[\"X-Powered-By\"].should eq(\"Halite\")\n          response.body.should_not eq(\"\")\n        end\n\n        it \"should return on first interceptor\" do\n          response = Halite.use(\"mock\").use(\"404\").get(SERVER.api(\"/anything?a=b\"))\n          response.status_code.should eq(400)\n          response.body.should eq(\"mock\")\n        end\n      end\n    end\n  end\n\n  describe \"raise\" do\n    it \"should throws a Halite::ConnectionError exception with not exist uri\" do\n      expect_raises Halite::ConnectionError do\n        Halite.get(\"http://404-not_found.xyz/\")\n      end\n    end\n\n    it \"should throws a Halite::ConnectionError exception with illegal port\" do\n      expect_raises Halite::ConnectionError do\n        Halite.get(\"http://127.0.0.1:000\")\n      end\n    end\n\n    it \"should throws a Halite::TimeoutError exception with long time not response\" do\n      expect_raises Halite::TimeoutError do\n        Halite.timeout(read: 1.milliseconds).get(\"https://baidu.com/\")\n      end\n    end\n\n    it \"should throws a Halite::RequestError exception with http request via tls\" do\n      expect_raises Halite::RequestError, \"SSL context given for HTTP URI = http://google.com\" do\n        Halite.timeout(connect: 1.milliseconds).get(\"http://google.com\", tls: OpenSSL::SSL::Context::Client.new)\n      end\n    end\n  end\n\n  describe Halite::FeatureRegister do\n    it \"should use a registered feature\" do\n      Halite.feature?(\"null\").should be_nil\n      Halite.register_feature \"null\", TestFeatures::Null\n      Halite.has_feature?(\"null\").should be_true\n      Halite.feature(\"null\").should eq(TestFeatures::Null)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"./support/mock_server\"\nrequire \"../src/halite\"\n\ndef with_tempfile(filename)\n  yield File.tempname(filename)\nend\n\nmodule TestFeatures\n  class Null < Halite::Feature; end\n\n  class AppendHeaders < Halite::Feature\n    def request(request)\n      request.headers[\"X-API-Limit\"] = \"60\"\n      request\n    end\n\n    def response(response)\n      response.headers[\"X-Powered-By\"] = \"Halite\"\n      response\n    end\n\n    Halite.register_feature \"append_headers\", self\n  end\nend\n\nmodule TestInterceptors\n  class Mock < Halite::Feature\n    def intercept(chain)\n      response = Halite::Response.new(chain.request.uri, 400, \"mock\")\n      chain.return(response)\n    end\n\n    Halite.register_feature \"mock\", self\n  end\n\n  class AlwaysNotFound < Halite::Feature\n    def intercept(chain)\n      response = chain.perform\n      response = Halite::Response.new(chain.request.uri, 404, response.body, response.headers)\n      chain.next(response)\n    end\n\n    Halite.register_feature \"404\", self\n  end\n\n  class PoweredBy < Halite::Feature\n    def intercept(chain)\n      if response = chain.response\n        response.headers[\"X-Powered-By\"] = \"Halite\"\n        chain.return(response)\n      else\n        chain\n      end\n    end\n\n    Halite.register_feature \"powered_by\", self\n  end\nend\n\nclass SimpleLogger < Halite::Logging::Abstract\n  def request(request)\n    @logger.info { \"request\" }\n  end\n\n  def response(response)\n    @logger.info { \"response\" }\n  end\n\n  Halite::Logging.register \"simple\", self\nend\n\ndef fixture_path(file)\n  File.join(File.dirname(__FILE__), \"fixtures\", file)\nend\n\ndef load_fixture(file)\n  File.read_lines(fixture_path(file)).join(\"\\n\")\nend\n\n####################\n# Start mock server\n####################\nSERVER = MockServer.new\nspawn do\n  SERVER.listen\nend\n"
  },
  {
    "path": "spec/support/mock_server/route_handler.cr",
    "content": "class MockServer < HTTP::Server\n  class RouteHandler\n    include HTTP::Handler\n\n    METHODS = [:get, :post, :put, :delete, :head, :patch, :options]\n    ROUTES  = {} of String => (HTTP::Server::Context -> HTTP::Server::Context)\n\n    def call(context : HTTP::Server::Context)\n      process_route(context)\n    end\n\n    def process_route(context : HTTP::Server::Context)\n      method = context.request.method.downcase\n      path = context.request.path.to_s\n      route = \"#{method}:#{path}\"\n\n      if block = ROUTES[route]?\n        block.call(context)\n      else\n        not_found(context)\n      end\n    end\n\n    def not_found(context : HTTP::Server::Context)\n      context.response.status_code = 404\n      context.response.content_type = \"text/html\"\n      context.response.print \"Not Found\"\n\n      context\n    end\n\n    def self.not_found(context : HTTP::Server::Context)\n      context.response.status_code = 404\n      context.response.content_type = \"text/html\"\n      context.response.print \"Not Found\"\n\n      context\n    end\n\n    {% for verb in METHODS %}\n      def self.{{ verb.id }}(route : String, &block : HTTP::Server::Context -> HTTP::Server::Context)\n        ROUTES[\"{{ verb.id }}:#{route}\"] = block\n      end\n    {% end %}\n\n    def self.any(route : String, &block : HTTP::Server::Context -> HTTP::Server::Context)\n      METHODS.each do |method|\n        ROUTES[\"#{method}:#{route}\"] = block\n      end\n    end\n\n    # Any\n    any \"/anything\" do |context|\n      body = {\n        \"verb\"    => context.request.method,\n        \"url\"     => context.request.resource,\n        \"query\"   => context.request.query,\n        \"headers\" => context.request.headers.to_flat_h,\n      }\n\n      context.response.status_code = 200\n      context.response.content_type = \"application/json\"\n      context.response.print body.to_json\n      context\n    end\n\n    any \"/stream\" do |context|\n      total = context.request.query_params[\"n\"].to_i\n\n      body = {\n        \"verb\"    => context.request.method,\n        \"url\"     => context.request.resource,\n        \"query\"   => context.request.query,\n        \"headers\" => context.request.headers.to_flat_h,\n      }\n\n      total.times do |i|\n        context.response.puts body.to_json\n        context.response.flush\n      end\n\n      context\n    end\n\n    # GET\n    get \"/\" do |context|\n      context.response.status_code = 200\n\n      case context.request.headers[\"Accept\"]?\n      when \"application/json\"\n        context.response.content_type = \"application/json\"\n        context.response.print \"{\\\"json\\\": true}\"\n      else\n        context.response.content_type = \"text/html\"\n        context.response.print \"<!doctype html><body>Mock Server is running.</body></html>\"\n      end\n\n      context\n    end\n\n    get \"/sleep\" do |context|\n      sleep 2\n\n      context.response.status_code = 200\n      context.response.print \"hello\"\n      context\n    end\n\n    get \"/params\" do |context|\n      next not_found(context) unless context.request.query == \"foo=bar\"\n\n      context.response.status_code = 200\n      context.response.print \"Params!\"\n      context\n    end\n\n    get \"/multiple-params\" do |context|\n      next not_found(context) unless context.request.query_params == HTTP::Params.new({\"foo\" => [\"bar\"], \"baz\" => [\"quux\"]})\n\n      context.response.status_code = 200\n      context.response.print \"More Params!\"\n      context\n    end\n\n    get \"/bytes\" do |context|\n      bytes = [80, 75, 3, 4, 20, 0, 0, 0, 8, 0, 123, 104, 169, 70, 99, 243, 243]\n      context.response.content_type = \"application/octet-stream\"\n      context.response.print bytes.map { |b| b.unsafe_chr }.join\n\n      context\n    end\n\n    get \"/image\" do |context|\n      path = File.expand_path(\"../../../../halite-logo.png\", __FILE__)\n      context.response.content_type = \"image/png\"\n      context.response.content_length = File.size(path)\n      context.response.headers[\"Content-Disposition\"] = \"attachment; filename=logo.png\"\n      File.open(path) do |file|\n        IO.copy(file, context.response)\n      end\n      context\n    end\n\n    get \"/redirect-301\" do |context|\n      context.response.status_code = 301\n      location =\n        if context.request.query_params[\"relative_path\"]?\n          \"/\"\n        elsif context.request.query_params[\"relative_path_without_slash\"]?\n          \"sleep\"\n        else\n          {% if Crystal::VERSION < \"0.36.0\" %}\n            \"http://#{context.request.host_with_port}/\"\n          {% else %}\n            \"http://#{context.request.headers[\"Host\"]?}/\"\n          {% end %}\n        end\n\n      context.response.headers[\"Location\"] = location\n      context\n    end\n\n    get \"/redirect-302\" do |context|\n      context.response.status_code = 302\n      location =\n        if context.request.query_params[\"relative_path\"]?\n          \"/\"\n        else\n          {% if Crystal::VERSION < \"0.36.0\" %}\n            \"http://#{context.request.host_with_port}/\"\n          {% else %}\n            \"http://#{context.request.headers[\"Host\"]?}/\"\n          {% end %}\n        end\n\n      context.response.headers[\"Location\"] = location\n      context\n    end\n\n    get \"/multi-redirect\" do |context|\n      context.response.status_code = 302\n      if n = context.request.query_params[\"n\"]?\n        n = n.to_i\n        next_r = if (r = context.request.query_params[\"r\"]?)\n                   r.to_i + 1\n                 else\n                   1\n                 end\n\n        if next_r <= n\n          location = \"/multi-redirect?n=#{n}&r=#{next_r}\"\n          context.response.headers[\"Location\"] = location\n        else\n          context.response.status_code = 200\n          context.response.print \"Finished #{n} redirect\"\n        end\n      else\n        context.response.status_code = 200\n        context.response.print \"Please Set ?n={n} to multi-redirect\"\n      end\n\n      context\n    end\n\n    get \"/cookies\" do |context|\n      context.response.headers[\"Set-Cookie\"] = \"foo=bar\"\n      context.response.print context.request.cookies.map { |c| \"#{c.name}: #{c.value}\" }.join(\"\\n\")\n\n      context\n    end\n\n    get \"/get-cookies\" do |context|\n      body = JSON.build do |json|\n        json.object do\n          context.request.cookies.each do |cookie|\n            json.field cookie.name do\n              cookie.value.to_json(json)\n            end\n          end\n        end\n      end\n\n      context.response.content_type = \"application/json\"\n      context.response.print body\n\n      context\n    end\n\n    get \"/user_agent\" do |context|\n      body = context.request.headers[\"User-Agent\"]\n      context.response.print body\n      context\n    end\n\n    get \"/auth\" do |context|\n      body = context.request.headers[\"Authorization\"]\n      context.response.print body\n      context\n    end\n\n    get \"/rate-limit\" do |context|\n      context.response.headers[\"X-RateLimit-Limit\"] = \"6000\"\n      context.response.headers[\"X-RateLimit-Remaining\"] = \"5998\"\n      context.response.headers[\"X-RateLimit-Reset\"] = \"1613727325\"\n      context\n    end\n\n    # POST\n    post \"/\" do |context|\n      context.response.status_code = 200\n      context.response.content_type = \"text/html\"\n      context.response.print \"<!doctype html><body>Mock Server is running.</body></html>\"\n      context\n    end\n\n    post \"/echo-body\" do |context|\n      body = parse_body(context.request.body)\n      context.response.status_code = 200\n      context.response.content_length = body.bytesize\n      context.response.print body\n      context\n    end\n\n    post \"/form\" do |context|\n      form = parse_form(context.request.body)\n      if form.empty?\n        context.response.status_code = 400\n        context.response.print \"invalid form data! >:E\"\n      else\n        context.response.status_code = 200\n        form.each do |k, v|\n          context.response.print \"#{k}: #{v}\\n\"\n        end\n      end\n\n      context\n    end\n\n    post \"/upload\" do |context|\n      if multipart?(context.request.headers)\n        upload = parse_upload_form(context.request)\n        context.response.status_code = 200\n        context.response.content_type = \"application/json\"\n\n        body = JSON.build do |json|\n          json.object do\n            json.field \"params\" do\n              json.object do\n                upload.params.each do |k, v|\n                  json.field k, v\n                end\n              end\n            end\n\n            json.field \"files\" do\n              json.object do\n                upload.files.each do |k, v|\n                  json.field k do\n                    if v.is_a?(Array)\n                      json.array do\n                        v.as(Array).each do |vv|\n                          json.object do\n                            json.field \"filename\", vv.filename\n                            json.field \"body\", \"[binary file]\"\n                          end\n                        end\n                      end\n                    else\n                      json.object do\n                        json.field \"filename\", v.filename\n                        json.field \"body\", \"[binary file]\"\n                      end\n                    end\n                  end\n                end\n              end\n            end\n          end\n        end\n\n        context.response.print body\n      else\n        context.response.status_code = 400\n        context.response.print \"invalid form data! >:E\"\n      end\n\n      context\n    end\n\n    post \"/sleep\" do |context|\n      sleep 2\n\n      context.response.status_code = 200\n      context.response.print \"hello\"\n      context\n    end\n\n    # PUT\n    put \"/\" do |context|\n      context.response.status_code = 200\n      context.response.content_type = \"text/html\"\n      context.response.print \"<!doctype html><body>Mock Server is running.</body></html>\"\n      context\n    end\n\n    # DELETE\n    delete \"/\" do |context|\n      context.response.status_code = 200\n      context.response.content_type = \"text/html\"\n      context.response.print \"<!doctype html><body>Mock Server is running.</body></html>\"\n      context\n    end\n\n    # HEAD\n    head \"/\" do |context|\n      context.response.status_code = 200\n      context.response.content_type = \"text/html\"\n      context.response.print \"<!doctype html><body>Mock Server is running.</body></html>\"\n      context\n    end\n\n    # PATCH\n    patch \"/\" do |context|\n      context.response.status_code = 200\n      context.response.content_type = \"text/html\"\n      context.response.print \"<!doctype html><body>Mock Server is running.</body></html>\"\n      context\n    end\n\n    # OPTIONS\n    options \"/\" do |context|\n      context.response.status_code = 200\n      context.response.content_type = \"text/html\"\n      context.response.print \"<!doctype html><body>Mock Server is running.</body></html>\"\n      context\n    end\n\n    private def self.parse_body(body : (IO | String)?) : String\n      case body\n      when IO\n        body.gets_to_end\n      when String\n        body\n      else\n        \"\"\n      end\n    end\n\n    private def self.multipart?(headers : HTTP::Headers)\n      if content_type = headers[\"content_type\"]?\n        return content_type.includes?(\"multipart/form-data\") ? true : false\n      end\n\n      false\n    end\n\n    private def self.parse_form(body : (IO | String)?) : HTTP::Params\n      HTTP::Params.parse(parse_body(body))\n    end\n\n    private def self.parse_upload_form(request : HTTP::Request) : UploadParams\n      params = HTTP::Params.parse(\"\")\n      files = {} of String => HTTP::FormData::Part | Array(HTTP::FormData::Part)\n\n      HTTP::FormData.parse(request) do |part|\n        next unless part\n\n        name = part.name\n        if part.filename\n          if files.has_key?(name) && files[name].is_a?(HTTP::FormData::Part)\n            file = files.delete(name).as(HTTP::FormData::Part)\n            files[name] = [file, part]\n          else\n            files[name] = part\n          end\n        else\n          params.add name, part.body.gets_to_end\n        end\n      end\n\n      UploadParams.new(params, files)\n    end\n\n    record UploadParams, params : HTTP::Params, files : Hash(String, HTTP::FormData::Part | Array(HTTP::FormData::Part))\n  end\nend\n"
  },
  {
    "path": "spec/support/mock_server.cr",
    "content": "require \"http/server\"\nrequire \"./mock_server/route_handler\"\n\nclass MockServer < HTTP::Server\n  HANDLERS = MockServer::RouteHandler.new\n\n  BIND_ADDRESS = \"127.0.0.1\"\n  BIND_PORT    = 18624\n\n  getter running : Bool\n\n  @@instance = MockServer.new\n\n  def self.instance\n    @@instance\n  end\n\n  def initialize\n    super(HANDLERS)\n    @running = false\n  end\n\n  def listen\n    @running = true\n    bind_tcp(BIND_ADDRESS, BIND_PORT)\n    super\n  end\n\n  def running?\n    @running == true\n  end\n\n  def endpoint\n    \"#{scheme}://#{BIND_ADDRESS}:#{BIND_PORT}\"\n  end\n\n  def host\n    BIND_ADDRESS\n  end\n\n  def scheme\n    \"http\"\n  end\n\n  def api(path : String)\n    URI.parse(endpoint).resolve(path).to_s\n  end\nend\n"
  },
  {
    "path": "src/halite/chainable.cr",
    "content": "require \"base64\"\n\nmodule Halite\n  module Chainable\n    {% for verb in %w(get head) %}\n      # {{ verb.id.capitalize }} a resource\n      #\n      # ```\n      # Halite.{{ verb.id }}(\"http://httpbin.org/anything\", params: {\n      #   first_name: \"foo\",\n      #   last_name:  \"bar\"\n      # })\n      # ```\n      def {{ verb.id }}(uri : String, *,\n                        headers : (Hash(String, _) | NamedTuple)? = nil,\n                        params : (Hash(String, _) | NamedTuple)? = nil,\n                        raw : String? = nil,\n                        tls : OpenSSL::SSL::Context::Client? = nil) : Halite::Response\n        request({{ verb }}, uri, headers: headers, params: params, raw: raw, tls: tls)\n      end\n\n      # {{ verb.id.capitalize }} a streaming resource\n      #\n      # ```\n      # Halite.{{ verb.id }}(\"http://httpbin.org/anything\") do |response|\n      #   puts response.status_code\n      #   while line = response.body_io.gets\n      #     puts line\n      #   end\n      # end\n      # ```\n      def {{ verb.id }}(uri : String, *,\n                        headers : (Hash(String, _) | NamedTuple)? = nil,\n                        params : (Hash(String, _) | NamedTuple)? = nil,\n                        raw : String? = nil,\n                        tls : OpenSSL::SSL::Context::Client? = nil,\n                        &block : Halite::Response ->)\n        request({{ verb }}, uri, headers: headers, params: params, raw: raw, tls: tls, &block)\n      end\n    {% end %}\n\n    {% for verb in %w(put post patch delete options) %}\n      # {{ verb.id.capitalize }} a resource\n      #\n      # ### Request with form data\n      #\n      # ```\n      # Halite.{{ verb.id }}(\"http://httpbin.org/anything\", form: {\n      #   first_name: \"foo\",\n      #   last_name:  \"bar\"\n      # })\n      # ```\n      #\n      # ### Request with json data\n      #\n      # ```\n      # Halite.{{ verb.id }}(\"http://httpbin.org/anything\", json: {\n      #   first_name: \"foo\",\n      #   last_name:  \"bar\"\n      # })\n      # ```\n      #\n      # ### Request with raw string\n      #\n      # ```\n      # Halite.{{ verb.id }}(\"http://httpbin.org/anything\", raw: \"name=Peter+Lee&address=%23123+Happy+Ave&Language=C%2B%2B\")\n      # ```\n      def {{ verb.id }}(uri : String, *,\n                        headers : (Hash(String, _) | NamedTuple)? = nil,\n                        params : (Hash(String, _) | NamedTuple)? = nil,\n                        form : (Hash(String, _) | NamedTuple)? = nil,\n                        json : (Hash(String, _) | NamedTuple)? = nil,\n                        raw : String? = nil,\n                        tls : OpenSSL::SSL::Context::Client? = nil) : Halite::Response\n        request({{ verb }}, uri, headers: headers, params: params, form: form, json: json, raw: raw, tls: tls)\n      end\n\n      # {{ verb.id.capitalize }} a streaming resource\n      #\n      # ```\n      # Halite.{{ verb.id }}(\"http://httpbin.org/anything\") do |response|\n      #   puts response.status_code\n      #   while line = response.body_io.gets\n      #     puts line\n      #   end\n      # end\n      # ```\n      def {{ verb.id }}(uri : String, *,\n                        headers : (Hash(String, _) | NamedTuple)? = nil,\n                        params : (Hash(String, _) | NamedTuple)? = nil,\n                        form : (Hash(String, _) | NamedTuple)? = nil,\n                        json : (Hash(String, _) | NamedTuple)? = nil,\n                        raw : String? = nil,\n                        tls : OpenSSL::SSL::Context::Client? = nil,\n                        &block : Halite::Response ->)\n        request({{ verb }}, uri, headers: headers, params: params, form: form, json: json, raw: raw, tls: tls, &block)\n      end\n    {% end %}\n\n    # Adds a endpoint to the request.\n    #\n    #\n    # ```\n    # Halite.endpoint(\"https://httpbin.org\")\n    #   .get(\"/get\")\n    # ```\n    def endpoint(endpoint : String | URI) : Halite::Client\n      branch(default_options.with_endpoint(endpoint))\n    end\n\n    # Make a request with the given Basic authorization header\n    #\n    # ```\n    # Halite.basic_auth(\"icyleaf\", \"p@ssw0rd\")\n    #   .get(\"http://httpbin.org/get\")\n    # ```\n    #\n    # See Also: [http://tools.ietf.org/html/rfc2617](http://tools.ietf.org/html/rfc2617)\n    def basic_auth(user : String, pass : String) : Halite::Client\n      auth(\"Basic \" + Base64.strict_encode(user + \":\" + pass))\n    end\n\n    # Make a request with the given Authorization header\n    #\n    # ```\n    # Halite.auth(\"private-token\", \"6abaef100b77808ceb7fe26a3bcff1d0\")\n    #   .get(\"http://httpbin.org/get\")\n    # ```\n    def auth(value : String) : Halite::Client\n      headers({\"Authorization\" => value})\n    end\n\n    # Accept the given MIME type\n    #\n    # ```\n    # Halite.accept(\"application/json\")\n    #   .get(\"http://httpbin.org/get\")\n    # ```\n    def accept(value : String) : Halite::Client\n      headers({\"Accept\" => value})\n    end\n\n    # Set requests user agent\n    #\n    # ```\n    # Halite.user_agent(\"Custom User Agent\")\n    #   .get(\"http://httpbin.org/get\")\n    # ```\n    def user_agent(value : String) : Halite::Client\n      headers({\"User-Agent\" => value})\n    end\n\n    # Make a request with the given headers\n    #\n    # ```\n    # Halite.headers({\"Content-Type\", \"application/json\", \"Connection\": \"keep-alive\"})\n    #       .get(\"http://httpbin.org/get\")\n    # # Or\n    # Halite.headers({content_type: \"application/json\", connection: \"keep-alive\"})\n    #       .get(\"http://httpbin.org/get\")\n    # ```\n    def headers(headers : Hash(String, _) | NamedTuple) : Halite::Client\n      branch(default_options.with_headers(headers))\n    end\n\n    # Make a request with the given headers\n    #\n    # ```\n    # Halite.headers(content_type: \"application/json\", connection: \"keep-alive\")\n    #   .get(\"http://httpbin.org/get\")\n    # ```\n    def headers(**kargs) : Halite::Client\n      branch(default_options.with_headers(kargs))\n    end\n\n    # Make a request with the given cookies\n    #\n    # ```\n    # Halite.cookies({\"private-token\", \"6abaef100b77808ceb7fe26a3bcff1d0\"})\n    #       .get(\"http://httpbin.org/get\")\n    # # Or\n    # Halite.cookies({private-token: \"6abaef100b77808ceb7fe26a3bcff1d0\"})\n    #       .get(\"http://httpbin.org/get\")\n    # ```\n    def cookies(cookies : Hash(String, _) | NamedTuple) : Halite::Client\n      branch(default_options.with_cookies(cookies))\n    end\n\n    # Make a request with the given cookies\n    #\n    # ```\n    # Halite.cookies(name: \"icyleaf\", \"gender\": \"male\")\n    #   .get(\"http://httpbin.org/get\")\n    # ```\n    def cookies(**kargs) : Halite::Client\n      branch(default_options.with_cookies(kargs))\n    end\n\n    # Make a request with the given cookies\n    #\n    # ```\n    # cookies = HTTP::Cookies.from_client_headers(headers)\n    # Halite.cookies(cookies)\n    #   .get(\"http://httpbin.org/get\")\n    # ```\n    def cookies(cookies : HTTP::Cookies) : Halite::Client\n      branch(default_options.with_cookies(cookies))\n    end\n\n    # Adds a timeout to the request.\n    #\n    # How long to wait for the server to send data before giving up, as a int, float or time span.\n    # The timeout value will be applied to both the connect and the read timeouts.\n    #\n    # Set `nil` to timeout to ignore timeout.\n    #\n    # ```\n    # Halite.timeout(5.5).get(\"http://httpbin.org/get\")\n    # # Or\n    # Halite.timeout(2.minutes)\n    #   .post(\"http://httpbin.org/post\", form: {file: \"file.txt\"})\n    # ```\n    def timeout(timeout : (Int32 | Float64 | Time::Span)?)\n      timeout ? timeout(timeout, timeout, timeout) : branch\n    end\n\n    # Adds a timeout to the request.\n    #\n    # How long to wait for the server to send data before giving up, as a int, float or time span.\n    # The timeout value will be applied to both the connect and the read timeouts.\n    #\n    # ```\n    # Halite.timeout(3, 3.minutes, 5)\n    #   .post(\"http://httpbin.org/post\", form: {file: \"file.txt\"})\n    # # Or\n    # Halite.timeout(3.04, 64, 10.0)\n    #   .get(\"http://httpbin.org/get\")\n    # ```\n    def timeout(connect : (Int32 | Float64 | Time::Span)? = nil,\n                read : (Int32 | Float64 | Time::Span)? = nil,\n                write : (Int32 | Float64 | Time::Span)? = nil)\n      branch(default_options.with_timeout(connect, read, write))\n    end\n\n    # Returns `Options` self with automatically following redirects.\n    #\n    # ```\n    # # Automatically following redirects.\n    # Halite.follow\n    #   .get(\"http://httpbin.org/relative-redirect/5\")\n    #\n    # # Always redirect with any request methods\n    # Halite.follow(strict: false)\n    #   .get(\"http://httpbin.org/get\")\n    # ```\n    def follow(strict = Halite::Options::Follow::STRICT) : Halite::Client\n      branch(default_options.with_follow(strict: strict))\n    end\n\n    # Returns `Options` self with given max hops of redirect times.\n    #\n    # ```\n    # # Max hops 3 times\n    # Halite.follow(3)\n    #   .get(\"http://httpbin.org/relative-redirect/3\")\n    #\n    # # Always redirect with any request methods\n    # Halite.follow(4, strict: false)\n    #   .get(\"http://httpbin.org/relative-redirect/4\")\n    # ```\n    def follow(hops : Int32, strict = Halite::Options::Follow::STRICT) : Halite::Client\n      branch(default_options.with_follow(hops, strict))\n    end\n\n    # Returns `Options` self with enable or disable logging.\n    #\n    # #### Enable logging\n    #\n    # Same as call `logging` method without any argument.\n    #\n    # ```\n    # Halite.logging.get(\"http://httpbin.org/get\")\n    # ```\n    #\n    # #### Disable logging\n    #\n    # ```\n    # Halite.logging(false).get(\"http://httpbin.org/get\")\n    # ```\n    def logging(enable : Bool = true)\n      options = default_options\n      options.logging = enable\n      branch(options)\n    end\n\n    # Returns `Options` self with given the logging which it integration from `Halite::Logging`.\n    #\n    # #### Simple logging\n    #\n    # ```\n    # Halite.logging\n    #   .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n    #\n    # => 2018-08-28 14:33:19 +08:00 | request  | POST   | http://httpbin.org/post\n    # => 2018-08-28 14:33:21 +08:00 | response | 200    | http://httpbin.org/post | 1.61s | application/json\n    # { ... }\n    # ```\n    #\n    # #### Logger configuration\n    #\n    # By default, Halite will logging all outgoing HTTP requests and their responses(without binary stream) to `STDOUT` on DEBUG level.\n    # You can configuring the following options:\n    #\n    # - `skip_request_body`: By default is `false`.\n    # - `skip_response_body`: By default is `false`.\n    # - `skip_benchmark`: Display elapsed time, by default is `false`.\n    # - `colorize`: Enable colorize in terminal, only apply in `common` format, by default is `true`.\n    #\n    # ```\n    # Halite.logging(skip_request_body: true, skip_response_body: true)\n    #   .post(\"http://httpbin.org/get\", form: {image: File.open(\"halite-logo.png\")})\n    #\n    # # => 2018-08-28 14:33:19 +08:00 | request  | POST   | http://httpbin.org/post\n    # # => 2018-08-28 14:33:21 +08:00 | response | 200    | http://httpbin.org/post | 1.61s | application/json\n    # ```\n    #\n    # #### Use custom logging\n    #\n    # Creating the custom logging by integration `Halite::Logging::Abstract` abstract class.\n    # Here has two methods must be implement: `#request` and `#response`.\n    #\n    # ```\n    # class CustomLogger < Halite::Logging::Abstract\n    #   def request(request)\n    #     @logger.info \"| >> | %s | %s %s\" % [request.verb, request.uri, request.body]\n    #   end\n    #\n    #   def response(response)\n    #     @logger.info \"| << | %s | %s %s\" % [response.status_code, response.uri, response.content_type]\n    #   end\n    # end\n    #\n    # # Add to adapter list (optional)\n    # Halite::Logging.register_adapter \"custom\", CustomLogger.new\n    #\n    # Halite.logging(logging: CustomLogger.new)\n    #   .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n    #\n    # # We can also call it use format name if you added it.\n    # Halite.logging(format: \"custom\")\n    #   .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n    #\n    # # => 2017-12-13 16:40:13 +08:00 | >> | GET | http://httpbin.org/get?name=foobar\n    # # => 2017-12-13 16:40:15 +08:00 | << | 200 | http://httpbin.org/get?name=foobar application/json\n    # ```\n    def logging(logging : Halite::Logging::Abstract = Halite::Logging::Common.new)\n      branch(default_options.with_logging(logging))\n    end\n\n    # Returns `Options` self with given the file with the path.\n    #\n    # #### JSON-formatted logging\n    #\n    # ```\n    # Halite.logging(format: \"json\")\n    #   .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n    # ```\n    #\n    # #### create a http request and log to file\n    #\n    # ```\n    # Log.setup(\"halite.file\", backend: Log::IOBackend.new(File.open(\"/tmp/halite.log\", \"a\")))\n    # Halite.logging(for: \"halite.file\")\n    #   .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n    # ```\n    #\n    # #### Always create new log file and store data to JSON formatted\n    #\n    # ```\n    # Log.setup(\"halite.file\", backend: Log::IOBackend.new(File.open(\"/tmp/halite.log\", \"w\"))\n    # Halite.logging(for: \"halite.file\", format: \"json\")\n    #   .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n    # ```\n    #\n    # Check the log file content: **/tmp/halite.log**\n    def logging(format : String = \"common\", *, for : String = \"halite\",\n                skip_request_body = false, skip_response_body = false,\n                skip_benchmark = false, colorize = true)\n      opts = {\n        for:                for,\n        skip_request_body:  skip_request_body,\n        skip_response_body: skip_response_body,\n        skip_benchmark:     skip_benchmark,\n        colorize:           colorize,\n      }\n      branch(default_options.with_logging(format, **opts))\n    end\n\n    # Turn on given features and its options.\n    #\n    # Available features to review all subclasses of `Halite::Feature`.\n    #\n    # #### Use JSON logging\n    #\n    # ```\n    # Halite.use(\"logging\", format: \"json\")\n    #   .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n    #\n    # # => { ... }\n    # ```\n    #\n    # #### Use common format logging and skip response body\n    # ```\n    # Halite.use(\"logging\", format: \"common\", skip_response_body: true)\n    #   .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n    #\n    # # => 2018-08-28 14:58:26 +08:00 | request  | GET    | http://httpbin.org/get\n    # # => 2018-08-28 14:58:27 +08:00 | response | 200    | http://httpbin.org/get | 615.8ms | application/json\n    # ```\n    def use(feature : String, **opts)\n      branch(default_options.with_features(feature, **opts))\n    end\n\n    # Turn on given the name of features.\n    #\n    # Available features to review all subclasses of `Halite::Feature`.\n    #\n    # ```\n    # Halite.use(\"logging\", \"your-custom-feature-name\")\n    #   .get(\"http://httpbin.org/get\", params: {name: \"foobar\"})\n    # ```\n    def use(*features)\n      branch(default_options.with_features(*features))\n    end\n\n    # Make an HTTP request with the given verb\n    #\n    # ```\n    # Halite.request(\"get\", \"http://httpbin.org/get\", {\n    #   \"headers\" = { \"user_agent\" => \"halite\" },\n    #   \"params\" => { \"nickname\" => \"foo\" },\n    #   \"form\" => { \"username\" => \"bar\" },\n    # })\n    # ```\n    def request(verb : String, uri : String, *,\n                headers : (Hash(String, _) | NamedTuple)? = nil,\n                params : (Hash(String, _) | NamedTuple)? = nil,\n                form : (Hash(String, _) | NamedTuple)? = nil,\n                json : (Hash(String, _) | NamedTuple)? = nil,\n                raw : String? = nil,\n                tls : OpenSSL::SSL::Context::Client? = nil) : Halite::Response\n      request(verb, uri, options_with(headers, params, form, json, raw, tls))\n    end\n\n    # Make an HTTP request with the given verb and options\n    #\n    # > This method will be executed with oneshot request.\n    #\n    # ```\n    # Halite.request(\"get\", \"http://httpbin.org/stream/3\", headers: {\"user-agent\" => \"halite\"}) do |response|\n    #   puts response.status_code\n    #   while line = response.body_io.gets\n    #     puts line\n    #   end\n    # end\n    # ```\n    def request(verb : String, uri : String, *,\n                headers : (Hash(String, _) | NamedTuple)? = nil,\n                params : (Hash(String, _) | NamedTuple)? = nil,\n                form : (Hash(String, _) | NamedTuple)? = nil,\n                json : (Hash(String, _) | NamedTuple)? = nil,\n                raw : String? = nil,\n                tls : OpenSSL::SSL::Context::Client? = nil,\n                &block : Halite::Response ->)\n      request(verb, uri, options_with(headers, params, form, json, raw, tls), &block)\n    end\n\n    # Make an HTTP request with the given verb and options\n    #\n    # > This method will be executed with oneshot request.\n    #\n    # ```\n    # Halite.request(\"get\", \"http://httpbin.org/get\", Halite::Options.new(\n    #   \"headers\" = { \"user_agent\" => \"halite\" },\n    #   \"params\" => { \"nickname\" => \"foo\" },\n    #   \"form\" => { \"username\" => \"bar\" },\n    # )\n    # ```\n    def request(verb : String, uri : String, options : Halite::Options? = nil) : Halite::Response\n      branch(options).request(verb, uri)\n    end\n\n    # Make an HTTP request with the given verb and options\n    #\n    # > This method will be executed with oneshot request.\n    #\n    # ```\n    # Halite.request(\"get\", \"http://httpbin.org/stream/3\") do |response|\n    #   puts response.status_code\n    #   while line = response.body_io.gets\n    #     puts line\n    #   end\n    # end\n    # ```\n    def request(verb : String, uri : String, options : Halite::Options? = nil, &block : Halite::Response ->)\n      branch(options).request(verb, uri, &block)\n    end\n\n    private def branch(options : Halite::Options? = nil) : Halite::Client\n      options ||= default_options\n      Halite::Client.new(options)\n    end\n\n    private DEFAULT_OPTIONS = Halite::Options.new\n\n    private def default_options\n      {% if @type.superclass %}\n        @default_options\n      {% else %}\n        DEFAULT_OPTIONS.clear!\n      {% end %}\n    end\n\n    private def options_with(headers : (Hash(String, _) | NamedTuple)? = nil,\n                             params : (Hash(String, _) | NamedTuple)? = nil,\n                             form : (Hash(String, _) | NamedTuple)? = nil,\n                             json : (Hash(String, _) | NamedTuple)? = nil,\n                             raw : String? = nil,\n                             tls : OpenSSL::SSL::Context::Client? = nil)\n      options = Halite::Options.new(headers: headers, params: params, form: form, json: json, raw: raw, tls: tls)\n      default_options.merge!(options)\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/client.cr",
    "content": "require \"./request\"\nrequire \"./response\"\nrequire \"./redirector\"\n\nrequire \"http/client\"\nrequire \"json\"\n\nmodule Halite\n  # Clients make requests and receive responses\n  #\n  # Support all `Chainable` methods.\n  #\n  # ### Simple setup\n  #\n  # ```\n  # client = Halite::Client.new(headers: {\n  #   \"private-token\" => \"bdf39d82661358f80b31b67e6f89fee4\"\n  # })\n  #\n  # client.auth(private_token: \"bdf39d82661358f80b31b67e6f89fee4\").\n  #       .get(\"http://httpbin.org/get\", params: {\n  #         name: \"icyleaf\"\n  #       })\n  # ```\n  #\n  # ### Setup with block\n  #\n  # ```\n  # client = Halite::Client.new do\n  #   basic_auth \"name\", \"foo\"\n  #   headers content_type: \"application/jsong\"\n  #   read_timeout 3.minutes\n  #   logging true\n  # end\n  # ```\n  class Client\n    include Chainable\n\n    # Instance a new client\n    #\n    # ```\n    # Halite::Client.new(headers: {\"private-token\" => \"bdf39d82661358f80b31b67e6f89fee4\"})\n    # ```\n    def self.new(*,\n                 endpoint : (String | URI)? = nil,\n                 headers : (Hash(String, _) | NamedTuple)? = nil,\n                 cookies : (Hash(String, _) | NamedTuple)? = nil,\n                 params : (Hash(String, _) | NamedTuple)? = nil,\n                 form : (Hash(String, _) | NamedTuple)? = nil,\n                 json : (Hash(String, _) | NamedTuple)? = nil,\n                 raw : String? = nil,\n                 timeout = Timeout.new,\n                 follow = Follow.new,\n                 tls : OpenSSL::SSL::Context::Client? = nil)\n      new(Options.new(\n        endpoint: endpoint,\n        headers: headers,\n        cookies: cookies,\n        params: params,\n        form: form,\n        json: json,\n        raw: raw,\n        tls: tls,\n        timeout: timeout,\n        follow: follow\n      ))\n    end\n\n    property options\n\n    # Instance a new client with block\n    #\n    # ```\n    # client = Halite::Client.new do\n    #   basic_auth \"name\", \"foo\"\n    #   logging true\n    # end\n    # ```\n    def self.new(&block)\n      instance = new\n      yield_instance = with instance yield\n      if yield_instance\n        # yield_instance.options.merge!(yield_instance.oneshot_options)\n        # yield_instance.oneshot_options.clear!\n        instance = yield_instance\n      end\n\n      instance\n    end\n\n    # Instance a new client\n    #\n    # ```\n    # options = Halite::Options.new(headers: {\n    #   \"private-token\" => \"bdf39d82661358f80b31b67e6f89fee4\",\n    # })\n    #\n    # client = Halite::Client.new(options)\n    # ```\n    def initialize(@options = Halite::Options.new)\n      @history = [] of Response\n      @default_options = Halite::Options.new(@options)\n    end\n\n    # Make an HTTP request\n    def request(verb : String, uri : String, options : Halite::Options? = nil) : Halite::Response\n      opts = options ? @default_options.merge(options.not_nil!) : @default_options\n      request = build_request(verb, uri, opts)\n      response = perform_chain(request, opts) do\n        perform(request, opts)\n      end\n\n      return response if opts.follow.hops.zero?\n\n      Redirector.new(request, response, opts).perform do |req|\n        perform(req, opts)\n      end\n    end\n\n    # Make an HTTP request\n    def request(verb : String, uri : String, options : Halite::Options? = nil, &block : Halite::Response ->)\n      opts = options ? @default_options.merge(options.not_nil!) : @default_options\n      request = build_request(verb, uri, opts)\n      perform(request, opts, &block)\n    end\n\n    # Find interceptor and return `Response` else perform HTTP request.\n    private def perform_chain(request : Halite::Request, options : Halite::Options, &block : -> Response)\n      chain = Feature::Chain.new(request, nil, options, &block)\n      options.features.each do |_, feature|\n        current_chain = feature.intercept(chain)\n        if current_chain.result == Feature::Chain::Result::Next\n          chain = current_chain\n        elsif current_chain.result == Feature::Chain::Result::Return && (response = current_chain.response)\n          return handle_response(response, options)\n        end\n      end\n\n      # Make sure return if has response with each interceptor\n      if response = chain.response\n        return handle_response(response, options)\n      end\n\n      # Perform original HTTP request if not found any response in interceptors\n      block.call\n    end\n\n    # Perform a single (no follow) HTTP request\n    private def perform(request : Halite::Request, options : Halite::Options) : Halite::Response\n      raise RequestError.new(\"SSL context given for HTTP URI = #{request.uri}\") if request.scheme == \"http\" && options.tls\n\n      conn = make_connection(request, options)\n      conn_response = conn.exec(request.verb, request.full_path, request.headers, request.body)\n      handle_response(request, conn_response, options)\n    rescue ex : IO::TimeoutError\n      raise TimeoutError.new(ex.message)\n    rescue ex : Socket::Error\n      raise ConnectionError.new(ex.message)\n    end\n\n    # Perform a single (no follow) streaming HTTP request and redirect automatically\n    private def perform(request : Halite::Request, options : Halite::Options, &block : Halite::Response ->)\n      raise RequestError.new(\"SSL context given for HTTP URI = #{request.uri}\") if request.scheme == \"http\" && options.tls\n\n      conn = make_connection(request, options)\n      conn.exec(request.verb, request.full_path, request.headers, request.body) do |conn_response|\n        response = handle_response(request, conn_response, options)\n        redirector = Redirector.new(request, response, options)\n        if redirector.avaiable?\n          redirector.each_redirect do |req|\n            perform(req, options, &block)\n          end\n        else\n          block.call(response)\n        end\n\n        return response\n      end\n    end\n\n    # Prepare a HTTP request\n    private def build_request(verb : String, uri : String, options : Halite::Options) : Halite::Request\n      uri = make_request_uri(uri, options)\n      body_data = make_request_body(options)\n      headers = make_request_headers(options, body_data.content_type)\n      request = Request.new(verb, uri, headers, body_data.body)\n\n      # reset options during onshot request, see `default_options` method at the bottom of file.\n      # default_options.clear!\n\n      options.features.reduce(request) do |req, (_, feature)|\n        feature.request(req)\n      end\n    end\n\n    # Merges query params if needed\n    private def make_request_uri(url : String, options : Halite::Options) : URI\n      uri = resolve_uri(url, options)\n      if params = options.params\n        query = HTTP::Params.encode(params)\n        uri.query = [uri.query, query].compact.join('&') unless query.empty?\n      end\n\n      uri\n    end\n\n    # Merges request headers\n    private def make_request_headers(options : Halite::Options, content_type : String?) : HTTP::Headers\n      headers = options.headers\n      if (value = content_type) && !value.empty? && !headers.has_key?(\"Content-Type\")\n        headers.add(\"Content-Type\", value)\n      end\n\n      # Cookie shards\n      options.cookies.add_request_headers(headers)\n    end\n\n    # Create the request body object to send\n    private def make_request_body(options : Halite::Options) : Halite::Request::Data\n      if (form = options.form) && !form.empty?\n        FormData.create(form)\n      elsif (hash = options.json) && !hash.empty?\n        body = JSON.build do |builder|\n          hash.to_json(builder)\n        end\n\n        Halite::Request::Data.new(body, \"application/json\")\n      elsif (raw = options.raw) && !raw.empty?\n        Halite::Request::Data.new(raw, \"text/plain\")\n      else\n        Halite::Request::Data.new(\"\")\n      end\n    end\n\n    # Create the http connection\n    private def make_connection(request, options)\n      conn = HTTP::Client.new(request.domain, options.tls)\n      conn.connect_timeout = options.timeout.connect.not_nil! if options.timeout.connect\n      conn.read_timeout = options.timeout.read.not_nil! if options.timeout.read\n      conn.write_timeout = options.timeout.write.not_nil! if options.timeout.write\n      conn\n    end\n\n    # Convert HTTP::Client::Response to response and handles response (see below)\n    private def handle_response(request, conn_response : HTTP::Client::Response, options) : Halite::Response\n      response = Response.new(uri: request.uri, conn: conn_response, history: @history)\n      handle_response(response, options)\n    end\n\n    # Handles response by reduce the response of feature, add history and update options\n    private def handle_response(response, options) : Halite::Response\n      response = options.features.reduce(response) do |res, (_, feature)|\n        feature.response(res)\n      end\n\n      # Append history of response if enable follow\n      @history << response unless options.follow.hops.zero?\n      store_cookies_from_response(response)\n    end\n\n    # Store cookies for sessions use from response\n    private def store_cookies_from_response(response : Halite::Response) : Halite::Response\n      return response unless response.headers\n\n      @default_options.with_cookies(HTTP::Cookies.from_server_headers(response.headers))\n      response\n    end\n\n    # # Use in instance/session mode, it will replace same method in `Halite::Chainable`.\n    # private def branch(options : Halite::Options? = nil) : Halite::Client\n    #   oneshot_options.merge!(options)\n    #   self\n    # end\n\n    private def resolve_uri(url : String, options : Halite::Options) : URI\n      return URI.parse(url) unless endpoint = options.endpoint\n      return endpoint if url.empty?\n\n      endpoint.path += '/' unless endpoint.path.ends_with?('/')\n      endpoint.resolve(url)\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/error.cr",
    "content": "module Halite\n  module Exception\n    # Generic error\n    class Error < ::Exception; end\n\n    # Generic Connection error\n    class ConnectionError < Error; end\n\n    # Generic Request error\n    class RequestError < Error; end\n\n    # Generic Response error\n    class ResponseError < Error; end\n\n    # Generic Feature error\n    class FeatureError < Error; end\n\n    # The method given was not understood\n    class UnsupportedMethodError < RequestError; end\n\n    # The scheme given was not understood\n    class UnsupportedSchemeError < RequestError; end\n\n    # The head method can not streaming without empty response\n    class UnsupportedStreamMethodError < RequestError; end\n\n    # Requested to do something when we're in the wrong state\n    class StateError < RequestError; end\n\n    # Generic Timeout error\n    class TimeoutError < RequestError; end\n\n    # The feature given was not understood\n    class UnRegisterFeatureError < FeatureError; end\n\n    # The format given was not understood\n    class UnRegisterLoggerFormatError < FeatureError; end\n\n    # Notifies that we reached max allowed redirect hops\n    class TooManyRedirectsError < ResponseError; end\n\n    # Notifies that following redirects got into an endless loop\n    class EndlessRedirectError < TooManyRedirectsError; end\n\n    # The MIME type(adapter) given was not understood\n    class UnRegisterMimeTypeError < ResponseError; end\n\n    # Generic API error\n    class APIError < ResponseError\n      getter uri\n      getter status_code\n      getter status_message : String? = nil\n\n      def initialize(@message : String? = nil, @status_code : Int32? = nil, @uri : URI? = nil)\n        @status_message = build_status_message\n        if status_code = @status_code\n          @message ||= \"#{status_code} #{@status_message}\"\n        end\n\n        super(@message)\n      end\n\n      private def build_status_message : String\n        String::Builder.build do |io|\n          if status_code = @status_code\n            io << \"#{HTTP::Status.new(status_code).description.to_s.downcase} error\"\n          else\n            io << \"#{@message || \"unknown\"} error\"\n          end\n\n          io << \" with url: #{@uri}\" if uri = @uri\n        end.to_s\n      end\n    end\n\n    # 4XX client error\n    class ClientError < APIError; end\n\n    # 5XX server error\n    class ServerError < APIError; end\n  end\n\n  {% for cls in Exception.constants %}\n    # :nodoc:\n    alias {{ cls.id }} = Exception::{{ cls.id }}\n  {% end %}\nend\n"
  },
  {
    "path": "src/halite/ext/file_to_json.cr",
    "content": "# :nodoc:\nclass File\n  def to_json(json : JSON::Builder)\n    json.string(to_s)\n  end\nend\n"
  },
  {
    "path": "src/halite/ext/http_headers_encode.cr",
    "content": "module HTTP\n  # This is **extension** apply in Halite.\n  struct Headers\n    # Returns the given key value pairs as HTTP Headers\n    #\n    # Every parameter added is directly written to an IO, where keys are properly escaped.\n    #\n    # ```\n    # HTTP::Headers.encode({\n    #   content_type: \"application/json\",\n    # })\n    # # => \"HTTP::Headers{\"Content-Type\" => \"application/json\"}\"\n    #\n    # HTTP::Headers.encode({\n    #   \"conTENT-type\": \"application/json\",\n    # })\n    # # => \"HTTP::Headers{\"Content-Type\" => \"application/json\"}\"\n    # ```\n    def self.encode(data : Hash(String, _) | NamedTuple) : HTTP::Headers\n      ::HTTP::Headers.new.tap do |builder|\n        data = data.is_a?(NamedTuple) ? data.to_h : data\n        data.each do |key, value|\n          key = key.to_s.gsub(\"_\", \"-\").split(\"-\").map { |v| v.capitalize }.join(\"-\")\n          # skip invalid value of content length\n          next if key == \"Content-Length\" && !(value =~ /^\\d+$/)\n\n          builder.add key, value.is_a?(Array(String)) ? value : value.to_s\n        end\n      end\n    end\n\n    # Same as `#encode`\n    def self.encode(**data)\n      encode(data)\n    end\n\n    # Similar as `Hahs#to_h` but return `String` if it has one value of the key.\n    #\n    # ```\n    # headers = HTTP::Headers{\"Accepts\" => [\"application/json\", \"text/html\"], \"Content-Type\" => [\"text/html\"]}\n    # headers[\"Accepts\"]      # => [\"application/json\", \"text/html\"]\n    # headers[\"Content-Type\"] # => \"text/html\"\n    # ```\n    def to_flat_h\n      @hash.each_with_object({} of String => String | Array(String)) do |(key, values), obj|\n        obj[key.name] = case values\n                        when String\n                          values.as(String)\n                        when Array\n                          values.size == 1 ? values[0].as(String) : values.as(Array(String))\n                        else\n                          raise Halite::Error.new(\"Not support type `#{values.class} with value: #{values}\")\n                        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/ext/http_params_encode.cr",
    "content": "module HTTP\n  # This is **extension**  apply in Halite.\n  struct Params\n    # Returns the given key value pairs as a url-encoded query.\n    #\n    # Every parameter added is directly written to an IO, where keys and values are properly escaped.\n    #\n    # ```\n    # HTTP::Params.encode({\n    #   \"name\"    => \"Lizeth Gusikowski\",\n    #   \"skill\"   => [\"ruby\", \"crystal\"],\n    #   \"company\" => {\n    #     \"name\" => \"Keeling Inc\",\n    #   },\n    #   \"avatar\" => File.open(\"avatar_big.png\"),\n    # })\n    # # => \"name=Lizeth+Gusikowski&skill=ruby&skill=crystal&company=%7B%22name%22+%3D%3E+%22Keeling+Inc%22%7D&avatar=avatar_big.png\"\n    # ```\n    def self.encode(hash : Hash) : String\n      ::HTTP::Params.build do |form|\n        hash.each do |key, value|\n          key = key.to_s\n          case value\n          when Array\n            value.each do |item|\n              form.add(\"#{key}\", item.to_s)\n            end\n          when File\n            form.add(key, value.as(File).path)\n          when Hash\n            value.each do |hkey, hvalue|\n              form.add(\"#{key}[#{hkey}]\", hvalue.to_s)\n            end\n          else\n            form.add(key, value.to_s)\n          end\n        end\n      end\n    end\n\n    # Returns the given key value pairs as a url-encoded query.\n    #\n    # Every parameter added is directly written to an IO, where keys and values are properly escaped.\n    #\n    # ```\n    # HTTP::Params.encode({\n    #   name: \"Lizeth Gusikowski\",\n    #   skill: [\"ruby\", \"crystal\"],\n    #   company: {\n    #     name: \"Keeling Inc\",\n    #   },\n    #   avatar: File.open(\"avatar_big.png\"\n    # })\n    # # => \"name=Lizeth+Gusikowski&skill=ruby&skill=crystal&company=%7B%22name%22+%3D%3E+%22Keeling+Inc%22%7D&avatar=avatar_big.png\"\n    # ```\n    def self.encode(named_tuple : NamedTuple) : String\n      encode(named_tuple.to_h)\n    end\n\n    # Returns the given key value pairs as a url-encoded query.\n    #\n    # Every parameter added is directly written to an IO, where keys and values are properly escaped.\n    #\n    # ```\n    # HTTP::Params.encode(\n    #   name: \"Lizeth Gusikowski\",\n    #   skill: [\"ruby\", \"crystal\"],\n    #   company: {\n    #     name: \"Keeling Inc\",\n    #   },\n    #   avatar: File.open(\"avatar_big.png\"\n    # )\n    # # => \"name=Lizeth+Gusikowski&skill=ruby&skill=crystal&company=%7B%22name%22+%3D%3E+%22Keeling+Inc%22%7D&avatar=avatar_big.png\"\n    # ```\n    def self.encode(**named_tuple) : String\n      encode(named_tuple)\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/feature.cr",
    "content": "module Halite\n  abstract class Feature\n    def initialize(**options)\n    end\n\n    # Cooks with request\n    def request(request : Halite::Request) : Halite::Request\n      request\n    end\n\n    # Cooking with response\n    def response(response : Halite::Response) : Halite::Response\n      response\n    end\n\n    # Intercept and cooking request and response\n    def intercept(chain : Halite::Feature::Chain) : Halite::Feature::Chain\n      chain\n    end\n\n    # Feature chain\n    #\n    # Chain has two result:\n    #\n    # next: perform and run next interceptor\n    # return: perform and return\n    class Chain\n      enum Result\n        Next\n        Return\n      end\n\n      property request\n      getter response\n      getter result\n\n      @performed_response : Halite::Response?\n\n      def initialize(@request : Halite::Request, @response : Halite::Response?, @options : Halite::Options, &block : -> Halite::Response)\n        @result = Result::Next\n        @performed_response = nil\n        @perform_request_block = block\n      end\n\n      def next(response)\n        @result = Result::Next\n        @response = response\n\n        self\n      end\n\n      def return(response)\n        @result = Result::Return\n        @response = response\n\n        self\n      end\n\n      def performed?\n        !@performed_response.nil?\n      end\n\n      def perform\n        @performed_response ||= @perform_request_block.call\n        @performed_response.not_nil!\n      end\n    end\n  end\nend\n\nrequire \"./features/*\"\n"
  },
  {
    "path": "src/halite/features/cache.cr",
    "content": "require \"json\"\nrequire \"digest\"\nrequire \"file_utils\"\n\nmodule Halite\n  # Cache feature use for caching HTTP response to local storage to speed up in developing stage.\n  #\n  # It has the following options:\n  #\n  # - `file`: Load cache from file. it conflict with `path` and `expires`.\n  # - `path`: The path of cache, default is \"/tmp/halite/cache/\"\n  # - `expires`: The expires time of cache, default is never expires.\n  # - `debug`: The debug mode of cache, default is `true`\n  #\n  # With debug mode, cached response it always included some headers information:\n  #\n  # - `X-Halite-Cached-From`: Cache source (cache or file)\n  # - `X-Halite-Cached-Key`: Cache key with verb, uri and body (return with cache, not `file` passed)\n  # - `X-Halite-Cached-At`:  Cache created time\n  # - `X-Halite-Cached-Expires-At`: Cache expired time (return with cache, not `file` passed)\n  #\n  # ```\n  # Halite.use(\"cache\").get \"http://httpbin.org/anything\"     # request a HTTP\n  # r = Halite.use(\"cache\").get \"http://httpbin.org/anything\" # request from local storage\n  # r.headers                                                 # => {..., \"X-Halite-Cached-At\" => \"2018-08-30 10:41:14 UTC\", \"X-Halite-Cached-By\" => \"Halite\", \"X-Halite-Cached-Expires-At\" => \"2018-08-30 10:41:19 UTC\", \"X-Halite-Cached-Key\" => \"2bb155e6c8c47627da3d91834eb4249a\"}}\n  # ```\n  class Cache < Feature\n    DEFAULT_PATH = \"/tmp/halite/cache/\"\n\n    getter file : String?\n    getter path : String\n    getter expires : Time::Span?\n    getter debug : Bool\n\n    # return a new Cache instance\n    #\n    # Accepts argument:\n    #\n    # - **debug**: `Bool`\n    # - **path**: `String`\n    # - **expires**: `(Int32 | Time::Span)?`\n    def initialize(**options)\n      @debug = options.fetch(:debug, true).as(Bool)\n      if file = options[:file]?\n        @file = file\n        @path = DEFAULT_PATH\n        @expires = nil\n      else\n        @file = nil\n        @path = options.fetch(:path, DEFAULT_PATH).as(String)\n        @expires = case expires = options[:expires]?\n                   when Time::Span\n                     expires.as(Time::Span)\n                   when Int32\n                     Time::Span.new(seconds: expires.as(Int32), nanoseconds: 0)\n                   when Nil\n                     nil\n                   else\n                     raise \"Only accept Int32 and Time::Span type.\"\n                   end\n      end\n    end\n\n    def intercept(chain)\n      response = cache(chain) do\n        chain.perform\n      end\n\n      chain.return(response)\n    end\n\n    private def cache(chain, &block : -> Halite::Response)\n      if response = find_cache(chain.request)\n        return response\n      end\n\n      response = yield\n      write_cache(chain.request, response)\n      response\n    end\n\n    private def find_cache(request : Halite::Request) : Halite::Response?\n      if file = @file\n        build_response(request, file)\n      elsif response = build_response(request)\n        response\n      end\n    end\n\n    private def find_file(file) : Halite::Response\n      raise Error.new(\"Not find cache file: #{file}\") if File.file?(file)\n      build_response(file)\n    end\n\n    private def build_response(request : Halite::Request, file : String? = nil) : Halite::Response?\n      status_code = 200\n      headers = HTTP::Headers.new\n      cache_from = \"file\"\n\n      unless file\n        # Cache in path\n        key = generate_cache_key(request)\n        path = File.join(@path, key)\n\n        return unless Dir.exists?(path)\n\n        cache_from = \"cache\"\n        cache_file = File.join(path, \"#{key}.cache\")\n        if File.file?(cache_file) && !cache_expired?(cache_file)\n          file = cache_file\n\n          if metadata = find_metadata(path)\n            status_code = metadata[\"status_code\"].as_i\n            metadata[\"headers\"].as_h.each do |name, value|\n              headers[name] = value.as_s\n            end\n          end\n\n          if @debug\n            headers[\"X-Halite-Cached-Key\"] = key\n            headers[\"X-Halite-Cached-Expires-At\"] = @expires ? (cache_created_time(file) + @expires.not_nil!).to_s : \"None\"\n          end\n        end\n      end\n\n      return unless file\n\n      if @debug\n        headers[\"X-Halite-Cached-From\"] = cache_from\n        headers[\"X-Halite-Cached-At\"] = cache_created_time(file).to_s\n      end\n\n      body = File.read_lines(file).join(\"\\n\")\n      Response.new(request.uri, status_code, body, headers)\n    end\n\n    private def find_metadata(path)\n      file = File.join(path, \"metadata.json\")\n      if File.exists?(file)\n        JSON.parse(File.open(file)).as_h\n      end\n    end\n\n    private def cache_expired?(file)\n      return false unless expires = @expires\n      file_modified_time = cache_created_time(file)\n      Time.utc >= (file_modified_time + expires)\n    end\n\n    private def cache_created_time(file)\n      File.info(file).modification_time\n    end\n\n    private def generate_cache_key(request) : String\n      Digest::MD5.hexdigest(\"#{request.verb}-#{request.uri}-#{request.body}\")\n    end\n\n    private def write_cache(request, response)\n      key = generate_cache_key(request)\n      path = File.join(@path, key)\n      FileUtils.mkdir_p(path) unless Dir.exists?(path)\n\n      write_metadata(path, response)\n      write_body(path, key, response)\n    end\n\n    private def write_metadata(path, response)\n      File.open(File.join(path, \"metadata.json\"), \"w\") do |f|\n        f.puts({\n          \"status_code\" => response.status_code,\n          \"headers\"     => response.headers.to_flat_h,\n        }.to_json)\n      end\n    end\n\n    private def write_body(path, key, response)\n      File.open(File.join(path, \"#{key}.cache\"), \"w\") do |f|\n        f.puts response.body\n      end\n    end\n\n    Halite.register_feature \"cache\", self\n  end\nend\n"
  },
  {
    "path": "src/halite/features/logging/common.cr",
    "content": "class Halite::Logging\n  # Common logging format\n  #\n  # Instance variables to check `Halite::Logging::Abstract`\n  #\n  # ```\n  # Halite.use(\"logging\", logging: Halite::Logging::Common.new(skip_request_body: true))\n  #   .get(\"http://httpbin.org/get\")\n  #\n  # # Or\n  # Halite.logging(format: \"common\", skip_request_body: true)\n  #   .get(\"http://httpbin.org/get\")\n  #\n  # # => 2018-08-31 16:56:12 +08:00 | request  | GET    | http://httpbin.org/get\n  # # => 2018-08-31 16:56:13 +08:00 | response | 200    | http://httpbin.org/get | 1.08s | application/json\n  # ```\n  class Common < Abstract\n    def request(request)\n      message = String.build do |io|\n        io << \"> | request  | \" << colorful_method(request.verb)\n        io << \"| \" << request.uri\n        unless request.body.empty? || @skip_request_body\n          io << \"\\n\" << request.body\n        end\n      end\n\n      @logger.info { message }\n      @request_time = Time.utc unless @skip_benchmark\n    end\n\n    def response(response)\n      message = String.build do |io|\n        content_type = response.content_type || \"Unknown MIME\"\n        io << \"< | response | \" << colorful_status_code(response.status_code)\n        io << \"| \" << response.uri\n        if !@skip_benchmark && (request_time = @request_time)\n          elapsed = Time.utc - request_time\n          io << \" | \" << human_time(elapsed)\n        end\n\n        io << \" | \" << content_type\n        unless response.body.empty? || binary_type?(content_type) || @skip_response_body\n          io << \"\\n\" << response.body\n        end\n      end\n\n      @logger.info { message }\n    end\n\n    protected def colorful_method(method, is_request = true)\n      fore, back = case method.upcase\n                   when \"GET\"\n                     [:white, :blue]\n                   when \"POST\"\n                     [:white, :cyan]\n                   when \"PUT\"\n                     [:white, :yellow]\n                   when \"DELETE\"\n                     [:white, :red]\n                   when \"PATCH\"\n                     [:white, :green]\n                   when \"HEAD\"\n                     [:white, :magenta]\n                   else\n                     [:dark_gray, :white]\n                   end\n\n      colorful((\" %-7s\" % method), fore, back)\n    end\n\n    protected def colorful_status_code(status_code : Int32)\n      fore, back = case status_code\n                   when 300..399\n                     [:dark_gray, :white]\n                   when 400..499\n                     [:white, :yellow]\n                   when 500..999\n                     [:white, :red]\n                   else\n                     [:white, :green]\n                   end\n\n      colorful((\" %-7s\" % status_code), fore, back)\n    end\n\n    protected def colorful(message, fore, back)\n      Colorize.enabled = !!(@colorize && (backend = @logger.backend.as?(Log::IOBackend)) && backend.io.tty?)\n\n      message.colorize.fore(fore).back(back)\n    end\n\n    # return `true` if is binary types with MIME type\n    #\n    # MIME types list: https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types\n    private def binary_type?(content_type)\n      binary_types = %w(image audio video)\n      application_types = %w(pdf octet-stream ogg 3gpp ebook archive rar zip tar 7z word powerpoint excel flash font)\n\n      binary_types.each do |name|\n        return true if content_type.starts_with?(name)\n      end\n\n      application_types.each do |name|\n        return true if content_type.starts_with?(\"application\") && content_type.includes?(name)\n      end\n\n      false\n    end\n\n    Logging.register \"common\", self\n  end\nend\n"
  },
  {
    "path": "src/halite/features/logging/json.cr",
    "content": "require \"json\"\n\nclass Halite::Logging\n  # JSON logging format\n  #\n  # Instance variables to check `Halite::Logging::Abstract`.\n  #\n  # In JSON format, if you set skip some key, it will return `false`.\n  #\n  # ```\n  # Halite.use(\"logging\", logging: Halite::Logging::JSON.new(skip_request_body: true))\n  #   .get(\"http://httpbin.org/get\")\n  #\n  # # Or\n  # Halite.logging(format: \"json\", skip_request_body: true)\n  #   .get(\"http://httpbin.org/get\")\n  # ```\n  #\n  # Log will look like:\n  #\n  # ```\n  # {\n  #   \"created_at\": \"2018-08-31T16:53:57+08:00:00\",\n  #   \"entry\":      {\n  #     \"request\": {\n  #       \"body\":      \"\",\n  #       \"headers\":   {...},\n  #       \"method\":    \"GET\",\n  #       \"url\":       \"http://httpbin.org/anything\",\n  #       \"timestamp\": \"2018-08-31T16:53:59+08:00:00\",\n  #     },\n  #     \"response\": {\n  #       \"body\":         false,\n  #       \"header\":       {...},\n  #       \"status_code\":  200,\n  #       \"http_version\": \"HTTP/1.1\",\n  #       \"timestamp\":    \"2018-08-31T16:53:59+08:00:00\",\n  #     },\n  #   },\n  # }\n  # ```\n  class JSON < Abstract\n    @request : Request? = nil\n    @response : Response? = nil\n\n    def request(request)\n      @request_time = Time.utc\n      @request = Request.new(request, @skip_request_body)\n    end\n\n    def response(response)\n      @response = Response.new(response, @skip_response_body)\n      @logger.info { raw }\n    end\n\n    private def raw\n      elapsed : String? = nil\n      if !@skip_benchmark && (request_time = @request_time)\n        elapsed = human_time(Time.utc - request_time)\n      end\n\n      {\n        \"created_at\" => Helper.to_rfc3339(@request_time.not_nil!),\n        \"elapsed\"    => elapsed,\n        \"entry\"      => {\n          \"request\"  => @request.not_nil!.to_h,\n          \"response\" => @response.not_nil!.to_h,\n        },\n      }.to_pretty_json\n    end\n\n    # :nodoc:\n    private struct Request\n      def initialize(@request : Halite::Request, @skip_body = false)\n      end\n\n      def to_h\n        {\n          \"body\"      => @skip_body ? false : @request.body,\n          \"headers\"   => @request.headers.to_flat_h,\n          \"method\"    => @request.verb,\n          \"url\"       => @request.uri.to_s,\n          \"timestamp\" => Helper.to_rfc3339(Time.utc),\n        }\n      end\n    end\n\n    # :nodoc:\n    private struct Response\n      def initialize(@response : Halite::Response, @skip_body = false)\n      end\n\n      def to_h\n        {\n          \"body\"         => @skip_body ? false : @response.body,\n          \"header\"       => @response.headers.to_flat_h,\n          \"status_code\"  => @response.status_code,\n          \"http_version\" => @response.version,\n          \"timestamp\"    => Helper.to_rfc3339(Time.utc),\n        }\n      end\n    end\n\n    Logging.register \"json\", self\n  end\nend\n"
  },
  {
    "path": "src/halite/features/logging.cr",
    "content": "require \"log\"\nrequire \"colorize\"\nrequire \"file_utils\"\n\nLog.setup do |c|\n  backend = Log::IOBackend.new(formatter: Halite::Logging::ShortFormat)\n  c.bind(\"halite\", :info, backend)\nend\n\nmodule Halite\n  # Logging feature\n  class Logging < Feature\n    DEFAULT_LOGGER = Logging::Common.new\n\n    getter writer : Logging::Abstract\n\n    # return a new Cache instance\n    #\n    # Accepts argument:\n    #\n    # - **logging**: `Logging::Abstract`\n    def initialize(**options)\n      @writer = (logging = options[:logging]?) ? logging.as(Logging::Abstract) : DEFAULT_LOGGER\n    end\n\n    def request(request)\n      @writer.request(request)\n      request\n    end\n\n    def response(response)\n      @writer.response(response)\n      response\n    end\n\n    # Logging format Abstract\n    abstract class Abstract\n      setter logger : Log\n      getter skip_request_body : Bool\n      getter skip_response_body : Bool\n      getter skip_benchmark : Bool\n      getter colorize : Bool\n\n      @request_time : Time?\n\n      def initialize(*, for : String = \"halite\",\n                     @skip_request_body = false, @skip_response_body = false,\n                     @skip_benchmark = false, @colorize = true)\n        @logger = Log.for(for)\n        Colorize.enabled = @colorize\n      end\n\n      abstract def request(request)\n      abstract def response(response)\n\n      protected def human_time(elapsed : Time::Span)\n        elapsed = elapsed.to_f\n        case Math.log10(elapsed)\n        when 0..Float64::MAX\n          digits = elapsed\n          suffix = \"s\"\n        when -3..0\n          digits = elapsed * 1000\n          suffix = \"ms\"\n        when -6..-3\n          digits = elapsed * 1_000_000\n          suffix = \"µs\"\n        else\n          digits = elapsed * 1_000_000_000\n          suffix = \"ns\"\n        end\n\n        \"#{digits.round(2).to_s}#{suffix}\"\n      end\n    end\n\n    @@formats = {} of String => Abstract.class\n\n    # Logging format register\n    module Register\n      def register(name : String, format : Abstract.class)\n        @@formats[name] = format\n      end\n\n      def [](name : String)\n        @@formats[name]\n      end\n\n      def []?(name : String)\n        @@formats[name]?\n      end\n\n      def availables\n        @@formats.keys\n      end\n    end\n\n    # Similar to `Log::ShortFormat`\n    #\n    # **NOTE**: It invalid by calling `Log.setup` or `Log.setup_from_env` outside of Halite.\n    #\n    # Copy from https://github.com/crystal-lang/crystal/blob/3c48f311f/src/log/format.cr#L197\n    struct ShortFormat < Log::StaticFormatter\n      def run\n        \"#{timestamp} - #{source(before: \" \", after: \": \")}#{message}\" \\\n        \"#{data(before: \" -- \")}#{context(before: \" -- \")}#{exception}\"\n      end\n\n      def timestamp\n        Helper.to_rfc3339(@entry.timestamp, @io)\n      end\n    end\n\n    extend Register\n\n    Halite.register_feature \"logging\", self\n  end\nend\n\nrequire \"./logging/*\"\n"
  },
  {
    "path": "src/halite/form_data.cr",
    "content": "require \"http/formdata\"\nrequire \"mime/multipart\"\n\nmodule Halite\n  # Utility-belt to build form data request bodies.\n  #\n  # Provides support for `application/x-www-form-urlencoded` and\n  # `multipart/form-data` types.\n  #\n  # ```\n  # form = FormData.create({\n  #   \"name\"   => \"Lizeth Gusikowski\",\n  #   \"skill\"  => [\"ruby\", \"crystal\"],\n  #   \"avatar\" => File.open(\"avatar.png\"), # => \"image binary data\"\n  # })\n  #\n  # form.body    # => \"----------------------------_ytTht-0D5oif0cAGXSPjPSN\\r\\nContent-Disposition: form-data; name=\\\"name\\\"\\r\\n\\r\\nLizeth Gusikowski\\r\\n----------------------------_ytTht-0D5oif0cAGXSPjPSN\\r\\nContent-Disposition: form-data; name=\\\"skill\\\"\\r\\n\\r\\nruby\\r\\n----------------------------_ytTht-0D5oif0cAGXSPjPSN\\r\\nContent-Disposition: form-data; name=\\\"skill\\\"\\r\\n\\r\\ncrystal\\r\\n----------------------------_ytTht-0D5oif0cAGXSPjPSN\\r\\nContent-Disposition: form-data; name=\\\"avatar\\\"; filename=\\\"avatar.png\\\"\\r\\n\\r\\nimage binary data\\n\\r\\n----------------------------_ytTht-0D5oif0cAGXSPjPSN--\"\n  # form.headers # => HTTP::Headers{\"Content-Type\" => \"multipart/form-data; boundary=\\\"--------------------------SS0a9QKeM_6fcj2CE5D4d0LQ\\\"\"}\n  # ```\n  module FormData\n    # FormData factory. Automatically selects best type depending on given `data` Hash\n    def self.create(data : Hash(String, Halite::Options::Type) = {} of String => Halite::Options::Type) : Halite::Request::Data\n      if multipart?(data)\n        io = IO::Memory.new\n        builder = HTTP::FormData::Builder.new(io)\n        data.each do |k, v|\n          case v\n          when File\n            builder.file(k, v.as(IO), HTTP::FormData::FileMetadata.new(filename: File.basename(v.path)))\n          when Array\n            v.each do |e|\n              case e\n              when File\n                builder.file(k, e.as(IO), HTTP::FormData::FileMetadata.new(filename: File.basename(e.path)))\n              else\n                builder.field(k, e.to_s)\n              end\n            end\n          else\n            builder.field(k, v.to_s)\n          end\n        end\n        builder.finish\n\n        Halite::Request::Data.new(io.to_s, builder.content_type)\n      else\n        body = HTTP::Params.encode(data)\n        Halite::Request::Data.new(body, \"application/x-www-form-urlencoded\")\n      end\n    end\n\n    # Tells whenever data contains multipart data or not.\n    private def self.multipart?(data : Hash(String, Halite::Options::Type)) : Bool\n      data.any? do |_, v|\n        case v\n        when File\n          next true\n        when Array\n          v.any? do |vv|\n            next true if vv.is_a?(File)\n          end\n        else\n          false\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/header_link.cr",
    "content": "module Halite\n  # HeaderLink\n  #\n  # ref: [https://tools.ietf.org/html/rfc5988](https://tools.ietf.org/html/rfc5988)\n  struct HeaderLink\n    # Header link parser\n    def self.parse(raw : String, uri : URI? = nil) : Hash(String, Halite::HeaderLink)\n      links = {} of String => HeaderLink\n      raw.split(/,\\s*</).each do |rel|\n        head_link = parse_link(rel, uri)\n        links[head_link.rel] = head_link\n      end\n      links\n    end\n\n    private def self.parse_link(raw, uri)\n      params = {} of String => String\n      if raw.includes?(\";\")\n        target, attrs = raw.split(\";\", 2)\n        rel = target = target.gsub(/[<> '\\\"]/, \"\").strip\n        unless attrs.strip.empty?\n          attrs.split(\";\").each do |attr|\n            next if attr.strip.empty?\n            key, value = attr.split(\"=\")\n            key = key.gsub(/['\\\"]/, \"\").strip\n            next if params.has_key?(key)\n\n            value = value.gsub(/['\\\"]/, \"\").strip\n            params[key] = value\n          end\n\n          if name = params.delete(\"rel\")\n            rel = name\n            if target == \"/\"\n              target = rel\n            elsif target.starts_with?(\"/\") && (uri_local = uri)\n              full_target = uri_local.dup\n              full_target.path = target\n              target = full_target.to_s\n            end\n          end\n        end\n      else\n        rel = target = raw.gsub(/[<> '\\\"]/, \"\").strip\n      end\n\n      new(rel, target, params)\n    end\n\n    getter rel, target, params\n\n    def initialize(@rel : String, @target : String, @params : Hash(String, String))\n    end\n\n    def to_s(io)\n      io << target\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/mime_type.cr",
    "content": "module Halite\n  module MimeType\n    @@adapters = {} of String => MimeType::Adapter\n    @@aliases = {} of String => String\n\n    def self.register(adapter : MimeType::Adapter, name : String, *shortcuts)\n      @@adapters[name] = adapter\n      shortcuts.each do |shortcut|\n        next unless shortcut.is_a?(String)\n        @@aliases[shortcut] = name\n      end unless shortcuts.empty?\n    end\n\n    def self.[](name : String)\n      @@adapters[normalize name]\n    end\n\n    def self.[]?(name : String)\n      @@adapters[normalize name]?\n    end\n\n    private def self.normalize(name : String)\n      @@aliases.fetch name, name\n    end\n\n    abstract class Adapter\n      abstract def encode(obj)\n      abstract def decode(string)\n    end\n  end\nend\n\nrequire \"./mime_types/*\"\n"
  },
  {
    "path": "src/halite/mime_types/json.cr",
    "content": "require \"json\"\n\nmodule Halite::MimeType\n  class JSON < Adapter\n    def encode(obj)\n      obj.to_json\n    end\n\n    def decode(str)\n      ::JSON.parse str\n    end\n  end\nend\n\nHalite::MimeType.register Halite::MimeType::JSON.new, \"application/json\", \"json\"\n"
  },
  {
    "path": "src/halite/options/follow.cr",
    "content": "module Halite\n  class Options\n    struct Follow\n      # No follow by default\n      DEFAULT_HOPS = 0\n\n      # A maximum of 5 subsequent redirects\n      MAX_HOPS = 5\n\n      # Redirector hops policy\n      STRICT = true\n\n      property hops : Int32\n      property strict : Bool\n\n      def initialize(hops : Int32? = nil, strict : Bool? = nil)\n        @hops = hops || DEFAULT_HOPS\n        @strict = strict.nil? ? STRICT : strict\n      end\n\n      def strict?\n        @strict == true\n      end\n\n      def updated?\n        @hops != DEFAULT_HOPS || @strict != STRICT\n      end\n    end\n  end\n\n  # :nodoc:\n  alias Follow = Options::Follow\nend\n"
  },
  {
    "path": "src/halite/options/timeout.cr",
    "content": "module Halite\n  class Options\n    # Timeout struct\n    struct Timeout\n      getter connect : Float64?\n      getter read : Float64?\n      getter write : Float64?\n\n      def initialize(connect : (Int32 | Float64 | Time::Span)? = nil,\n                     read : (Int32 | Float64 | Time::Span)? = nil,\n                     write : (Int32 | Float64 | Time::Span)? = nil)\n        @connect = timeout_value(connect)\n        @read = timeout_value(read)\n        @write = timeout_value(write)\n      end\n\n      def connect=(connect : (Int32 | Float64 | Time::Span)?)\n        @connect = timeout_value(connect)\n      end\n\n      def read=(read : (Int32 | Float64 | Time::Span)?)\n        @read = timeout_value(read)\n      end\n\n      def write=(write : (Int32 | Float64 | Time::Span)?)\n        @write = timeout_value(write)\n      end\n\n      private def timeout_value(value : (Int32 | Float64 | Time::Span)? = nil) : Float64?\n        case value\n        when Int32\n          value.as(Int32).to_f\n        when Float64\n          value.as(Float64)\n        when Time::Span\n          value.as(Time::Span).total_seconds.to_f\n        else\n          nil\n        end\n      end\n    end\n  end\n\n  # :nodoc:\n  alias Timeout = Options::Timeout\nend\n"
  },
  {
    "path": "src/halite/options.cr",
    "content": "require \"openssl\"\nrequire \"./options/*\"\n\nmodule Halite\n  # Options class\n  #\n  # ### Init with splats options\n  #\n  # ```\n  # o = Options.new(\n  #   headers: {\n  #     user_agent: \"foobar\"\n  #   }\n  # }\n  # o.headers.class # => HTTP::Headers\n  # o.cookies.class # => HTTP::Cookies\n  # ```\n  #\n  # ### Set/Get timeout\n  #\n  # Set it with `connect_timeout`/`read_timeout`/`write_timeout` keys,\n  # but get it call `Timeout` class.\n  #\n  # ```\n  # o = Options.new(connect_timeout: 30, read_timeout: 30)\n  # o.timeout.connect # => 30.0\n  # o.timeout.read    # => 30.0\n  # o.timeout.write   # => nil\n  # ```\n  #\n  # ### Set/Get follow\n  #\n  # Set it with `follow`/`follow_strict` keys, but get it call `Follow` class.\n  #\n  # ```\n  # o = Options.new(follow: 3, follow_strict: false)\n  # o.follow.hops   # => 3\n  # o.follow.strict # => false\n  # ```\n  class Options\n    def self.new(options : Halite::Options)\n      options.is_a?(self) ? options : super\n    end\n\n    def self.new(endpoint : (String | URI)? = nil,\n                 headers : (Hash(String, _) | NamedTuple)? = nil,\n                 cookies : (Hash(String, _) | NamedTuple)? = nil,\n                 params : (Hash(String, _) | NamedTuple)? = nil,\n                 form : (Hash(String, _) | NamedTuple)? = nil,\n                 json : (Hash(String, _) | NamedTuple)? = nil,\n                 raw : String? = nil,\n                 connect_timeout : (Int32 | Float64 | Time::Span)? = nil,\n                 read_timeout : (Int32 | Float64 | Time::Span)? = nil,\n                 write_timeout : (Int32 | Float64 | Time::Span)? = nil,\n                 follow : Int32? = nil,\n                 follow_strict : Bool? = nil,\n                 tls : OpenSSL::SSL::Context::Client? = nil,\n                 features = {} of String => Feature)\n      new(\n        endpoint: endpoint,\n        headers: headers,\n        cookies: cookies,\n        params: params,\n        form: form,\n        json: json,\n        raw: raw,\n        timeout: Timeout.new(connect: connect_timeout, read: read_timeout, write: write_timeout),\n        follow: Follow.new(hops: follow, strict: follow_strict),\n        tls: tls,\n        features: features\n      )\n    end\n\n    # Types of options in a Hash\n    alias Type = Nil | Symbol | String | Int32 | Int64 | Float64 | Bool | File | Array(Type) | Hash(String, Type)\n\n    property endpoint : URI?\n    property headers : HTTP::Headers\n    property cookies : HTTP::Cookies\n    property timeout : Timeout\n    property follow : Follow\n    property tls : OpenSSL::SSL::Context::Client?\n\n    property params : Hash(String, Type)\n    property form : Hash(String, Type)\n    property json : Hash(String, Type)\n    property raw : String?\n\n    property features : Hash(String, Feature)\n\n    def initialize(*,\n                   endpoint : (String | URI)? = nil,\n                   headers : (Hash(String, _) | NamedTuple)? = nil,\n                   cookies : (Hash(String, _) | NamedTuple)? = nil,\n                   params : (Hash(String, _) | NamedTuple)? = nil,\n                   form : (Hash(String, _) | NamedTuple)? = nil,\n                   json : (Hash(String, _) | NamedTuple)? = nil,\n                   @raw : String? = nil,\n                   @timeout = Timeout.new,\n                   @follow = Follow.new,\n                   @tls : OpenSSL::SSL::Context::Client? = nil,\n                   @features = {} of String => Feature)\n      @endpoint = parse_endpoint(endpoint)\n      @headers = parse_headers(headers)\n      @cookies = parse_cookies(cookies)\n      @params = parse_params(params)\n      @form = parse_form(form)\n      @json = parse_json(json)\n    end\n\n    def initialize(*,\n                   @endpoint : URI?,\n                   @headers : HTTP::Headers,\n                   @cookies : HTTP::Cookies,\n                   @params : Hash(String, Type),\n                   @form : Hash(String, Type),\n                   @json : Hash(String, Type),\n                   @raw : String? = nil,\n                   @timeout = Timeout.new,\n                   @follow = Follow.new,\n                   @tls : OpenSSL::SSL::Context::Client? = nil,\n                   @features = {} of String => Feature)\n    end\n\n    def with_endpoint(endpoint : String | URI)\n      self.endpoint = endpoint\n      self\n    end\n\n    # Alias `with_headers` method.\n    def with_headers(**with_headers) : Halite::Options\n      with_headers(with_headers)\n    end\n\n    # Returns `Options` self with given headers combined.\n    def with_headers(headers : Hash(String, _) | NamedTuple) : Halite::Options\n      @headers.merge!(parse_headers(headers))\n      self\n    end\n\n    # Alias `with_cookies` method.\n    def with_cookies(**cookies) : Halite::Options\n      with_cookies(cookies)\n    end\n\n    # Returns `Options` self with given cookies combined.\n    def with_cookies(cookies : Hash(String, _) | NamedTuple) : Halite::Options\n      cookies.each do |key, value|\n        @cookies[key.to_s] = value.to_s\n      end\n\n      self\n    end\n\n    # Returns `Options` self with given cookies combined.\n    def with_cookies(cookies : HTTP::Cookies) : Halite::Options\n      cookies.each do |cookie|\n        with_cookies(cookie)\n      end\n\n      self\n    end\n\n    # Returns `Options` self with given cookies combined.\n    def with_cookies(cookie : HTTP::Cookie) : Halite::Options\n      cookie_header = HTTP::Headers{\"Cookie\" => cookie.to_cookie_header}\n      @headers.merge!(cookie_header)\n      @cookies.fill_from_client_headers(@headers)\n      self\n    end\n\n    # Returns `Options` self with given max hops of redirect times.\n    #\n    # ```\n    # # Automatically following redirects\n    # options.with_follow\n    # # A maximum of 3 subsequent redirects\n    # options.with_follow(3)\n    # # Set subsequent redirects\n    # options.with_follow(3)\n    # ```\n    def with_follow(follow = Follow::MAX_HOPS, strict = Follow::STRICT) : Halite::Options\n      @follow.hops = follow\n      @follow.strict = strict\n      self\n    end\n\n    # Returns `Options` self with given connect, read timeout.\n    def with_timeout(connect : (Int32 | Float64 | Time::Span)? = nil,\n                     read : (Int32 | Float64 | Time::Span)? = nil,\n                     write : (Int32 | Float64 | Time::Span)? = nil) : Halite::Options\n      @timeout.connect = connect.to_f if connect\n      @timeout.read = read.to_f if read\n      @timeout.write = write.to_f if write\n\n      self\n    end\n\n    # Returns `Options` self with the name of features.\n    def with_features(*features)\n      features.each do |feature|\n        with_features(feature, NamedTuple.new)\n      end\n      self\n    end\n\n    # Returns `Options` self with feature name and options.\n    def with_features(feature_name : String, **opts)\n      with_features(feature_name, opts)\n    end\n\n    # Returns `Options` self with feature name and options.\n    def with_features(name : String, opts : NamedTuple)\n      raise UnRegisterFeatureError.new(\"Not available feature: #{name}\") unless klass = Halite.feature?(name)\n      @features[name] = klass.new(**opts)\n      self\n    end\n\n    # Returns `Options` self with feature name and feature.\n    def with_features(name : String, feature : Feature)\n      @features[name] = feature\n      self\n    end\n\n    # Returns `Options` iitself with given format and the options of format.\n    def with_logging(format : String, **opts)\n      raise UnRegisterLoggerFormatError.new(\"Not available logging format: #{format}\") unless format_cls = Logging[format]?\n      with_logging(format_cls.new(**opts))\n    end\n\n    # Returns `Options` self with given logging, depend on `with_features`.\n    def with_logging(logging : Halite::Logging::Abstract)\n      with_features(\"logging\", logging: logging)\n      self\n    end\n\n    # Set endpoint of request\n    def endpoint=(endpoint : String)\n      @endpoint = URI.parse(endpoint)\n    end\n\n    # Set headers of request\n    def headers=(headers : (Hash(String, _) | NamedTuple))\n      @headers = parse_headers(headers)\n    end\n\n    # Alias `Timeout.connect`\n    def connect_timeout\n      @timeout.connect\n    end\n\n    # Alias `Timeout.connect=`\n    def connect_timeout=(timeout : Int32 | Float64 | Time::Span)\n      @timeout.connect = timeout\n    end\n\n    # Alias `Timeout.read`\n    def read_timeout\n      @timeout.read\n    end\n\n    # Alias `Timeout.read=`\n    def read_timeout=(timeout : Int32 | Float64 | Time::Span)\n      @timeout.read = timeout\n    end\n\n    # Alias `Timeout.write`\n    def write_timeout\n      @timeout.write\n    end\n\n    # Alias `Timeout.write=`\n    def write_timeout=(timeout : Int32 | Float64 | Time::Span)\n      @timeout.write = timeout\n    end\n\n    # Alias `Follow.hops=`\n    def follow=(hops : Int32)\n      @follow.hops = hops\n    end\n\n    # Alias `Follow.strict`\n    def follow_strict\n      @follow.strict\n    end\n\n    # Alias `Follow.strict=`\n    def follow_strict=(strict : Bool)\n      @follow.strict = strict\n    end\n\n    # Get logging status\n    def logging : Bool\n      !@features.values.select { |v| v.is_a?(Halite::Logging) }.empty?\n    end\n\n    # Quick enable logging\n    #\n    # By defaults, use `Logging::Common` as logging output.\n    def logging=(enable : Bool)\n      if enable\n        with_features(\"logging\") unless logging\n      else\n        @features.delete(\"logging\")\n      end\n    end\n\n    # Merge with other `Options` and return new `Halite::Options`\n    def merge(other : Halite::Options) : Halite::Options\n      options = Halite::Options.new\n      options.merge!(dup)\n      options.merge!(other)\n      options\n    end\n\n    # Merge with other `Options` and return self\n    def merge!(other : Halite::Options) : Halite::Options\n      @endpoint = other.endpoint if other.endpoint\n\n      @headers.merge!(other.headers)\n\n      other.cookies.each do |cookie|\n        @cookies << cookie\n      end if other.cookies != @cookies\n\n      if other.timeout.connect || other.timeout.read || other.timeout.write\n        @timeout = other.timeout\n      end\n\n      if other.follow.updated?\n        @follow = other.follow\n      end\n\n      @features.merge!(other.features) unless other.features.empty?\n      @params.merge!(other.params) if other.params\n      @form.merge!(other.form) if other.form\n      @json.merge!(other.json) if other.json\n      @raw = other.raw if other.raw\n      @tls = other.tls if other.tls\n\n      self\n    end\n\n    # Reset options\n    def clear! : Halite::Options\n      @endpoint = nil\n      @headers = HTTP::Headers.new\n      @cookies = HTTP::Cookies.new\n      @params = {} of String => Type\n      @form = {} of String => Type\n      @json = {} of String => Type\n      @raw = nil\n      @timeout = Timeout.new\n      @follow = Follow.new\n      @features = {} of String => Feature\n      @tls = nil\n\n      self\n    end\n\n    # Produces a shallow copy of obj—the instance variables of obj are copied,\n    # but not the objects they reference. dup copies the tainted state of obj.\n    def dup\n      Halite::Options.new(\n        endpoint: @endpoint,\n        headers: @headers.dup,\n        cookies: @cookies,\n        params: @params,\n        form: @form,\n        json: @json,\n        raw: @raw,\n        timeout: @timeout,\n        follow: @follow,\n        features: @features,\n        tls: @tls\n      )\n    end\n\n    # Returns this collection as a plain Hash.\n    def to_h\n      {\n        \"endpoint\"        => @endpoint,\n        \"headers\"         => @headers.to_h,\n        \"cookies\"         => @cookies.to_h,\n        \"params\"          => @params ? @params.to_h : nil,\n        \"form\"            => @form ? @form.to_h : nil,\n        \"json\"            => @json ? @json.to_h : nil,\n        \"raw\"             => @raw,\n        \"connect_timeout\" => @timeout.connect,\n        \"read_timeout\"    => @timeout.read,\n        \"follow\"          => @follow.hops,\n        \"follow_strict\"   => @follow.strict,\n      }\n    end\n\n    private def parse_endpoint(endpoint : (String | URI)?) : URI?\n      case endpoint\n      when String\n        URI.parse(endpoint)\n      when URI\n        endpoint.as(URI)\n      else\n        nil\n      end\n    end\n\n    private def parse_headers(raw : (Hash(String, _) | NamedTuple | HTTP::Headers)?) : HTTP::Headers\n      case raw\n      when Hash, NamedTuple\n        HTTP::Headers.encode(raw)\n      when HTTP::Headers\n        raw.as(HTTP::Headers)\n      else\n        HTTP::Headers.new\n      end\n    end\n\n    private def parse_cookies(raw : (Hash(String, _) | NamedTuple | HTTP::Cookies)?) : HTTP::Cookies\n      cookies = HTTP::Cookies.from_client_headers(@headers)\n      if objects = raw\n        objects.each do |key, value|\n          cookies[key] = case value\n                         when HTTP::Cookie\n                           value\n                         else\n                           value.to_s\n                         end\n        end\n      end\n      cookies\n    end\n\n    private def parse_cookies(headers : HTTP::Headers) : HTTP::Cookies\n      cookies = HTTP::Cookies.from_client_headers(headers)\n    end\n\n    {% for attr in %w(params form json) %}\n      private def parse_{{ attr.id }}(raw : (Hash(String, _) | NamedTuple)?) : Hash(String, Options::Type)\n        new_{{ attr.id }} = {} of String => Type\n        return new_{{ attr.id }} unless {{ attr.id }} = raw\n\n        if {{ attr.id }}.responds_to?(:each)\n          {{ attr.id }}.each do |key, value|\n            new_{{ attr.id }}[key.to_s] = case value\n                                          when Array\n                                            cast_hash(value.as(Array))\n                                          when Hash\n                                            cast_hash(value.as(Hash))\n                                          when NamedTuple\n                                            cast_hash(value.as(NamedTuple))\n                                          when Type\n                                            value\n                                          else\n                                            value.as(Type)\n                                          end\n          end\n        end\n\n        new_{{ attr.id }}\n      end\n    {% end %}\n\n    private def cast_hash(raw : Array) : Options::Type\n      raw.each_with_object([] of Type) do |value, obj|\n        obj << case value\n        when Array\n          cast_hash(value.as(Array))\n        when Hash\n          cast_hash(value.as(Hash))\n        when NamedTuple\n          cast_hash(value.as(NamedTuple))\n        else\n          value.as(Type)\n        end\n      end.as(Type)\n    end\n\n    private def cast_hash(raw : Hash) : Options::Type\n      raw.each_with_object({} of String => Type) do |(key, value), obj|\n        if key.responds_to?(:to_s)\n          obj[key.to_s] = case value\n                          when Array\n                            cast_hash(value.as(Array))\n                          when Hash\n                            cast_hash(value.as(Hash))\n                          when NamedTuple\n                            cast_hash(value.as(NamedTuple))\n                          else\n                            value.as(Type)\n                          end\n        end\n      end.as(Type)\n    end\n\n    private def cast_hash(raw : NamedTuple) : Options::Type\n      cast_hash(raw.to_h)\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/rate_limit.cr",
    "content": "module Halite\n  # Limit Rate\n  #\n  # ref: [https://tools.ietf.org/id/draft-polli-ratelimit-headers-00.html](https://tools.ietf.org/id/draft-polli-ratelimit-headers-00.html)\n  #\n  # ```\n  # > X-RateLimit-Limit: 5000\n  # > X-RateLimit-Remaining: 4987\n  # > X-RateLimit-Reset: 1350085394\n  # ```\n  struct RateLimit\n    RATELIMIT_LIMIT     = \"X-RateLimit-Limit\"\n    RATELIMIT_REMAINING = \"X-RateLimit-Remaining\"\n    RATELIMIT_RESET     = \"X-RateLimit-Reset\"\n\n    def self.parse(headers : HTTP::Headers)\n      limit = headers[RATELIMIT_LIMIT]?.try &.to_i\n      remaining = headers[RATELIMIT_REMAINING]?.try &.to_i\n      reset = headers[RATELIMIT_RESET]?.try &.to_i\n      return if !limit && !remaining && !reset\n\n      new(limit, remaining, reset)\n    end\n\n    getter limit, remaining, reset\n\n    def initialize(@limit : Int32?, @remaining : Int32?, @reset : Int32?)\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/redirector.cr",
    "content": "module Halite\n  class Redirector\n    # HTTP status codes which indicate redirects\n    REDIRECT_CODES = [300, 301, 302, 303, 307, 308]\n\n    # Codes which which should raise StateError in strict mode if original\n    # request was any of {UNSAFE_VERBS}\n    STRICT_SENSITIVE_CODES = [300, 301, 302]\n\n    # Insecure http verbs, which should trigger StateError in strict mode\n    # upon {STRICT_SENSITIVE_CODES}\n    UNSAFE_VERBS = %w(PUT DELETE POST)\n\n    # Verbs which will remain unchanged upon See Other response.\n    SEE_OTHER_ALLOWED_VERBS = %w(GET HEAD)\n\n    def self.new(request : Halite::Request, response : Halite::Response, options : Halite::Options)\n      new(request, response, options.follow.hops, options.follow.strict)\n    end\n\n    getter strict : Bool\n    getter max_hops : Int32\n\n    # Instance a new Redirector\n    def initialize(@request : Halite::Request, @response : Halite::Response, @max_hops = 5, @strict = true)\n      @visited = [] of String\n    end\n\n    # Follows redirects until non-redirect response found\n    def perform(&block : Halite::Request -> Halite::Response) : Halite::Response\n      if avaiable?\n        each_redirect do |request|\n          block.call(request)\n        end\n      end\n\n      @response\n    end\n\n    # Loop each redirect request with block call\n    def each_redirect(&block : Halite::Request -> Halite::Response)\n      while avaiable?\n        @visited << \"#{@request.verb} #{@request.uri}\"\n\n        raise TooManyRedirectsError.new if too_many_hops?\n        raise EndlessRedirectError.new if endless_loop?\n\n        @request = redirect_to(@response.headers[\"Location\"]?)\n        @response = block.call(@request)\n      end\n    end\n\n    # Return `true` if it should redirect, else `false`\n    def avaiable?\n      REDIRECT_CODES.includes?(@response.status_code)\n    end\n\n    # Redirect policy for follow\n    private def redirect_to(uri : String?)\n      raise StateError.new(\"No found `Location` in headers\") unless uri\n\n      verb = @request.verb\n      code = @response.status_code\n\n      if UNSAFE_VERBS.includes?(verb) && STRICT_SENSITIVE_CODES.includes?(code)\n        raise StateError.new(\"Can not follow #{code} redirect\") if @strict\n        verb = \"GET\"\n      end\n\n      verb = \"GET\" if !SEE_OTHER_ALLOWED_VERBS.includes?(verb) && code == 303\n      @request.redirect(uri, verb)\n    end\n\n    # Check if we reached max amount of redirect hops\n    private def too_many_hops? : Bool\n      1 <= @max_hops && @max_hops < @visited.size\n    end\n\n    # Check if we got into an endless loop\n    def endless_loop?\n      2 <= @visited.count(@visited.last)\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/request.cr",
    "content": "module Halite\n  class Request\n    # Allowed methods\n    #\n    # See more: [https://github.com/crystal-lang/crystal/blob/863f301cfe9e9757a6bf1a494ab7bf49bfc07a06/src/http/client.cr#L329](https://github.com/crystal-lang/crystal/blob/863f301cfe9e9757a6bf1a494ab7bf49bfc07a06/src/http/client.cr#L329)\n    METHODS = %w(GET PUT POST DELETE HEAD PATCH OPTIONS)\n\n    # Allowed schemes\n    SCHEMES = %w(http https)\n\n    # Request user-agent by default\n    USER_AGENT = \"Halite/#{Halite::VERSION}\"\n\n    # The verb name of request\n    getter verb : String\n\n    # The uri of request\n    getter uri : URI\n\n    # The scheme name of request\n    getter scheme : String\n\n    # The headers of request\n    getter headers : HTTP::Headers\n\n    # The payload of request\n    getter body : String\n\n    def initialize(verb : String, @uri : URI, @headers : HTTP::Headers = HTTP::Headers.new, @body : String = \"\")\n      @verb = verb.upcase\n\n      raise UnsupportedMethodError.new(\"Unknown method: #{@verb}\") unless METHODS.includes?(@verb)\n      raise UnsupportedSchemeError.new(\"Missing scheme: #{@uri}\") unless @uri.scheme\n\n      @scheme = @uri.scheme.not_nil!\n\n      raise UnsupportedSchemeError.new(\"Unknown scheme: #{@scheme}\") unless SCHEMES.includes?(@scheme)\n\n      @headers[\"User-Agent\"] ||= USER_AGENT\n      @headers[\"Connection\"] ||= \"close\"\n    end\n\n    # Returns new Request with updated uri\n    def redirect(uri : String, verb = @verb)\n      headers = @headers.dup\n      headers.delete(\"Host\")\n\n      Request.new(verb, redirect_uri(domain, uri), headers, body)\n    end\n\n    # @return `URI` with the scheme, user, password, port and host combined\n    def domain\n      URI.new(@uri.scheme, @uri.host, @uri.port, \"\", nil, @uri.user, @uri.password, nil)\n    end\n\n    # @return `String` with the path, query and fragment combined\n    def full_path\n      String.build do |str|\n        {% if Crystal::VERSION < \"0.36.0\" %}\n          str << @uri.full_path\n        {% else %}\n          str << @uri.request_target\n        {% end %}\n        if @uri.fragment\n          str << \"#\" << @uri.fragment\n        end\n      end\n    end\n\n    private def redirect_uri(source : URI, uri : String) : URI\n      return source if uri == '/'\n\n      new_uri = URI.parse(uri)\n      # return a new uri with source and relative path\n      unless new_uri.scheme && new_uri.host\n        new_uri = source.dup.tap do |u|\n          u.path = (uri[0] == '/') ? uri : \"/#{uri}\"\n        end\n      end\n\n      new_uri\n    end\n\n    # Request data of body\n    struct Data\n      getter body, content_type\n\n      def initialize(@body : String, @content_type : String? = nil)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite/response.cr",
    "content": "module Halite\n  class Response\n    def self.new(uri : URI, status_code : Int32, body : String? = nil, headers = HTTP::Headers.new,\n                 status_message = nil, body_io : IO? = nil, version = \"HTTP/1.1\", history = [] of Halite::Response)\n      conn = HTTP::Client::Response.new(status_code, body, headers, status_message, version, body_io)\n      new(uri, conn, history)\n    end\n\n    getter uri\n    getter conn\n    getter history : Array(Response)\n\n    def initialize(@uri : URI, @conn : HTTP::Client::Response, @history = [] of Halite::Response)\n    end\n\n    delegate version, to: @conn\n    delegate status_code, to: @conn\n    delegate status_message, to: @conn\n    delegate content_type, to: @conn\n    delegate success?, to: @conn\n\n    delegate headers, to: @conn\n    delegate charset, to: @conn\n\n    delegate body, to: @conn\n    delegate body_io, to: @conn\n\n    # Content Length\n    def content_length : Int64?\n      if value = @conn.headers[\"Content-Length\"]?\n        value.to_i64\n      end\n    end\n\n    # Return a `HTTP::Cookies` of parsed cookie headers or else nil.\n    def cookies : HTTP::Cookies?\n      cookies = @conn.cookies ? @conn.cookies : HTTP::Cookies.from_server_headers(@conn.headers)\n\n      # Try to fix empty domain\n      cookies.map do |cookie|\n        cookie.domain = @uri.host unless cookie.domain\n        cookie\n      end\n\n      cookies\n    end\n\n    # Return a list of parsed link headers proxies or else nil.\n    def links : Hash(String, Halite::HeaderLink)?\n      return unless raw = headers[\"Link\"]?\n\n      HeaderLink.parse(raw, uri)\n    end\n\n    def rate_limit : Halite::RateLimit?\n      RateLimit.parse(headers)\n    end\n\n    # Raise `Halite::ClientError`/`Halite::ServerError` if one occurred.\n    #\n    # - `4XX` raise an `Halite::ClientError` exception\n    # - `5XX` raise an `Halite::ServerError` exception\n    # - return `nil` with other status code\n    #\n    # ```\n    # Halite.get(\"https://httpbin.org/status/404\").raise_for_status\n    # # => Unhandled exception: 404 not found error with url: https://httpbin.org/status/404  (Halite::ClientError)\n    #\n    # Halite.get(\"https://httpbin.org/status/500\", params: {\"foo\" => \"bar\"}).raise_for_status\n    # # => Unhandled exception: 500 internal server error error with url: https://httpbin.org/status/500?foo=bar  (Halite::ServerError)\n    #\n    # Halite.get(\"https://httpbin.org/status/301\").raise_for_status\n    # # => nil\n    # ```\n    def raise_for_status\n      if status_code >= 400 && status_code < 500\n        raise Halite::ClientError.new(status_code: status_code, uri: uri)\n      elsif status_code >= 500 && status_code < 600\n        raise Halite::ServerError.new(status_code: status_code, uri: uri)\n      end\n    end\n\n    # Parse response body with corresponding MIME type adapter.\n    def parse(name : String? = nil)\n      name ||= content_type\n      raise Halite::Error.new(\"Missing media type\") unless name\n      raise Halite::UnRegisterMimeTypeError.new(\"unregister MIME type adapter: #{name}\") unless MimeType[name]?\n      MimeType[name].decode to_s\n    end\n\n    # Return filename if it exists, else `Nil`.\n    def filename : String?\n      headers[\"Content-Disposition\"]?.try do |value|\n        value.split(\"filename=\")[1]\n      end\n    end\n\n    # Return raw of response\n    def to_raw\n      io = IO::Memory.new\n      @conn.to_io(io)\n      io\n    end\n\n    # Return status_code, headers and body in a array\n    def to_a\n      [@conn.status_code, @conn.headers, to_s]\n    end\n\n    # Return String eagerly consume the entire body as a string\n    def to_s\n      @conn.body? ? @conn.body : @conn.body_io.to_s\n    end\n\n    def inspect\n      \"#<#{self.class} #{version} #{status_code} #{status_message} #{headers.to_flat_h}>\"\n    end\n\n    def to_s(io)\n      io << to_s\n    end\n  end\nend\n"
  },
  {
    "path": "src/halite.cr",
    "content": "require \"./halite/*\"\nrequire \"./halite/ext/*\"\n\nmodule Halite\n  extend Chainable\n\n  VERSION = \"0.12.1\"\n\n  module Helper\n    # Parses a `Time` into a [RFC 3339](https://tools.ietf.org/html/rfc3339) datetime format string\n    # ([ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile).\n    #\n    # > Load Enviroment named \"TZ\" as high priority\n    def self.to_rfc3339(time : Time, *, timezone = ENV[\"TZ\"]?, fraction_digits : Int = 0)\n      Time::Format::RFC_3339.format(time.in(configure_location(timezone)), fraction_digits: fraction_digits)\n    end\n\n    # Parses a `Time` into a [RFC 3339](https://tools.ietf.org/html/rfc3339) datetime format string to `IO`\n    # ([ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile).\n    #\n    # > Load Enviroment named \"TZ\" as high priority\n    def self.to_rfc3339(time : Time, io : IO, *, timezone = ENV[\"TZ\"]?, fraction_digits : Int = 0)\n      Time::Format::RFC_3339.format(time.in(configure_location(timezone)), io, fraction_digits)\n    end\n\n    # :nodoc:\n    private def self.configure_location(timezone = ENV[\"TZ\"]?)\n      timezone ? Time::Location.load(timezone.not_nil!) : Time::Location::UTC\n    end\n  end\n\n  @@features = {} of String => Feature.class\n\n  module FeatureRegister\n    def register_feature(name : String, klass : Feature.class)\n      @@features[name] = klass\n    end\n\n    def feature(name : String)\n      @@features[name]\n    end\n\n    def feature?(name : String)\n      @@features[name]?\n    end\n\n    def has_feature?(name)\n      @@features.keys.includes?(name)\n    end\n  end\n\n  extend FeatureRegister\nend\n"
  }
]