[
  {
    "path": ".ameba.yml",
    "content": "Documentation/DocumentationAdmonition:\n  Enabled: false\nLint/ComparisonToBoolean:\n  Enabled: false # TODO: Enable once https://github.com/crystal-ameba/ameba/issues/432 is resolved\nLint/Formatting:\n  Enabled: false # This has its own dedicated CI job\nLint/NotNil:\n  Enabled: false\nLint/Typos:\n  Enabled: false # This has its own dedicated CI job\nLint/UselessAssign:\n  ExcludeTypeDeclarations: true # TODO: Disable this once https://github.com/crystal-ameba/ameba/issues/447 is resolved\nNaming/AccessorMethodName:\n  Enabled: false\nNaming/BlockParameterName:\n  Enabled: false\nNaming/QueryBoolMethods:\n  Enabled: false\nStyle/LargeNumbers:\n  Enabled: true\n"
  },
  {
    "path": ".changes/clock/v0.2.0.md",
    "content": "## [0.2.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Remove `Athena::Clock::Interface#sleep(Number)` overload ([#449]) (George Dietrich)\n\n### Fixed\n\n- Fix type error when trying to use `ACLK::Aware#now` ([#498]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/clock/releases/tag/v0.2.0\n[#449]: https://github.com/athena-framework/athena/pull/449\n[#498]: https://github.com/athena-framework/athena/pull/498\n\n## [0.1.2] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Fixed\n\n- Fix that `Athena::Clock::Aware` was not required by default ([#365]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/clock/releases/tag/v0.1.2\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.1.1] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.1.1]: https://github.com/athena-framework/clock/releases/tag/v0.1.1\n\n## [0.1.0] - 2023-09-16\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/clock/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/clock/v0.3.0.md",
    "content": "## [0.3.0] - 2026-04-19\n\n### Removed\n\n- Remove `ACLK::Monotonic` ([#667]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.3.0]: https://github.com/athena-framework/clock/releases/tag/v0.3.0\n[#667]: https://github.com/athena-framework/athena/pull/667\n"
  },
  {
    "path": ".changes/console/v0.4.1.md",
    "content": "## [0.4.1] - 2025-02-08\n\n### Fixed\n\n- Fix incorrectly aligned block ([#519]) (Zohir Tamda)\n\n[0.4.1]: https://github.com/athena-framework/console/releases/tag/v0.4.1\n[#519]: https://github.com/athena-framework/athena/pull/519\n\n## [0.4.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n\n### Added\n\n- **Breaking:** Add `ACON::Output::Verbosity::SILENT` verbosity level ([#489]) (George Dietrich)\n- **Breaking:** Rename `ACON::Completion::Input#must_suggest_values_for?` to `#must_suggest_option_values_for?` ([#498]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.13.0` ([#498]) (George Dietrich)\n- Add `#assert_command_is_not_successful` spec expectation method ([#498]) (George Dietrich)\n- Add support for [`FORCE_COLOR`](https://force-color.org/) and improve color support logic ([#488]) (George Dietrich)\n\n### Fixed\n\n- Fix unexpected completion value when given an array of options ([#498]) (George Dietrich)\n- Fix error when trying to set `ACON::Helper::Table::Style#padding_char` ([#498]) (George Dietrich)\n\n[0.4.0]: https://github.com/athena-framework/console/releases/tag/v0.4.0\n[#428]: https://github.com/athena-framework/athena/pull/428\n[#488]: https://github.com/athena-framework/athena/pull/488\n[#489]: https://github.com/athena-framework/athena/pull/489\n[#498]: https://github.com/athena-framework/athena/pull/498\n\n## [0.3.6] - 2024-07-31\n\n### Changed\n\n- **Breaking:** `ACON::Application#getter` and constructor argument must now be a `String` instead of `SemanticVersion` ([#419]) (George Dietrich)\n- Changed the default `ACON::Application` version to `UNKNOWN` from `0.1.0` ([#419]) (George Dietrich)\n- List commands in a namespace when using it as the command name ([#427]) (George Dietrich)\n- Use single quotes in text descriptor to quote values in the output ([#427]) (George Dietrich)\n\n[0.3.6]: https://github.com/athena-framework/console/releases/tag/v0.3.6\n[#419]: https://github.com/athena-framework/athena/pull/419\n[#427]: https://github.com/athena-framework/athena/pull/427\n\n## [0.3.5] - 2024-04-09\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.11.0` ([#270]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- Support for Windows OS ([#270]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect column/width `ACON::Terminal` values on Windows ([#361]) (George Dietrich)\n\n[0.3.5]: https://github.com/athena-framework/console/releases/tag/v0.3.5\n[#270]: https://github.com/athena-framework/athena/pull/270\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#361]: https://github.com/athena-framework/athena/pull/361\n\n## [0.3.4] - 2023-10-10\n\n### Added\n\n- Add support for tab completion to the `bash` shell when binary is in the `bin/` directory and referenced with `./` ([#323]) (George Dietrich)\n\n[0.3.4]: https://github.com/athena-framework/console/releases/tag/v0.3.4\n[#323]: https://github.com/athena-framework/athena/pull/323\n\n## [0.3.3] - 2023-10-09\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.8.0` ([#282]) (George Dietrich)\n\n### Added\n\n- **Breaking:** Add `ACON::Helper::ProgressBar` to enable rendering progress bars ([#304]) (George Dietrich)\n- Add native shell tab completion support for `bash`, `zsh`, and `fish` for both built-in and custom commands ([#294], [#296], [#297], [#299]) (George Dietrich)\n- Add `ACON::Helper::ProgressIndicator` to enable rendering spinners ([#314]) (George Dietrich)\n- Add support for defining a max height for an `ACON::Output::Section` ([#303]) (George Dietrich)\n- Add `ACON::Helper.format_time` to format a duration as a human readable string ([#304]) (George Dietrich)\n- Add `#assert_command_is_successful` helper method to `ACON::Spec::CommandTester` and `ACON::Spec::ApplicationTester` ([#294]) (George Dietrich)\n\n### Fixed\n\n- Ensure long lines with URLs are not cut when wrapped ([#314]) (George Dietrich)\n- Do not emit erroneous newline from `ACON::Style::Athena` when it's the first thing being written ([#314]) (George Dietrich)\n- Fix misalignment when word wrapping a hyperlink ([#305]) (George Dietrich)\n- Do not emit erroneous extra newlines from an `ACON::Output::Section` ([#303]) (George Dietrich)\n- Fix misalignment within a vertical table with multi-line cell ([#300]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/console/releases/tag/v0.3.3\n[#282]: https://github.com/athena-framework/athena/pull/282\n[#294]: https://github.com/athena-framework/athena/pull/294\n[#296]: https://github.com/athena-framework/athena/pull/296\n[#297]: https://github.com/athena-framework/athena/pull/297\n[#299]: https://github.com/athena-framework/athena/pull/299\n[#300]: https://github.com/athena-framework/athena/pull/300\n[#303]: https://github.com/athena-framework/athena/pull/303\n[#304]: https://github.com/athena-framework/athena/pull/304\n[#305]: https://github.com/athena-framework/athena/pull/305\n[#314]: https://github.com/athena-framework/athena/pull/314\n\n## [0.3.2] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n### Fixed\n\n- Fix formatting issue in Crystal `1.8-dev` ([#258]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/console/releases/tag/v0.3.2\n[#261]: https://github.com/athena-framework/athena/pull/261\n[#258]: https://github.com/athena-framework/athena/pull/258\n\n## [0.3.1] - 2023-02-04\n\n### Added\n\n- Add better integration between `Athena::Console` and `Athena::DependencyInjection` ([#259]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/console/releases/tag/v0.3.1\n[#259]: https://github.com/athena-framework/athena/pull/259\n\n## [0.3.0] - 2023-01-07\n\n### Changed\n\n- **Breaking:** deprecate command default name/description class variables in favor of the new `ACONA::AsCommand` annotation ([#214]) (George Dietrich)\n- **Breaking:** refactor `ACON::Command#application=` to no longer have a `nil` default value ([#217]) (George Dietrich)\n- **Breaking:** refactor `ACON::Command#process_title=` no longer accept `nil` ([#217]) (George Dietrich)\n- **Breaking:** rename `ACON::Command#process_title=` to `ACON::Command#process_title` ([#217]) (George Dietrich)\n\n### Added\n\n- **Breaking:** add `#table` method to `ACON::Style::Interface` ([#220]) (George Dietrich)\n- Add `ACONA::AsCommand` annotation to configure a command's name, description, aliases, and if it should be hidden ([#214]) (George Dietrich)\n- Add support for generating tables ([#220]) (George Dietrich)\n\n### Fixed\n\n- Fix issue with using `ACON::Formatter::Output#format_and_wrap` with `nil` input and an edge case when wrapping a string with a space at the limit ([#220]) (George Dietrich)\n- Fix `ACON::Formatter::NullStyle#*_option` method using incorrect `ACON::Formatter::Mode` type restriction ([#220]) (George Dietrich)\n- Fix some flakiness when testing commands with input ([#224]) (George Dietrich)\n- Fix compiler error when trying to use `ACON::Style::Athena#error_style` ([#240]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/console/releases/tag/v0.3.0\n[#214]: https://github.com/athena-framework/athena/pull/214\n[#217]: https://github.com/athena-framework/athena/pull/217\n[#220]: https://github.com/athena-framework/athena/pull/220\n[#224]: https://github.com/athena-framework/athena/pull/224\n[#240]: https://github.com/athena-framework/athena/pull/240\n\n## [0.2.1] - 2022-09-05\n\n### Changed\n\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n### Added\n\n- Add an `ACON::Input::Interface` based on a command line string ([#186], [#187]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/console/releases/tag/v0.2.1\n[#186]: https://github.com/athena-framework/athena/pull/186\n[#187]: https://github.com/athena-framework/athena/pull/187\n[#188]: https://github.com/athena-framework/athena/pull/188\n\n## [0.2.0] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Changed\n\n- **Breaking:** remove `ACON::Formatter::Mode` in favor of `Colorize::Mode`. Breaking only if not using symbol autocasting. ([#170]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Added\n\n- Add `VERSION` constant to `Athena::Console` namespace ([#166]) (George Dietrich)\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Fixed\n\n- Disallow multi char option shortcuts made up of diff chars ([#164]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/console/releases/tag/v0.2.0\n[#164]: https://github.com/athena-framework/athena/pull/164\n[#166]: https://github.com/athena-framework/athena/pull/166\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#170]: https://github.com/athena-framework/athena/pull/170\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.1] - 2021-12-01\n\n### Fixed\n\n- **Breaking:** fix typo in parameter name of `ACON::Command#option` method ([#3]) (George Dietrich)\n- Fix recursive struct error ([#4]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/console/releases/tag/v0.1.1\n[#3]: https://github.com/athena-framework/console/pull/3\n[#4]: https://github.com/athena-framework/console/pull/4\n\n## [0.1.0] - 2021-10-30\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/console/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/console/v0.4.2.md",
    "content": "## [0.4.2] - 2025-09-04\n\n### Added\n\n- Add ability to customize the finished state of an `ACON::Helper::ProgressIndicator` ([#535]) (George Dietrich) <!-- blacksmoke16 -->\n- Add `markdown` `ACON::Helper::Table` style ([#536]) (George Dietrich) <!-- blacksmoke16 -->\n- Add support for nested style tags ([#568]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix `ACON::Helper::ProgressBar` messing up output in console section with EOL ([#537]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.2]: https://github.com/athena-framework/console/releases/tag/v0.4.2\n[#535]: https://github.com/athena-framework/athena/pull/535\n[#536]: https://github.com/athena-framework/athena/pull/536\n[#568]: https://github.com/athena-framework/athena/pull/568\n[#537]: https://github.com/athena-framework/athena/pull/537\n"
  },
  {
    "path": ".changes/console/v0.4.3.md",
    "content": "## [0.4.3] - 2026-04-19\n\n### Added\n\n- Add opt-in support for deriving the command name from `PROGRAM_NAME` when a CLI binary is invoked via a symlink ([#645]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.3]: https://github.com/athena-framework/console/releases/tag/v0.4.3\n[#645]: https://github.com/athena-framework/athena/pull/645\n"
  },
  {
    "path": ".changes/contracts/v0.1.0.md",
    "content": "## [0.1.0] - 2025-08-02\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/contracts/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/dependency-injection/v0.4.3.md",
    "content": "## [0.4.3] - 2025-02-08\n\n### Changed\n\n- **Breaking:** prevent auto registering of already registered services ([#520]) (George Dietrich)\n\n### Fixed\n\n- Ensure all array values have proper `#of` type ([#508]) (George Dietrich)\n\n[0.4.3]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.3\n[#508]: https://github.com/athena-framework/athena/pull/508\n[#520]: https://github.com/athena-framework/athena/pull/520\n\n## [0.4.2] - 2025-01-26\n\n_Administrative release, no functional changes_\n\n[0.4.2]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.2\n\n## [0.4.1] - 2024-07-31\n\n### Changed\n\n- **Breaking:** single implementation aliases are now explicit ([#408]) (George Dietrich)\n\n### Fixed\n\n- Fix default/nil values related to `object_of` and `array_of` being unavailable in bundle extensions ([#432]) (George Dietrich)\n\n[0.4.1]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.1\n[#408]: https://github.com/athena-framework/athena/pull/408\n[#432]: https://github.com/athena-framework/athena/pull/432\n\n## [0.4.0] - 2024-04-09\n\n### Changed\n\n- **Breaking:** remove `Clock`, `Console`, and `EventDispatcher` built-in integrations ([#337]) (George Dietrich)\n- **Breaking:** major internal refactor ([#337], [#378]) (George Dietrich)\n- **Breaking:** replace `ADI.auto_configure` with [ADI::Autoconfigure](https://athenaframework.org/DependencyInjection/Autoconfigure/) ([#387]) (George Dietrich)\n- **Breaking:** replace `alias` `ADI::Register` field with [ADI::AsAlias](https://athenaframework.org/DependencyInjection/AsAlias/) ([#389]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- Add ability to easily extend/customize the container ([#337], [#348], [#371], [#372], [#373], [#374], [#377], [#379], [#382], [#383]) (George Dietrich)\n- Add ability to define method calls that should be made during service instantiation ([#384]) (George Dietrich)\n- Add new [ADI::AutoconfigureTag](https://athenaframework.org/DependencyInjection/AutoconfigureTag/) and [ADI::TaggedIterator](https://athenaframework.org/DependencyInjection/TaggedIterator/) to make working with tagged services easier ([#387]) (George Dietrich)\n- Add `ADI.configuration_annotation` to `Athena::DependencyInjection` from `Athena::Config` ([#392]) (George Dietrich)\n\n[0.4.0]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.0\n[#337]: https://github.com/athena-framework/athena/pull/337\n[#348]: https://github.com/athena-framework/athena/pull/348\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#371]: https://github.com/athena-framework/athena/pull/371\n[#372]: https://github.com/athena-framework/athena/pull/372\n[#373]: https://github.com/athena-framework/athena/pull/373\n[#374]: https://github.com/athena-framework/athena/pull/374\n[#377]: https://github.com/athena-framework/athena/pull/377\n[#378]: https://github.com/athena-framework/athena/pull/378\n[#379]: https://github.com/athena-framework/athena/pull/379\n[#382]: https://github.com/athena-framework/athena/pull/382\n[#383]: https://github.com/athena-framework/athena/pull/383\n[#384]: https://github.com/athena-framework/athena/pull/384\n[#387]: https://github.com/athena-framework/athena/pull/387\n[#389]: https://github.com/athena-framework/athena/pull/389\n[#392]: https://github.com/athena-framework/athena/pull/392\n\n## [0.3.8] - 2023-12-16\n\n### Fixed\n\n- Avoid depending directly on Crystal macro types ([#335]) (George Dietrich)\n\n[0.3.8]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.8\n[#335]: https://github.com/athena-framework/athena/pull/335\n\n## [0.3.7] - 2023-10-09\n\n### Added\n\n- Add integration between `Athena::DependencyInjection` and the `Athena::Clock` component ([#318]) (George Dietrich)\n\n[0.3.7]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.7\n[#318]: https://github.com/athena-framework/athena/pull/318\n\n## [0.3.6] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.3.6]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.6\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.3.5] - 2023-02-04\n\n### Added\n\n- Add better integration between `Athena::DependencyInjection` and the `Athena::Console` and `Athena::EventDispatcher` components ([#259]) (George Dietrich)\n\n[0.3.5]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.5\n[#259]: https://github.com/athena-framework/athena/pull/259\n\n## [0.3.4] - 2023-01-07\n\n### Changed\n\n- Refactor various internal logic (George Dietrich)\n\n[0.3.4]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.4\n\n## [0.3.3] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.3\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.3.2] - 2021-10-30\n\n### Changed\n\n- Unused services are now excluded from the container ([#30]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.2\n[#30]: https://github.com/athena-framework/dependency-injection/pull/30\n\n## [0.3.1] - 2021-03-28\n\n### Fixed\n\n- Fix error with untyped parameters with default values injecting ([#28]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.1\n[#28]: https://github.com/athena-framework/dependency-injection/pull/28\n\n## [0.3.0] - 2021-03-20\n\n### Added\n\n- Allow injecting [configuration](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--configuration) into services ([#27]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.0\n[#27]: https://github.com/athena-framework/dependency-injection/pull/27\n\n## [0.2.6] - 2021-03-15\n\n### Added\n\n- Allow using the `ADI::Inject` annotation on class methods to create [factories](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--factories) ([#25]) (George Dietrich)\n\n[0.2.6]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.6\n[#25]: https://github.com/athena-framework/dependency-injection/pull/25\n\n## [0.2.5] - 2021-01-30\n\n### Changed\n\n- Migrate documentation to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#23], [#24]) (George Dietrich)\n\n[0.2.5]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.5\n[#23]: https://github.com/athena-framework/dependency-injection/pull/23\n[#24]: https://github.com/athena-framework/dependency-injection/pull/24\n\n## [0.2.4] - 2021-01-29\n\n### Added\n\n- Add dependency on `athena-framework/config` ([#20]) (George Dietrich)\n- Add support for injecting [parameters](https://athenaframework.org/architecture/config/#parameters) into a service ([#20]) (George Dietrich)\n- Add support for [service proxies](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--service-proxies) ([#21]) (George Dietrich)\n\n### Removed\n\n- Remove the `lazy` `ADI::Register` field. All services are lazy by default now ([#21]) (George Dietrich)\n\n### Fixed\n\n- Fix issue building documentation ([#22]) (George Dietrich)\n\n[0.2.4]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.4\n[#20]: https://github.com/athena-framework/dependency-injection/pull/20\n[#21]: https://github.com/athena-framework/dependency-injection/pull/21\n[#22]: https://github.com/athena-framework/dependency-injection/pull/22\n\n## [0.2.3] - 2020-12-24\n\n### Fixed\n\n- Fix error when a parameter has a default value after an array parameter ([#19]) (George Dietrich)\n\n[0.2.3]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.3\n[#19]: https://github.com/athena-framework/dependency-injection/pull/19\n\n## [0.2.2] - 2020-12-03\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#18]) (George Dietrich)\n\n[0.2.2]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.2\n[#18]: https://github.com/athena-framework/dependency-injection/pull/18\n\n## [0.2.1] - 2020-11-14\n\n### Added\n\n- Add a mock container instance to allow mocking services ([#15]) (George Dietrich)\n- Add ability to customize the type of a service within the container ([#15]) (George Dietrich)\n- Add support for [factory pattern](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--factories) constructors ([#16]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.1\n[#15]: https://github.com/athena-framework/dependency-injection/pull/15\n[#16]: https://github.com/athena-framework/dependency-injection/pull/16\n\n## [0.2.0] - 2020-06-09\n\n_Major refactor of the component._\n\n### Added\n\n- Add concept of [aliasing services](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--aliasing-services) ([#10]) (George Dietrich)\n- Add concept of [binding values](https://athenaframework.org/DependencyInjection/#Athena::DependencyInjection:bind(key,value)) ([#10]) (George Dietrich)\n- Add concept of [auto configuration](https://athenaframework.org/DependencyInjection/#Athena::DependencyInjection:auto_configure(type,options)) ([#10]) (George Dietrich)\n- Add [ADI::Inject](https://athenaframework.org/DependencyInjection/Inject/) annotation ([#10]) (George Dietrich)\n- Add support for [generic services](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--generic-services) ([#10]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** manually provided arguments now need to be prefixed with a `_` ([#10]) (George Dietrich)\n- **Breaking:** service names are now based on the `FQN` of the type, downcase underscored by default ([#10]) (George Dietrich)\n- Updated [optional services](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--optional-services) to now be based on the type/default value of the parameter ([#10]) (George Dietrich)\n- Service dependencies are now resolved automatically, removes need to manually provide them ([#10]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** remove the `ADI::Service` module ([#10]) (George Dietrich)\n- **Breaking:** remove the `ADI::Injectable` module ([#10]) (George Dietrich)\n- **Breaking:** remove the `@?` syntax ([#10]) (George Dietrich)\n- **Breaking:** remove the `#get`, `#has`, `#resolve`, `#tagged`, and `#tags` methods from `ADI::ServiceContainer` ([#10]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.0\n[#10]: https://github.com/athena-framework/dependency-injection/pull/10\n\n## [0.1.3] - 2020-04-06\n\n### Fixed\n\n- Fix an edge case by checking includers via `<=` ([#7]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.1.3\n[#7]: https://github.com/athena-framework/dependency-injection/pull/7\n\n## [0.1.2] - 2020-02-22\n\n### Changed\n\n- Change type resolution logic to operate at compile time instead of runtime ([#6]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.1.2\n[#6]: https://github.com/athena-framework/dependency-injection/pull/6\n\n## [0.1.1] - 2020-02-06\n\n### Added\n\n- Add the ability to redefine services ([#4]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.1.1\n[#4]: https://github.com/athena-framework/dependency-injection/pull/4\n\n## [0.1.0] - 2020-01-31\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/dependency-injection/v0.4.4.md",
    "content": "## [0.4.4] - 2025-09-04\n\n### Changed\n\n- Relax DI argument validation for string parameters ([#548]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.4]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.4\n[#548]: https://github.com/athena-framework/athena/pull/548\n"
  },
  {
    "path": ".changes/dependency-injection/v0.4.5.md",
    "content": "## [0.4.5] - 2026-04-19\n\n### Changed\n\n- Improve compile time error messages ([#646]) (George Dietrich) <!-- blacksmoke16 -->\n- Reduce the amount of ivars within `ADI::ServiceContainer` ([#649]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Add ability to define schema configuration maps; with arbitrary keys, but structured values ([#641]) (George Dietrich) <!-- blacksmoke16 -->\n- Add ability to define re-usable schema object types ([#641]) (George Dietrich) <!-- blacksmoke16 -->\n- Add ability for aliases to take constructor parameter names into account ([#660]) (George Dietrich) <!-- blacksmoke16 -->\n- Add support for nested `>>` doc markup when using `object_schema` ([#684]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix global extension schema `Enum` types not retaining their `::` prefix ([#639]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix falsey binding values not resolving ([#647]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix issue with using multiple extensions when one has a nested schema ([#658]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix service argument validation errors overriding schema validation errors ([#659]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix enum typed `object_schema` types not allowing symbol/number values ([#661]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix compile time error when inadvertently using a type name that conflicts with an internal component type ([#678]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix being unable to link to non top-level types within a nested properties' `>>` doc markup ([#684]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.5]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.5\n[#646]: https://github.com/athena-framework/athena/pull/646\n[#649]: https://github.com/athena-framework/athena/pull/649\n[#641]: https://github.com/athena-framework/athena/pull/641\n[#660]: https://github.com/athena-framework/athena/pull/660\n[#684]: https://github.com/athena-framework/athena/pull/684\n[#639]: https://github.com/athena-framework/athena/pull/639\n[#647]: https://github.com/athena-framework/athena/pull/647\n[#658]: https://github.com/athena-framework/athena/pull/658\n[#659]: https://github.com/athena-framework/athena/pull/659\n[#661]: https://github.com/athena-framework/athena/pull/661\n[#678]: https://github.com/athena-framework/athena/pull/678\n"
  },
  {
    "path": ".changes/dotenv/v0.2.0.md",
    "content": "## [0.2.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/dotenv/releases/tag/v0.2.0\n[#428]: https://github.com/athena-framework/athena/pull/428\n\n## [0.1.3] - 2024-07-31\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.13.0` ([#433]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/dotenv/releases/tag/v0.1.3\n[#433]: https://github.com/athena-framework/athena/pull/433\n\n## [0.1.2] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- Add helper `Athena::Dotenv.load` method to create and load `.env` files in one call ([#363]) (George Dietrich)\n\n### Fixed\n\n- Fixed error parsing ENV vars starting with `_` ([#346]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/dotenv/releases/tag/v0.1.2\n[#346]: https://github.com/athena-framework/athena/pull/346\n[#363]: https://github.com/athena-framework/athena/pull/363\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.1.1] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.1.1]: https://github.com/athena-framework/dotenv/releases/tag/v0.1.1\n\n## [0.1.0] - 2023-04-23\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/dotenv/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/dotenv/v0.2.1.md",
    "content": "## [0.2.1] - 2025-11-09\n\n### Fixed\n\n- Fix being unable to call `Athena::Dotenv.load` with a single file ([#609]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.2.1]: https://github.com/athena-framework/dotenv/releases/tag/v0.2.1\n[#609]: https://github.com/athena-framework/athena/pull/609\n"
  },
  {
    "path": ".changes/event-dispatcher/v0.3.1.md",
    "content": "## [0.3.1] - 2025-01-26\n\n_Administrative release, no functional changes_\n\n[0.3.1]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.3.1\n\n## [0.3.0] - 2024-04-09\n\n### Changed\n\n- **Breaking:** remove `AED::EventListenerInterface` ([#391]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.3.0\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#391]: https://github.com/athena-framework/athena/pull/391\n\n## [0.2.3] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.2.3]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.2.3\n\n## [0.2.2] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.2.2]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.2.2\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.2.1] - 2023-02-04\n\n### Added\n\n- Add better integration between `Athena::EventDispatcher` and `Athena::DependencyInjection` ([#259]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.2.1\n[#259]: https://github.com/athena-framework/athena/pull/259\n\n## [0.2.0] - 2023-01-07\n\n### Changed\n\n- **Breaking:** refactor how listeners are registered to use the new `AEDA::AsEventListener` annotation on the method instead of the `self.subscribed_events` class method ([#236]) (George Dietrich)\n- **Breaking:** refactor and rename the majority of `AED::EventDispatcherInterface` API ([#236]) (George Dietrich)\n- **Breaking:** change the representation of a listener when returned from a dispatcher to be an `AED::Callable` instance ([#236]) (George Dietrich)\n- **Breaking:** refactor `AED::Event` to now be `abstract` ([#236]) (George Dietrich)\n\n### Added\n\n- Add `AED::GenericEvent` that can be used for convenience within simple use cases ([#236]) (George Dietrich)\n- Add the ability to use a listener method without the `AED::EventDispatcherInterface` parameter ([#236]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** remove ability for listeners to automatically be registered with the dispatcher ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED::EventDispatcher.new` constructor that accepts an `Array(AED::EventListenerInterface)` ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED::EventListenerType` alias ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED::SubscribedEvents` alias ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED::EventListener` struct ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED.create_listener` method ([#236]) (George Dietrich)\n- Remove the requirement that listeners methods need to be called `call` ([#236]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.2.0\n[#236]: https://github.com/athena-framework/athena/pull/236\n\n## [0.1.4] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Fix the `VERSION` constant's value ([#166]) (George Dietrich)\n\n[0.1.4]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.4\n[#166]: https://github.com/athena-framework/athena/pull/166\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.3] - 2021-01-29\n\n### Changed\n\n- Migrate documentation to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#14]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.3\n[#14]: https://github.com/athena-framework/event-dispatcher/pull/14\n\n## [0.1.2] - 2020-12-03\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#13]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.2\n[#13]: https://github.com/athena-framework/event-dispatcher/pull/13\n\n## [0.1.1] - 2020-11-12\n\n### Added\n\n- Add the [AED::Spec](https://athenaframework.org/EventDispatcher/Spec/) module to provide helpful testing utilities ([#11]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.1\n[#11]: https://github.com/athena-framework/event-dispatcher/pull/11\n\n## [0.1.0] - 2020-01-11\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/event-dispatcher/v0.4.0.md",
    "content": "## [0.4.0] - 2025-09-04\n\n### Changed\n\n- **Breaking:** Changed interface of `AED::EventDispatcherInterface#dispatch` to accept an `ACTR::EventDispatcher::Event` vs `AED::Event` ([#544]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Removed\n\n- Removed `AED::StoppableEvent` in favor of `ACTR::EventDispatcher::StoppableEvent` ([#544]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.0]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.4.0\n[#544]: https://github.com/athena-framework/athena/pull/544\n"
  },
  {
    "path": ".changes/event-dispatcher/v0.4.1.md",
    "content": "## [0.4.1] - 2026-04-19\n\n### Changed\n\n- Improve compile time error messages ([#646]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix compatibility with `ACTR::EventDispatcher::Event` based event types ([#656]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.1]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.4.1\n[#646]: https://github.com/athena-framework/athena/pull/646\n[#656]: https://github.com/athena-framework/athena/pull/656\n"
  },
  {
    "path": ".changes/framework/v0.20.1.md",
    "content": "## [0.20.1] - 2025-02-08\n\n### Fixed\n\n- Fix `ATH::ViewHandler` bundle configuration values not being correctly set ([#520]) (George Dietrich)\n\n[0.20.1]: https://github.com/athena-framework/framework/releases/tag/v0.20.1\n[#520]: https://github.com/athena-framework/athena/pull/520\n\n## [0.20.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n- **Breaking:** The `ATHR::Interface.configuration` macro is no longer scoped to the resolver namespace ([#425]) (George Dietrich)\n- **Breaking:** Rename `ATHR::RequestBody::Extract` to `ATHA::MapRequestBody` ([#425]) (George Dietrich)\n- **Breaking:** Rename `ATHR::Time::Format` to `ATHA::MapTime` ([#425]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.14.0` ([#433]) (George Dietrich)\n- Refactor auto redirection logic to be more robust ([#436], [#480]) (George Dietrich)\n- Refactor `ATHR::RequestBody` to raise more accurate deserialization errors ([#490]) (George Dietrich)\n\n### Added\n\n- Add support for [Proxies & Load Balancers](https://athenaframework.org/guides/proxies/) ([#440], [#444]) (George Dietrich)\n- Add new `trusted_host` bundle scheme property to allow setting trusted hostnames ([#474]) (George Dietrich)\n- Add support for deserializing `application/x-www-form-urlencoded` bodies via `ATHA::MapRequestBody` ([#477]) (George Dietrich)\n- Add `ATHA::MapQueryString` to map a request's query string into a DTO type ([#477]) (George Dietrich)\n- Add `ATH::Exception.from_status` helper method ([#426]) (George Dietrich)\n- Add `ATHA::MapQueryParameter` for handling query parameters ([#426]) (George Dietrich)\n- Add `#validation_groups` and `#accept_formats` annotation properties to `ATHA::MapRequestBody` ([#486]) (George Dietrich)\n- Add `#validation_groups` annotation property to `ATHA::MapQueryString` ([#486]) (George Dietrich)\n- Add `ATH::Request#port` and `ATH::Response#redirect?` methods ([#436]) (George Dietrich)\n- Add `#host`, `#scheme`, `#secure?`, and `#from_trusted_proxy?` methods to `ATH::Request` ([#440]) (George Dietrich)\n- Add `ATH::Request#content_type_format` to return the request format's name from its `content-type` header ([#477]) (George Dietrich)\n- Add `ATH::IPUtils` module ([#440]) (George Dietrich)\n- Add `.unquote`, `.split`, and `.combine` methods `ATH::HeaderUtils` ([#440]) (George Dietrich)\n- Add request matchers for headers and query parameters ([#491]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** Remove `ATHA::QueryParam` ([#426]) (George Dietrich)\n- **Breaking:** Remove `ATHA::RequestParam` ([#426]) (George Dietrich)\n- **Breaking:** Remove `ATH::Exception::InvalidParameter` ([#426]) (George Dietrich)\n- **Breaking:** Remove everything within `ATH::Params` namespace ([#426]) (George Dietrich)\n- **Breaking:** Remove `ATH::Action#params` ([#426]) (George Dietrich)\n- **Breaking:** Remove `ATH::Listeners::ParamFetcher` ([#426]) (George Dietrich)\n\n### Fixed\n\n- Fix query parameters being dropped when redirecting to a trailing/non-trailing slash endpoint ([#436]) (George Dietrich)\n- Fix auto redirection with non-standard ports ([#480]) (George Dietrich)\n- Fix `multipart/form-data` not being mapped to the `form` format ([#441]) (George Dietrich)\n- Fix being unable to provide the path of an `ARTA::Route` annotation on a class as a positional argument ([#482]) (George Dietrich)\n- Fix error when attempting to use `ATH::Controller#redirect_view` and `ATH::Controller#route_redirect_view` ([#498]) (George Dietrich)\n- Fix error when attempting to use `ATH::Spec::APITestCase#unlink` ([#498]) (George Dietrich)\n\n[0.20.0]: https://github.com/athena-framework/framework/releases/tag/v0.20.0\n[#425]: https://github.com/athena-framework/athena/pull/425\n[#426]: https://github.com/athena-framework/athena/pull/426\n[#428]: https://github.com/athena-framework/athena/pull/428\n[#433]: https://github.com/athena-framework/athena/pull/433\n[#436]: https://github.com/athena-framework/athena/pull/436\n[#440]: https://github.com/athena-framework/athena/pull/440\n[#441]: https://github.com/athena-framework/athena/pull/441\n[#444]: https://github.com/athena-framework/athena/pull/444\n[#474]: https://github.com/athena-framework/athena/pull/474\n[#477]: https://github.com/athena-framework/athena/pull/477\n[#480]: https://github.com/athena-framework/athena/pull/480\n[#482]: https://github.com/athena-framework/athena/pull/482\n[#486]: https://github.com/athena-framework/athena/pull/486\n[#490]: https://github.com/athena-framework/athena/pull/490\n[#491]: https://github.com/athena-framework/athena/pull/491\n[#498]: https://github.com/athena-framework/athena/pull/498\n\n## [0.19.2] - 2024-07-31\n\n### Added\n\n- Add `ATH.run_console` as an easier entrypoint into the console application ([#413]) (George Dietrich)\n- Add support for additional boolean conversion values from request attributes ([#422]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** `ATH::RequestMatcher::Method` now requires an `Array(String)` as opposed to any `Enumerable(String)` ([#431]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.13.0` ([#433]) (George Dietrich)\n- Updates usages of `UTF-8` in response headers to `utf-8` as preferred by the RFC ([#417]) (George Dietrich)\n\n### Fixed\n\n- Fix the content negotiation implementation not working ([#431]) (George Dietrich)\n\n[0.19.2]: https://github.com/athena-framework/framework/releases/tag/v0.19.2\n[#413]: https://github.com/athena-framework/athena/pull/413\n[#417]: https://github.com/athena-framework/athena/pull/417\n[#422]: https://github.com/athena-framework/athena/pull/422\n[#431]: https://github.com/athena-framework/athena/pull/431\n[#433]: https://github.com/athena-framework/athena/pull/433\n\n## [0.19.1] - 2024-04-27\n\n### Fixed\n\n- Fix `framework` component docs landing on an empty page ([#399]) (George Dietrich)\n- Fix `Athena::Clock` not being aliased to the interface correctly ([#400]) (George Dietrich)\n- Fix `ATHA::View` annotation being defined in incorrect namespace ([#403]) (George Dietrich)\n- Fix `ATH::ErrorRenderer` not being aliased to the interface correctly ([#404]) (George Dietrich)\n\n[0.19.1]: https://github.com/athena-framework/framework/releases/tag/v0.19.1\n[#399]: https://github.com/athena-framework/athena/pull/399\n[#400]: https://github.com/athena-framework/athena/pull/400\n[#403]: https://github.com/athena-framework/athena/pull/403\n[#404]: https://github.com/athena-framework/athena/pull/404\n\n## [0.19.0] - 2024-04-09\n\n### Changed\n\n- **Breaking:** change how framework features are configured ([#337], [#374], [#383]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.11.0` ([#270]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- Support for Windows OS ([#270]) (George Dietrich)\n- Add `ATH::RequestMatcher` as a generic way of matching an `ATH::Request` given a set of rules ([#338]) (George Dietrich)\n- Raise an exception if a controller's return value fails to serialize instead of just returning `nil` ([#357]) (George Dietrich)\n- Add support for new Crystal 1.12 `Process.on_terminate` method ([#394]) (George Dietrich)\n\n### Fixed\n\n- Fix macro splat deprecation ([#330]) (George Dietrich)\n- Normalize `ATH::Request#method` to always be uppercase ([#338]) (George Dietrich)\n- Fixed not being able to use top level configuration annotations on controller action parameters ([#356]) (George Dietrich)\n\n[0.19.0]: https://github.com/athena-framework/framework/releases/tag/v0.19.0\n[#270]: https://github.com/athena-framework/athena/pull/270\n[#330]: https://github.com/athena-framework/athena/pull/330\n[#337]: https://github.com/athena-framework/athena/pull/337\n[#338]: https://github.com/athena-framework/athena/pull/338\n[#356]: https://github.com/athena-framework/athena/pull/356\n[#357]: https://github.com/athena-framework/athena/pull/357\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#374]: https://github.com/athena-framework/athena/pull/374\n[#383]: https://github.com/athena-framework/athena/pull/383\n[#394]: https://github.com/athena-framework/athena/pull/394\n\n## [0.18.2] - 2023-10-09\n\n### Changed\n\n- Change routing logic to redirect `GET` and `HEAD` requests with a trailing slash to the route without one if it exists, and vice versa ([#307]) (George Dietrich)\n\n### Added\n\n- Add native tab completion support to the built-in `ATH::Commands` ([#296]) (George Dietrich)\n- Add support for defining multiple route annotations on a single controller action method ([#315]) (George Dietrich)\n- Require the new `Athena::Clock` component ([#318]) (George Dietrich)\n- Add additional `ATH::Spec::APITestCase` request helper methods ([#312], [#313]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrectly generated route paths with a controller level prefix and no action level `/` prefix ([#308]) (George Dietrich)\n\n[0.18.2]: https://github.com/athena-framework/framework/releases/tag/v0.18.2\n[#296]: https://github.com/athena-framework/athena/pull/296\n[#307]: https://github.com/athena-framework/athena/pull/307\n[#308]: https://github.com/athena-framework/athena/pull/308\n[#312]: https://github.com/athena-framework/athena/pull/312\n[#313]: https://github.com/athena-framework/athena/pull/313\n[#315]: https://github.com/athena-framework/athena/pull/315\n[#318]: https://github.com/athena-framework/athena/pull/318\n\n## [0.18.1] - 2023-05-29\n\n### Added\n\n- Add support for serializing arbitrarily nested controller action return types ([#273]) (George Dietrich)\n- Allow using constants for controller action's `path` ([#279]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect `content-length` header value when returning multi-byte strings ([#288]) (George Dietrich)\n\n[0.18.1]: https://github.com/athena-framework/framework/releases/tag/v0.18.1\n[#273]: https://github.com/athena-framework/athena/pull/273\n[#279]: https://github.com/athena-framework/athena/pull/279\n[#288]: https://github.com/athena-framework/athena/pull/288\n\n## [0.18.0] - 2023-02-20\n\n### Changed\n\n- **Breaking:** upgrade [Athena::EventDispatcher](https://athenaframework.org/EventDispatcher/) to [0.2.x](https://github.com/athena-framework/event-dispatcher/blob/master/CHANGELOG.md#020---2023-01-07) ([#205]) (George Dietrich)\n- **Breaking:** deprecate the `ATH::ParamConverter` concept in favor of [Value Resolvers](https://athenaframework.org/Framework/Controller/ValueResolvers/Interface) ([#243]) (George Dietrich)\n- **Breaking:** rename various types/methods to better adhere to https://github.com/crystal-lang/crystal/issues/10374 ([#243]) (George Dietrich)\n- **Breaking:** Change `ATH::Spec::AbstractBrowser` to be a `class` ([#249]) (George Dietrich)\n- **Breaking:** upgrade [Athena::Validator](https://athenaframework.org/Validator/) to [0.3.x](https://github.com/athena-framework/validator/blob/master/CHANGELOG.md#030---2023-01-07) ([#250]) (George Dietrich)\n- Improve service `ATH::Controller`s to not need the `public: true` `ADI::Register` field ([#213]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.6.0` ([#205]) (George Dietrich)\n\n### Added\n\n- Add trace logging to `ATH::Listeners::CORS` to aid in debugging ([#265]) (George Dietrich)\n- Introduce new `framework.debug` parameter that is `true` if the binary was _not_ built with the `--release` flag ([#249]) (George Dietrich)\n- Add built-in [HTTP Expectation](https://athenaframework.org/Framework/Spec/Expectations/HTTP) methods to `ATH::Spec::WebTestCase` ([#249]) (George Dietrich)\n- Add `#response` and `#request` methods to `ATH::Spec::AbstractBrowser` types ([#249]) (George Dietrich)\n- Add [ATHR](https://athenaframework.org/Framework/aliases/#ATHR) alias to make using value resolver annotations easier ([#243]) (George Dietrich)\n- Add [ATH::Commands::Commands::DebugEventDispatcher](https://athenaframework.org/Framework/Commands/DebugEventDispatcher) framework CLI command to aid in debugging the event dispatcher ([#241]) (George Dietrich)\n- Add [ATH::Commands::Commands::DebugRouter](https://athenaframework.org/Framework/Commands/DebugRouter) and [ATH::Commands::Commands::DebugRouterMatch](https://athenaframework.org/Framework/Commands/DebugRouterMatch) framework CLI commands to aid in debugging the router ([#224]) (George Dietrich)\n- Add integration for the [Athena::Console](https://athenaframework.org/Console/) component ([#218]) (George Dietrich)\n\n### Fixed\n\n- Correctly populate `content-length` based on the response content's size ([#267]) (George Dietrich)\n- Prevent wildcard CORS `expose_headers` value when `allow_credentials` is `true` ([#264]) (George Dietrich)\n- Correctly handle `JSON::Serializable` values within `Hash`/`NamedTuple` controller action return types ([#253]) (George Dietrich)\n- Fix [ATH::ParameterBag#get?](https://athenaframework.org/Framework/ParameterBag/#Athena::Framework::ParameterBag#get?(name,_type)) not returning `nil` if it could not convert the value to the desired type ([#243]) (George Dietrich)\n\n[0.18.0]: https://github.com/athena-framework/framework/releases/tag/v0.18.0\n[#205]: https://github.com/athena-framework/athena/pull/205\n[#213]: https://github.com/athena-framework/athena/pull/213\n[#218]: https://github.com/athena-framework/athena/pull/218\n[#224]: https://github.com/athena-framework/athena/pull/224\n[#241]: https://github.com/athena-framework/athena/pull/241\n[#243]: https://github.com/athena-framework/athena/pull/243\n[#249]: https://github.com/athena-framework/athena/pull/249\n[#250]: https://github.com/athena-framework/athena/pull/250\n[#253]: https://github.com/athena-framework/athena/pull/253\n[#264]: https://github.com/athena-framework/athena/pull/264\n[#265]: https://github.com/athena-framework/athena/pull/265\n[#267]: https://github.com/athena-framework/athena/pull/267\n\n## [0.17.1] - 2022-09-05\n\n### Changed\n\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n[0.17.1]: https://github.com/athena-framework/framework/releases/tag/v0.17.1\n[#188]: https://github.com/athena-framework/athena/pull/188\n\n## [0.17.0] - 2022-05-14\n\n_Checkout [this](https://forum.crystal-lang.org/t/athena-0-17-0/4624) forum thread for an overview of changes within the ecosystem._\n\n### Added\n\n- Add `pcre2` library dependency to `shard.yml` ([#159]) (George Dietrich)\n- Add [ATH::Arguments::Resolvers::Enum](https://athenaframework.org/Framework/Arguments/Resolvers/Enum/) to allow resolving `Enum` members directly to controller actions ([#173]) (George Dietrich)\n- Add [ATH::Arguments::Resolvers::UUID](https://athenaframework.org/Framework/Arguments/Resolvers/UUID/) to allow resolving `UUID`s directly to controller actions by ([#176]) (George Dietrich)\n- Add [ATH::ParameterBag#has(name, type)](https://athenaframework.org/Framework/ParameterBag/#Athena::Framework::ParameterBag#has?(name,type)) that checks if a parameter with the provided name exists, and that is of the provided type ([#176]) (George Dietrich)\n- Add [ATH::Arguments::Resolvers::DefaultValue](https://athenaframework.org/Framework/Arguments/Resolvers/DefaultValue/) to allow resolving an action parameter's default value if no other value was provided ([#177]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** rename `ATH::Arguments::Resolvers::ArgumentValueResolverInterface` to `ATH::Arguments::Resolvers::Interface` ([#176]) (George Dietrich)\n- **Breaking:** bump `athena-framework/serializer` to `~> 0.3.0` ([#181]) (George Dietrich)\n- **Breaking:** bump `athena-framework/validator` to `~> 0.2.0` ([#181]) (George Dietrich)\n- Expose the default value of an [ATH::Arguments::ArgumentMetadata](https://athenaframework.org/Framework/Arguments/ArgumentMetadata/) ([#176]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Fix error when two controller share a common action name ([#146]) (George Dietrich)\n- Fix release badge to use correct repo ([#161]) (George Dietrich)\n- Fix query/request param docs to use new error responses ([#167]) (George Dietrich)\n- Fix incorrect `Athena::Framework` `Log` name ([#175]) (George Dietrich)\n\n[0.17.0]: https://github.com/athena-framework/framework/releases/tag/v0.17.0\n[#146]: https://github.com/athena-framework/athena/pull/146\n[#159]: https://github.com/athena-framework/athena/pull/159\n[#161]: https://github.com/athena-framework/athena/pull/161\n[#167]: https://github.com/athena-framework/athena/pull/167\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#173]: https://github.com/athena-framework/athena/pull/173\n[#175]: https://github.com/athena-framework/athena/pull/175\n[#176]: https://github.com/athena-framework/athena/pull/176\n[#177]: https://github.com/athena-framework/athena/pull/177\n[#181]: https://github.com/athena-framework/athena/pull/181\n\n## [0.16.0] - 2022-01-22\n\n_First release in the [athena-framework/framework](https://github.com/athena-framework/framework) repo, post monorepo._\n\n### Added\n\n- Add dependency on `athena-framework/routing` ([#141]) (George Dietrich)\n- Allow prepending [HTTP::Handlers](https://crystal-lang.org/api/HTTP/Handler.html) to the Athena server ([#133]) (George Dietrich)\n- Add common HTTP methods (get, post, put, delete) to [ATH::Spec::APITestCase](https://athenaframework.org//Framework/Spec/APITestCase/#Athena::Framework::Spec::APITestCase-methods) ([#134]) (George Dietrich)\n- Add overload of [ATH::Spec::APITestCase#request](https://athenaframework.org/Framework/Spec/APITestCase/#Athena::Framework::Spec::APITestCase#request(method,path,body,headers)) that accepts an [ATH::Request](https://athenaframework.org/Framework/Request/) or [HTTP::Request](https://crystal-lang.org/api/HTTP/Request.html) ([#134]) (George Dietrich)\n- Allow running an HTTPS server via passing an [OpenSSL::SSL::Context::Server](https://crystal-lang.org/api/OpenSSL/SSL/Context/Server.html) to `ATH.run` ([#135], [#136]) (George Dietrich)\n- Add [ATH::ParameterBag#set(hash)](https://athenaframework.org/Framework/ParameterBag/#Athena::Framework::ParameterBag#set(name,value,type)) that allows setting a hash of key/value pairs ([#141]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** integrate the [Athena::Routing](https://athenaframework.org/Routing/) component ([#141]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** remove dependency on [amberframework/amber-router](https://github.com/amberframework/amber-router) ([#141]) (George Dietrich)\n\n[0.16.0]: https://github.com/athena-framework/framework/releases/tag/v0.16.0\n[#133]: https://github.com/athena-framework/athena/pull/133\n[#134]: https://github.com/athena-framework/athena/pull/134\n[#135]: https://github.com/athena-framework/athena/pull/135\n[#136]: https://github.com/athena-framework/athena/pull/136\n[#141]: https://github.com/athena-framework/athena/pull/141\n\n## [0.15.1] - 2021-12-13\n\n### Changed\n\n- Include error list in `ATH::Exception::InvalidParameter` ([#124]) (George Dietrich)\n- Set the base path of parameter errors to the name of the parameter ([#124]) (George Dietrich)\n\n[0.15.1]: https://github.com/athena-framework/athena/releases/tag/v0.15.1\n[#124]: https://github.com/athena-framework/athena/pull/124\n\n## [0.15.0] - 2021-10-30\n\n_Last release in the [athena-framework/athena](https://github.com/athena-framework/athena) repo, pre monorepo._\n\n### Added\n\n- Expose the raw [HTTP::Request](https://crystal-lang.org/api/HTTP/Request.html) method from an `ATH::Request` ([#115]) (George Dietrich)\n- Add built in [ATH::RequestBodyConverter](https://athenaframework.org/Framework/RequestBodyConverter) param converter ([#116]) (George Dietrich)\n- Add `VERSION` constant to `Athena::Framework` namespace ([#120]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** rename base param converter type to `ATH::ParamConverter` and make it a class ([#116]) (George Dietrich)\n- **Breaking:** rename the component from `Athena::Routing` to `Athena::Framework` ([#120]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect parameter type restriction on `ATH::ParameterBag#set` ([#116]) (George Dietrich)\n- Fix incorrect ivar type on `AVD::Exception::Exceptions::ValidationFailed#violations` ([#116]) (George Dietrich)\n- Correctly reject requests with whitespace when converting numeric inputs ([#117]) (George Dietrich)\n\n[0.15.0]: https://github.com/athena-framework/athena/releases/tag/v0.15.0\n[#115]: https://github.com/athena-framework/athena/pull/115\n[#116]: https://github.com/athena-framework/athena/pull/116\n[#117]: https://github.com/athena-framework/athena/pull/117\n[#120]: https://github.com/athena-framework/athena/pull/120\n\n"
  },
  {
    "path": ".changes/framework/v0.21.0.md",
    "content": "## [0.21.0] - 2025-09-04\n\n### Changed\n\n- **Breaking:** Leverage `ATH::AbstractFile` within `ATH::BinaryFileResponse` ([#563]) (George Dietrich) <!-- blacksmoke16 -->\n- Leverage `mime` component within `ATH::BinaryFileResponse` ([#545]) (George Dietrich) <!-- blacksmoke16 -->\n- Setter methods on `ATH::Response` and subclasses now return `self` to better support method chaining ([#563]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Add support for Athena Contract component types ([#544]) (George Dietrich) <!-- blacksmoke16 -->\n- Add native file upload support ([#559]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Correctly apply `emit_nil` value from `ATHA::View` ([#526]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.21.0]: https://github.com/athena-framework/framework/releases/tag/v0.21.0\n[#545]: https://github.com/athena-framework/athena/pull/545\n[#563]: https://github.com/athena-framework/athena/pull/563\n[#544]: https://github.com/athena-framework/athena/pull/544\n[#559]: https://github.com/athena-framework/athena/pull/559\n[#526]: https://github.com/athena-framework/athena/pull/526\n"
  },
  {
    "path": ".changes/framework/v0.21.1.md",
    "content": "## [0.21.1] - 2025-10-04\n\n### Fixed\n\n- Fix improper handling of optional file uploads ([#595]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.21.1]: https://github.com/athena-framework/framework/releases/tag/v0.21.1\n[#595]: https://github.com/athena-framework/athena/pull/595\n"
  },
  {
    "path": ".changes/framework/v0.22.0.md",
    "content": "## [0.22.0] - 2026-04-19\n\n### Changed\n\n- **Breaking:** Store `ATH::Action` within `ATH::Request#attributes` instead of within an ivar ([#636]) (George Dietrich) <!-- blacksmoke16 -->\n- **Breaking:** Extract out HTTP related `framework` types into the new `http` component ([#640]) (George Dietrich) <!-- blacksmoke16 -->\n- **Breaking:** Extract out Request/Response handling related `framework` types into the new `http_kernel` component ([#657]) (George Dietrich) <!-- blacksmoke16 -->\n- **Breaking:** Refactor how annotations are fetched off an action/parameter ([#655]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix CORS error when using HTTP/2 but providing uppercase header names ([#670]) (George Dietrich) <!-- blackmsoke16 -->\n- Fix compile time error when inadvertently using a type name that conflicts with an internal component type ([#678]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.22.0]: https://github.com/athena-framework/framework/releases/tag/v0.22.0\n[#636]: https://github.com/athena-framework/athena/pull/636\n[#640]: https://github.com/athena-framework/athena/pull/640\n[#657]: https://github.com/athena-framework/athena/pull/657\n[#655]: https://github.com/athena-framework/athena/pull/655\n[#670]: https://github.com/athena-framework/athena/pull/670\n[#678]: https://github.com/athena-framework/athena/pull/678\n"
  },
  {
    "path": ".changes/header.tpl.md",
    "content": "# Changelog\n"
  },
  {
    "path": ".changes/http/v0.1.0.md",
    "content": "## [0.1.0] - 2026-04-19\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/http/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/http-kernel/v0.1.0.md",
    "content": "## [0.1.0] - 2026-04-19\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/http-kernel/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/image-size/v0.1.4.md",
    "content": "## [0.1.4] - 2025-01-26\n\n_Administrative release, no functional changes_\n\n[0.1.4]: https://github.com/athena-framework/image-size/releases/tag/v0.1.4\n\n## [0.1.3] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/image-size/releases/tag/v0.1.3\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.1.2] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.1.2]: https://github.com/athena-framework/image-size/releases/tag/v0.1.2\n\n## [0.1.1] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect `description` key in `shard.yml` ([#171]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/image-size/releases/tag/v0.1.1\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#171]: https://github.com/athena-framework/athena/pull/171\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.0] - 2022-02-21\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/image-size/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/mercure/v0.1.0.md",
    "content": "## [0.1.0] - 2026-04-19\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/mercure/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/mercure-bundle/v0.1.0.md",
    "content": "## [0.1.0] - 2026-04-19\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/mercure-bundle/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/mime/v0.2.0.md",
    "content": "## [0.2.0] - 2025-05-14\n\n### Added\n\n- **Breaking:** Add `AMIME::Types` to more robustly handles MIME type/file extension/guessing ([#534]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/mime/releases/tag/v0.2.0\n[#534]: https://github.com/athena-framework/athena/pull/534\n\n## [0.1.0] - 2025-01-26\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/mime/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/mime/v0.2.1.md",
    "content": "## [0.2.1] - 2025-09-04\n\n### Added\n\n- Add fallback MIME types guesser based on stdlib `MIME` module ([#546]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.2.1]: https://github.com/athena-framework/mime/releases/tag/v0.2.1\n[#546]: https://github.com/athena-framework/athena/pull/546\n"
  },
  {
    "path": ".changes/negotiation/v0.2.0.md",
    "content": "## [0.2.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n- Use lowercase `utf-8` within header values ([#417]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.13.0` ([#428]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/negotiation/releases/tag/v0.2.0\n[#417]: https://github.com/athena-framework/athena/pull/417\n[#428]: https://github.com/athena-framework/athena/pull/428\n\n## [0.1.5] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.1.5]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.5\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.1.4] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.1.4]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.4\n\n## [0.1.3] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.3\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.1.2] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Added\n\n- Add `VERSION` constant to `Athena::Negotiation` namespace ([#166]) (George Dietrich)\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Correct the shard version in `README.md` ([#6]) (syeopite)\n\n[0.1.2]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.2\n[#6]: https://github.com/athena-framework/negotiation/pull/6\n[#166]: https://github.com/athena-framework/athena/pull/166\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.1] - 2021-02-04\n\n### Changed\n\n- Migrate documentation to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#4]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.1\n[#4]: https://github.com/athena-framework/negotiation/pull/4\n\n## [0.1.0] - 2020-12-24\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/routing/v0.1.10.md",
    "content": "## [0.1.10] - 2025-01-26\n\n### Changed\n\n- Allow having multiple independent compiled route collections ([#468]) (George Dietrich)\n- Log unhandled `ART::RoutingHandler` exceptions ([#470]) (George Dietrich)\n\n### Fixed\n\n- Make `ART::RequestContext.from_uri` more robust ([#498]) (George Dietrich)\n\n[0.1.10]: https://github.com/athena-framework/routing/releases/tag/v0.1.10\n[#468]: https://github.com/athena-framework/athena/pull/468\n[#470]: https://github.com/athena-framework/athena/pull/470\n[#498]: https://github.com/athena-framework/athena/pull/498\n\n## [0.1.9] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- **Breaking:** add kwargs overload to `ART::Generator::Interface#generate` ([#375]) (George Dietrich)\n\n### Fixed\n\n- Fix compatibility with PCRE2 10.43 ([#362]) (George Dietrich)\n- Fix error when PCRE2 JIT mode is unavailable ([#381]) (George Dietrich)\n\n[0.1.9]: https://github.com/athena-framework/routing/releases/tag/v0.1.9\n[#362]: https://github.com/athena-framework/athena/pull/362\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#375]: https://github.com/athena-framework/athena/pull/375\n[#381]: https://github.com/athena-framework/athena/pull/381\n\n## [0.1.8] - 2023-10-09\n\n### Added\n\n- Internal support for redirecting within an `ART::Matcher::*` ([#307]) (George Dietrich)\n\n[0.1.8]: https://github.com/athena-framework/routing/releases/tag/v0.1.8\n[#307]: https://github.com/athena-framework/athena/pull/307\n\n## [0.1.7] - 2023-05-29\n\n### Changed\n\n- **Breaking:** Update minimum `crystal` version to `~> 1.8.0`. Drop support for `PCRE1`. ([#281]) (George Dietrich)\n\n[0.1.7]: https://github.com/athena-framework/routing/releases/tag/v0.1.7\n[#281]: https://github.com/athena-framework/athena/pull/281\n\n## [0.1.6] - 2023-03-26\n\n### Fixed\n\n- Fix compatibility with Crystal `1.8.0-dev` ([#272]) (George Dietrich)\n\n[0.1.6]: https://github.com/athena-framework/routing/releases/tag/v0.1.6\n[#272]: https://github.com/athena-framework/athena/pull/272\n\n## [0.1.5] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n### Added\n\n- Add additional `ART::Requirement` constants ([#257]) (George Dietrich)\n\n### Fixed\n\n- Fix formatting issue in Crystal `1.8-dev` ([#258]) (George Dietrich)\n\n[0.1.5]: https://github.com/athena-framework/routing/releases/tag/v0.1.5\n[#257]: https://github.com/athena-framework/athena/pull/257\n[#258]: https://github.com/athena-framework/athena/pull/258\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.1.4] - 2023-01-07\n\n### Changed\n\n- Change route compilation to be eager ([#207]) (George Dietrich)\n\n### Added\n\n- Add ability to bubble up exceptions from `ART::RoutingHandler` ([#206]) (George Dietrich)\n- Add `ART::Matcher::TraceableURLMatcher` to help with debugging route matches ([#224]) (George Dietrich)\n- Add `ART::Route#has_scheme?` ([#224]) (George Dietrich)\n\n[0.1.4]: https://github.com/athena-framework/routing/releases/tag/v0.1.4\n[#207]: https://github.com/athena-framework/athena/pull/207\n[#206]: https://github.com/athena-framework/athena/pull/206\n[#224]: https://github.com/athena-framework/athena/pull/224\n\n## [0.1.3] - 2022-09-05\n\n### Changed\n\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n### Added\n\n- Add an `HTTP::Handler` to add basic routing support to a `HTTP::Server` ([#189]) (George Dietrich)\n\n### Fixed\n\n- Fixed slash characters being double escaped in generated URL query params ([#180]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/routing/releases/tag/v0.1.3\n[#180]: https://github.com/athena-framework/athena/pull/180\n[#188]: https://github.com/athena-framework/athena/pull/188\n[#189]: https://github.com/athena-framework/athena/pull/189\n\n## [0.1.2] - 2022-05-14\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n- Add common route requirement constants to the [ART::Requirement](https://athenaframework.org/Routing/Requirement/) namespace ([#173]) (George Dietrich)\n- Add [ART::Requirement::Enum](https://athenaframework.org/Routing/Requirement/Enum/) to make creating [Enum](https://crystal-lang.org/api/Enum.html) based route requirements easier ([#173]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/routing/releases/tag/v0.1.2\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n[#173]: https://github.com/athena-framework/athena/pull/173\n\n## [0.1.1] - 2022-02-05\n\n_First release a part of the monorepo._\n\n### Fixed\n\n- Fix erroneous mutating of matched route data ([#144]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/routing/releases/tag/v0.1.1\n[#144]: https://github.com/athena-framework/athena/pull/144\n\n## [0.1.0] - 2022-01-10\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/routing/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/routing/v0.1.11.md",
    "content": "## [0.1.11] - 2025-09-04\n\n### Fixed\n\n- Fix linker warning due to duplicate `pcre2-8` linkage ([#560]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.1.11]: https://github.com/athena-framework/routing/releases/tag/v0.1.11\n[#560]: https://github.com/athena-framework/athena/pull/560\n"
  },
  {
    "path": ".changes/routing/v0.1.12.md",
    "content": "## [0.1.12] - 2025-11-01\n\n### Fixed\n\n- Fix Crystal `1.19` incompatibility ([#600]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.1.12]: https://github.com/athena-framework/routing/releases/tag/v0.1.12\n[#600]: https://github.com/athena-framework/athena/pull/600\n"
  },
  {
    "path": ".changes/routing/v0.2.0.md",
    "content": "## [0.2.0] - 2026-04-19\n\n### Changed\n\n- **Breaking:** Change `ART::Route#defaults` and matched route parameters return type to `ART::Parameters` ([#652]) (George Dietrich) <!-- blacksmoke16 -->\n- **Breaking:** Loosen the type restriction for the `params` parameter of `ART::Generator::Interface#generate` ([#669]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Allow query-specific parameters within `ART::Generator::URLGenerator` via special `_query` parameter ([#669]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.2.0]: https://github.com/athena-framework/routing/releases/tag/v0.2.0\n[#652]: https://github.com/athena-framework/athena/pull/652\n[#669]: https://github.com/athena-framework/athena/pull/669\n"
  },
  {
    "path": ".changes/serializer/v0.4.1.md",
    "content": "## [0.4.1] - 2025-02-08\n\n### Fixed\n\n- Fix serialization of value when its type is different type than the ivar ([#514]) (George Dietrich)\n\n[0.4.1]: https://github.com/athena-framework/serializer/releases/tag/v0.4.1\n[#514]: https://github.com/athena-framework/athena/pull/514\n\n## [0.4.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.13.0` ([#428]) (George Dietrich)\n\n[0.4.0]: https://github.com/athena-framework/serializer/releases/tag/v0.4.0\n[#428]: https://github.com/athena-framework/athena/pull/428\n\n## [0.3.6] - 2024-04-27\n\n### Fixed\n\n- Fix misnamed modules being defined in incorrect namespace ([#402]) (George Dietrich)\n\n[0.3.6]: https://github.com/athena-framework/serializer/releases/tag/v0.3.6\n[#402]: https://github.com/athena-framework/athena/pull/402\n\n## [0.3.5] - 2024-04-09\n\n### Changed\n\n- Change `Config` dependency to `DependencyInjection` for the custom annotation feature ([#392]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.3.5]: https://github.com/athena-framework/serializer/releases/tag/v0.3.5\n[#392]: https://github.com/athena-framework/athena/pull/392\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.3.4] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.3.4]: https://github.com/athena-framework/serializer/releases/tag/v0.3.4\n\n## [0.3.3] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/serializer/releases/tag/v0.3.3\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.3.2] - 2023-01-07\n\n### Fixed\n\n- Fix deserializing `JSON::Any` and `YAML::Any` ([#215]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/serializer/releases/tag/v0.3.2\n[#215]: https://github.com/athena-framework/athena/pull/215\n\n## [0.3.1] - 2022-09-05\n\n### Changed\n\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/serializer/releases/tag/v0.3.1\n[#188]: https://github.com/athena-framework/athena/pull/188\n\n## [0.3.0] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** change serialization of [Enums](https://crystal-lang.org/api/Enum.html) to underscored strings by default ([#173]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Fix compiler error when trying to deserialize a `Hash` ([#165]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/serializer/releases/tag/v0.3.0\n[#165]: https://github.com/athena-framework/athena/pull/165\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n[#173]: https://github.com/athena-framework/athena/pull/173\n\n## [0.2.10] - 2021-11-12\n\n### Fixed\n\n- Fix issue with empty YAML input ([#22]) (George Dietrich)\n\n[0.2.10]: https://github.com/athena-framework/serializer/releases/tag/v0.2.10\n[#22]: https://github.com/athena-framework/serializer/pull/22\n\n## [0.2.9] - 2021-10-30\n\n### Added\n\n- Add `VERSION` constant to `Athena::Serializer` namespace ([#20]) (George Dietrich)\n\n### Fixed\n\n- Fix broken type link ([#19]) (George Dietrich)\n\n[0.2.9]: https://github.com/athena-framework/serializer/releases/tag/v0.2.9\n[#19]: https://github.com/athena-framework/serializer/pull/19\n[#20]: https://github.com/athena-framework/serializer/pull/20\n\n## [0.2.8] - 2021-05-17\n\n### Fixed\n\n- Fixes incorrect `nil` check in macro logic ([#17]) (George Dietrich)\n\n[0.2.8]: https://github.com/athena-framework/serializer/releases/tag/v0.2.8\n[#17]: https://github.com/athena-framework/serializer/pull/17\n\n## [0.2.7] - 2021-04-09\n\n### Added\n\n- Add some more specialized exception types ([#16]) (George Dietrich)\n\n[0.2.7]: https://github.com/athena-framework/serializer/releases/tag/v0.2.7\n[#16]: https://github.com/athena-framework/serializer/pull/16\n\n## [0.2.6] - 2021-03-16\n\n### Added\n\n- Expose a setter for `ASR::Context#version=` ([#15]) (George Dietrich)\n\n### Changed\n\n- Change `athena-framework/config` version constraint to `>= 2.0.0` ([#15]) (George Dietrich)\n\n[0.2.6]: https://github.com/athena-framework/serializer/releases/tag/v0.2.6\n[#15]: https://github.com/athena-framework/serializer/pull/15\n\n## [0.2.5] - 2021-01-29\n\n### Changed\n\n- Migrate documentation to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#14]) (George Dietrich)\n\n[0.2.5]: https://github.com/athena-framework/serializer/releases/tag/v0.2.5\n[#14]: https://github.com/athena-framework/serializer/pull/14\n\n## [0.2.4] - 2021-01-29\n\n### Changed\n\n- Bump min `athena-framework/config` version to `~> 2.0.0` ([#13]) (George Dietrich)\n\n[0.2.4]: https://github.com/athena-framework/serializer/releases/tag/v0.2.4\n[#13]: https://github.com/athena-framework/serializer/pull/13\n\n## [0.2.3] - 2021-01-20\n\n### Fixed\n\n- Fix since/until and group annotations not working for virtual properties ([#12]) (George Dietrich)\n\n[0.2.3]: https://github.com/athena-framework/serializer/releases/tag/v0.2.3\n[#12]: https://github.com/athena-framework/serializer/pull/12\n\n## [0.2.2] - 2020-12-03\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#11]) (George Dietrich)\n\n[0.2.2]: https://github.com/athena-framework/serializer/releases/tag/v0.2.2\n[#11]: https://github.com/athena-framework/serializer/pull/11\n\n## [0.2.1] - 2020-11-08\n\n### Added\n\n- Add deserialization support to `ASRA::Name` ([#9]) (Joakim Repomaa)\n\n[0.2.1]: https://github.com/athena-framework/serializer/releases/tag/v0.2.1\n[#9]: https://github.com/athena-framework/serializer/pull/9\n\n## [0.2.0] - 2020-07-08\n\n### Added\n\n- Add dependency on `athena-framework/config` ([#8]) (George Dietrich)\n- Add ability to use custom annotations within [exclusion strategies](https://athenaframework.org/Serializer/ExclusionStrategies/ExclusionStrategyInterface/#Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface--annotation-configurations) ([#8]) (George Dietrich)\n- Add [ASR::Context#direction](https://athenaframework.org/Serializer/Context/#Athena::Serializer::Context#direction) to represent which direction the context object represents ([#8]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/serializer/releases/tag/v0.2.0\n[#8]: https://github.com/athena-framework/serializer/pull/8\n\n## [0.1.3] - 2020-07-08\n\n### Fixed\n\n- Fix overflow error when deserializing `Int64` values ([#7]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/serializer/releases/tag/v0.1.3\n[#7]: https://github.com/athena-framework/serializer/pull/7\n\n## [0.1.2] - 2020-07-05\n\n### Added\n\n- Add improved documentation to various types ([#6]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/serializer/releases/tag/v0.1.2\n[#6]: https://github.com/athena-framework/serializer/pull/6\n\n## [0.1.1] - 2020-06-27\n\n### Added\n\n- Add [naming strategies](https://athenaframework.org/Serializer/Annotations/Name/#Athena::Serializer::Annotations::Name--naming-strategies) to `ASRA::Name` ([#5]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/serializer/releases/tag/v0.1.1\n[#5]: https://github.com/athena-framework/serializer/pull/5\n\n## [0.1.0] - 2020-06-23\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/serializer/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/serializer/v0.4.2.md",
    "content": "## [0.4.2] - 2025-08-12\n\n### Fixed\n\n- Fix nightly type incompatibility with `ASR::Any` ([#562]) (George Dietrich)\n\n[0.4.2]: https://github.com/athena-framework/serializer/releases/tag/v0.4.2\n[#562]: https://github.com/athena-framework/athena/pull/562\n"
  },
  {
    "path": ".changes/serializer/v0.4.3.md",
    "content": "## [0.4.3] - 2026-04-19\n\n### Changed\n\n- Improve compile time error messages ([#646]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Removed\n\n- Remove `ASR::PropertyMetadata#class` method and generic variable ([#672]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.3]: https://github.com/athena-framework/serializer/releases/tag/v0.4.3\n[#646]: https://github.com/athena-framework/athena/pull/646\n[#672]: https://github.com/athena-framework/athena/pull/672\n"
  },
  {
    "path": ".changes/spec/v0.3.11.md",
    "content": "## [0.3.11] - 2025-05-19\n\n### Fixed\n\n- Fix duplicate test case runs with abstract generic parent test case ([#538]) (George Dietrich)\n\n[0.3.11]: https://github.com/athena-framework/spec/releases/tag/v0.3.11\n[#538]: https://github.com/athena-framework/athena/pull/538\n\n## [0.3.10] - 2025-02-08\n\n### Changed\n\n- **Breaking:** prevent defining `ASPEC::TestCase#initialize` methods that accepts arguments/blocks ([#516]) (George Dietrich)\n\n[0.3.10]: https://github.com/athena-framework/spec/releases/tag/v0.3.10\n[#516]: https://github.com/athena-framework/athena/pull/516\n\n## [0.3.9] - 2025-01-26\n\n_Administrative release, no functional changes_\n\n[0.3.9]: https://github.com/athena-framework/spec/releases/tag/v0.3.9\n\n## [0.3.8] - 2024-07-31\n\n### Added\n\n- Add support for using the `CRYSTAL` ENV var to customize binary used for `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` ([#424]) (George Dietrich)\n\n[0.3.8]: https://github.com/athena-framework/spec/releases/tag/v0.3.8\n[#424]: https://github.com/athena-framework/athena/pull/424\n\n## [0.3.7] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.3.7]: https://github.com/athena-framework/spec/releases/tag/v0.3.7\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.3.6] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.3.6]: https://github.com/athena-framework/spec/releases/tag/v0.3.6\n\n## [0.3.5] - 2023-04-26\n\n### Fixed\n\n- Ensure `#before_all` runs exactly once, and before `#initialize` ([#285]) (George Dietrich)\n\n[0.3.5]: https://github.com/athena-framework/spec/releases/tag/v0.3.5\n[#285]: https://github.com/athena-framework/athena/pull/285\n\n## [0.3.4] - 2023-03-19\n\n### Fixed\n\n- Fix exceptions not being counted as errors when raised within the `initialize` method of a test case ([#276]) (George Dietrich)\n- Fix a documentation typo in the `TestWith` example ([#269]) (George Dietrich)\n\n[0.3.4]: https://github.com/athena-framework/spec/releases/tag/v0.3.4\n[#269]: https://github.com/athena-framework/athena/pull/269\n[#276]: https://github.com/athena-framework/athena/pull/276\n\n## [0.3.3] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/spec/releases/tag/v0.3.3\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.3.2] - 2023-01-16\n\n### Added\n\n- Add `ASPEC::TestCase::TestWith` that works similar to the `ASPEC::TestCase::DataProvider` but without needing to create a dedicated method ([#254]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/spec/releases/tag/v0.3.2\n[#254]: https://github.com/athena-framework/athena/pull/254\n\n## [0.3.1] - 2023-01-07\n\n### Changed\n\n- Update the docs to clarify the component needs to be manually installed ([#247]) (George Dietrich)\n\n### Added\n\n- Add support for *codegen* for the `ASPEC.assert_error` and `ASPEC.assert_success` methods ([#219]) (George Dietrich)\n- Add ability to skip running all examples within a test case via the `ASPEC::TestCase::Skip` annotation ([#248]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/spec/releases/tag/v0.3.1\n[#219]: https://github.com/athena-framework/athena/pull/219\n[#247]: https://github.com/athena-framework/athena/pull/247\n[#248]: https://github.com/athena-framework/athena/pull/248\n\n## [0.3.0] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Changed\n\n- **Breaking:** change the `assert_error` to no longer be file based. Code should now be provided as a HEREDOC argument to the method ([#173]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Added\n\n- Add `VERSION` constant to `Athena::Spec` namespace ([#166]) (George Dietrich)\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n- Add [ASPEC::Methods.assert_success](https://athenaframework.org/Spec/Methods/#Athena::Spec::Methods#assert_success(code,*,line,file)) ([#173]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/spec/releases/tag/v0.3.0\n[#166]: https://github.com/athena-framework/athena/pull/166\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n[#173]: https://github.com/athena-framework/athena/pull/173\n\n## [0.2.6] - 2021-11-03\n\n### Fixed\n\n- Fix `test` helper macro generating invalid method names by replacing all non alphanumeric chars with `_`  ([#12]) (George Dietrich)\n\n[0.2.6]: https://github.com/athena-framework/spec/releases/tag/v0.2.6\n[#12]: https://github.com/athena-framework/spec/pull/12\n\n## [0.2.5] - 2021-11-03\n\n### Fixed\n\n- Fix `test` helper macro not actually calling `yield`  ([#11]) (George Dietrich)\n\n[0.2.5]: https://github.com/athena-framework/spec/releases/tag/v0.2.5\n[#11]: https://github.com/athena-framework/spec/pull/11\n\n## [0.2.4] - 2021-01-29\n\n### Changed\n\n- Finish migration to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#9]) (George Dietrich)\n\n[0.2.4]: https://github.com/athena-framework/spec/releases/tag/v0.2.4\n[#9]: https://github.com/athena-framework/spec/pull/9\n\n## [0.2.3] - 2020-12-03\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#7]) (George Dietrich)\n\n[0.2.3]: https://github.com/athena-framework/spec/releases/tag/v0.2.3\n[#7]: https://github.com/athena-framework/spec/pull/7\n\n## [0.2.2] - 2020-10-02\n\n### Added\n\n- Add support for data providers defined in parent types ([#6]) (George Dietrich)\n\n[0.2.2]: https://github.com/athena-framework/spec/releases/tag/v0.2.2\n[#6]: https://github.com/athena-framework/spec/pull/6\n\n## [0.2.1] - 2020-09-25\n\n### Changed\n\n- Changed data provider generated `it` blocks have proper file names and line numbers ([#4]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/spec/releases/tag/v0.2.1\n[#4]: https://github.com/athena-framework/spec/pull/4\n\n## [0.2.0] - 2020-08-08\n\n### Changed\n\n- **Breaking:** require [data providers](https://athenaframework.org/Spec/TestCase/DataProvider/) methods to declare a return type of `Hash`, `NamedTuple`, `Tuple`, or `Array` ([#3]) (George Dietrich)\n- Changed data provider generated `it` blocks to include the key/index ([#2]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/spec/releases/tag/v0.2.0\n[#2]: https://github.com/athena-framework/spec/pull/2\n[#3]: https://github.com/athena-framework/spec/pull/3\n\n## [0.1.0] - 2020-08-06\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/spec/releases/tag/v0.1.0\n"
  },
  {
    "path": ".changes/spec/v0.4.0.md",
    "content": "## [0.4.0] - 2025-09-04\n\n### Added\n\n- Add support for generating macro code coverage reports for `.assert_error` and `.assert_compiles` methods ([#551]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Removed\n\n- Remove `codegen` parameter from `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` ([#551]) (George Dietrich) <!-- blacksmoke16 -->\n- Remove `ASPEC::Methods.assert_error` in favor of `ASPEC::Methods.assert_compile_time_error` and `ASPEC::Methods.assert_runtime_error` ([#551]) (George Dietrich) <!-- blacksmoke16 -->\n- Remove `ASPEC::Methods.assert_success` in favor of `ASPEC::Methods.assert_compiles` and `ASPEC::Methods.assert_executes` ([#551]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.0]: https://github.com/athena-framework/spec/releases/tag/v0.4.0\n[#551]: https://github.com/athena-framework/athena/pull/551\n"
  },
  {
    "path": ".changes/spec/v0.4.1.md",
    "content": "## [0.4.1] - 2025-11-12\n\n### Fixed\n\n- Fix segfault when interacting with a test case ivar object's ivar that was left uninitialized due to an exception in its initializer, within the `tear_down` method ([#613]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.1]: https://github.com/athena-framework/spec/releases/tag/v0.4.1\n[#613]: https://github.com/athena-framework/athena/pull/613\n"
  },
  {
    "path": ".changes/spec/v0.4.2.md",
    "content": "## [0.4.2] - 2026-04-19\n\n### Added\n\n- Generate macro code coverage report for `ASPEC::Methods.assert_compiles` ([#642]) (George Dietrich) <!-- blacksmoke16 -->\n- Add `ASPEC.compile_time_assert` helper function for use with `assert_compiles` ([#686]) (George Dietrich) <!-- blacksmoke16 -->\n- Add ability to add code before/after the actual code of `ASPEC::Methods.assert_compiles` and `ASPEC::Methods.assert_compile_time_error` ([#687]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix compile time error when inadvertently using a type name that conflicts with an internal component type ([#678]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix incorrect macro code coverage line numbers ([#686]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix macro code coverage output file writing on windows ([#696]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.2]: https://github.com/athena-framework/spec/releases/tag/v0.4.2\n[#642]: https://github.com/athena-framework/athena/pull/642\n[#686]: https://github.com/athena-framework/athena/pull/686\n[#687]: https://github.com/athena-framework/athena/pull/687\n[#678]: https://github.com/athena-framework/athena/pull/678\n[#696]: https://github.com/athena-framework/athena/pull/696\n"
  },
  {
    "path": ".changes/unreleased/.gitkeep",
    "content": ""
  },
  {
    "path": ".changes/unreleased/event-dispatcher-Changed-20260502-225424.yaml",
    "content": "project: event-dispatcher\nkind: Changed\nbody: Event listeners with a generic type parameter now match all subclasses and module includers of the parameter's type\ntime: 2026-05-02T22:54:24.690293195-04:00\ncustom:\n    Author: George Dietrich\n    Breaking: \"No\"\n    PR: \"703\"\n    Username: blacksmoke16\n"
  },
  {
    "path": ".changes/validator/v0.4.0.md",
    "content": "## [0.4.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n\n### Added\n\n- **Breaking:** Add and make `require_tld: true` the default for `AVD::Constraints::URL` ([#492]) (George Dietrich)\n- Add example usages to `AVD::Constraints::*` docs ([#483], [#493]) (Zohir Tamda, George Dietrich)\n\n[0.4.0]: https://github.com/athena-framework/validator/releases/tag/v0.4.0\n[#428]: https://github.com/athena-framework/athena/pull/428\n[#483]: https://github.com/athena-framework/athena/pull/483\n[#492]: https://github.com/athena-framework/athena/pull/492\n[#493]: https://github.com/athena-framework/athena/pull/493\n\n## [0.3.4] - 2024-07-31\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.13.0` ([#433]) (George Dietrich)\n\n[0.3.4]: https://github.com/athena-framework/validator/releases/tag/v0.3.4\n[#433]: https://github.com/athena-framework/athena/pull/433\n\n## [0.3.3] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/validator/releases/tag/v0.3.3\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.3.2] - 2023-10-09\n\n### Fixed\n\n- Fix compiler error when using a composite constraint with a single member and no `of AVD::Constraint` ([#292]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/validator/releases/tag/v0.3.2\n[#292]: https://github.com/athena-framework/athena/pull/292\n\n## [0.3.1] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n### Fixed\n\n- Fix issue when using `AVD::Metadata::GetterMetadata` with methods that have parameters ([#252]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/validator/releases/tag/v0.3.1\n[#252]: https://github.com/athena-framework/athena/pull/252\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.3.0] - 2023-01-07\n\n### Changed\n\n- **Breaking:** update default `AVD::Constraints::Email::Mode` to be `:html5` ([#230]) (George Dietrich)\n- Refactor `AVD::Constraints::IP` to use new dedicated `Socket::IPAddress` methods ([#205]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.6` ([#205]) (George Dietrich)\n\n### Added\n\n- Add `AVD::Constraints::Collection` ([#229]) (George Dietrich)\n- Add `AVD::Constraints::Existence`, `AVD::Constraints::Required`, and `AVD::Constraints::Optional` for use with the collection constraint ([#229]) (George Dietrich)\n- Add `AVD::Spec::ConstraintValidatorTestCase#expect_validate_value_at` to more easily handle validation of nested constraints ([#229]) (George Dietrich)\n- Add `AVD::Constraints::Email::Mode::HTML5_ALLOW_NO_TLD` that allows matching `HTML` input field validation exactly ([#231]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** remove `AVD::Constraints::Email::Mode::Loose` ([#230]) (George Dietrich)\n\n### Fixed\n\n- **Breaking:** fix spelling of `AVD::Constraints::ISSN#require_hyphen` parameter ([#222]) (George Dietrich)\n- Fix property path display issue with `Enumerable` objects ([#229]) (George Dietrich)\n- Fix `AVD::Constraints::Valid` constraints incorrectly being allowed within `AVD::Constraints::Composite` ([#229]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/validator/releases/tag/v0.3.0\n[#205]: https://github.com/athena-framework/athena/pull/205\n[#222]: https://github.com/athena-framework/athena/pull/222\n[#229]: https://github.com/athena-framework/athena/pull/229\n[#230]: https://github.com/athena-framework/athena/pull/230\n[#231]: https://github.com/athena-framework/athena/pull/231\n\n## [0.2.1] - 2022-09-05\n\n### Added\n\n- Add support for exclusive end support to `AVD::Constraints::Range` ([#184]) (George Dietrich)\n\n### Changed\n\n- Include allowed MIME types within `AVD::Constraints::Image` if they were customized ([#183]) (George Dietrich)\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n### Fixed\n\n- Fix some file size factorization edge cases in `AVD::Constraints::File` ([#182]) (George Dietrich)\n- Fix duplicating constraints due to Crystal generics bug ([#192]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/validator/releases/tag/v0.2.1\n[#182]: https://github.com/athena-framework/athena/pull/182\n[#183]: https://github.com/athena-framework/athena/pull/183\n[#184]: https://github.com/athena-framework/athena/pull/184\n[#188]: https://github.com/athena-framework/athena/pull/188\n[#192]: https://github.com/athena-framework/athena/pull/192\n\n## [0.2.0] - 2022-05-14\n\n### Added\n\n- Add the [AVD::Constraints::File](https://athenaframework.org/Validator/Constraints/File/) constraint ([#153]) (George Dietrich)\n- Allow `AVD::Spec::MockValidator` to dynamically configure returned violations ([#155], [#157]) (George Dietrich)\n- Add the [AVD::Constraints::Image](https://athenaframework.org/Validator/Constraints/Image/) constraint ([#153]) (George Dietrich)\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** make `AVD::ConstraintValidator` classes ([#154]) (George Dietrich)\n- **Breaking:** `AVD::ExecutionContext` is no longer a generic type ([#156]) (George Dietrich)\n- Update `assert_violation` to use a clearer failure message if no violations were found ([#153]) (George Dietrich)\n- Update `AVD::Constraints::ISIN` to use the validator off the context versus an ivar ([#155]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** removed `AVD::Spec::MockValidator#violations=` ([#155]) (George Dietrich)\n\n### Fixed\n\n- Fix `AVD::Violation::ConstraintViolation` not comparing correctly ([#153]) (George Dietrich)\n- Ensure only `Indexable` types can be used with `AVD::Constraints::Unique` ([#168]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/validator/releases/tag/v0.2.0\n[#153]: https://github.com/athena-framework/athena/pull/153\n[#154]: https://github.com/athena-framework/athena/pull/154\n[#155]: https://github.com/athena-framework/athena/pull/155\n[#156]: https://github.com/athena-framework/athena/pull/156\n[#157]: https://github.com/athena-framework/athena/pull/157\n[#168]: https://github.com/athena-framework/athena/pull/168\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.7] - 2021-12-27\n\n_First release a part of the monorepo._\n\n### Fixed\n\n- Fix callback constraint methods being incorrectly added as getters ([#132]) (George Dietrich)\n\n[0.1.7]: https://github.com/athena-framework/validator/releases/tag/v0.1.7\n[#132]: https://github.com/athena-framework/athena/pull/132\n\n## [0.1.6] - 2021-12-13\n\n### Fixed\n\n- Fix `AVD::Validatable` not working when included into parent types ([#16]) (George Dietrich)\n\n[0.1.6]: https://github.com/athena-framework/validator/releases/tag/v0.1.6\n[#16]: https://github.com/athena-framework/validator/pull/16\n\n## [0.1.5] - 2021-10-30\n\n### Added\n\n- Add `VERSION` constant to `Athena::Validator` namespace ([#12]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect type restriction on validator factory ([#12]) (George Dietrich)\n- Fix incorrect link within the docs ([#14]) (George Dietrich)\n\n[0.1.5]: https://github.com/athena-framework/validator/releases/tag/v0.1.5\n[#12]: https://github.com/athena-framework/validator/pull/12\n[#14]: https://github.com/athena-framework/validator/pull/14\n\n## [0.1.4] - 2021-01-30\n\n### Changed\n\n- Finish migration to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#10], [#11]) (George Dietrich)\n\n[0.1.4]: https://github.com/athena-framework/validator/releases/tag/v0.1.4\n[#10]: https://github.com/athena-framework/validator/pull/10\n[#11]: https://github.com/athena-framework/validator/pull/11\n\n## [0.1.3] - 2020-12-07\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#9]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/validator/releases/tag/v0.1.3\n[#9]: https://github.com/athena-framework/validator/pull/9\n\n## [0.1.2] - 2020-11-25\n\n### Added\n\n- Add the [AVD::Constraints::Choice](https://athenaframework.org/Validator/Constraints/Choice/) constraint ([#7]) (George Dietrich)\n\n### Changed\n\n- Allow setting violations directly on mock validators ([#7]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/validator/releases/tag/v0.1.2\n[#7]: https://github.com/athena-framework/validator/pull/7\n\n## [0.1.1] - 2020-11-08\n\n### Fixed\n\n- Fix compiler error due to less strict `abstract def` implementations ([#6]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/validator/releases/tag/v0.1.1\n[#6]: https://github.com/athena-framework/validator/pull/6\n\n## [0.1.0] - 2020-10-17\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/validator/releases/tag/v0.1.0\n\n"
  },
  {
    "path": ".changes/validator/v0.4.1.md",
    "content": "## [0.4.1] - 2025-09-04\n\n### Changed\n\n- Leverage `mime` component for more robust `AVD::Constraints::File` MIME type validation ([#545]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Add `AVD::Spec::CompoundConstraintTestCase` to make testing `AVD::Constraints::Compound` easier ([#540]) (George Dietrich) <!-- blacksmoke16 -->\n- Add support for `ATH::UploadedFile` to `AVD::Constraints::File` and `AVD::Constraints::Image` ([#559]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix equality between `AVD::Constraint` instances ([#540]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.1]: https://github.com/athena-framework/validator/releases/tag/v0.4.1\n[#545]: https://github.com/athena-framework/athena/pull/545\n[#540]: https://github.com/athena-framework/athena/pull/540\n[#559]: https://github.com/athena-framework/athena/pull/559\n"
  },
  {
    "path": ".changes/validator/v0.5.0.md",
    "content": "## [0.5.0] - 2026-04-19\n\n### Changed\n\n- **Breaking:** Split `AVD::Constraints::Size` into `Count` and `Length` constraints ([#611]) (George Dietrich) <!-- blacksmoke16 -->\n- Make identifying constraint violation inequality easier within spec failures ([#610]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Allow picking the unit used for `AVD::Constraints::Length` validations ([#612]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.5.0]: https://github.com/athena-framework/validator/releases/tag/v0.5.0\n[#610]: https://github.com/athena-framework/athena/pull/610\n[#611]: https://github.com/athena-framework/athena/pull/611\n[#612]: https://github.com/athena-framework/athena/pull/612\n"
  },
  {
    "path": ".changie.yaml",
    "content": "changesDir: .changes\nunreleasedDir: unreleased\nheaderPath: header.tpl.md\nchangelogPath: CHANGELOG.md\nversionExt: md\nversionFormat: '## [{{.VersionNoPrefix}}] - {{.Time.Format \"2006-01-02\"}}'\nkindFormat: '### {{.Kind}}'\nchangeFormat: '- {{.Body}} ([#{{.Custom.PR}}]) ({{.Custom.Author}}) <!-- {{.Custom.Username}} -->'\nfooterFormat: |\n  [{{.VersionNoPrefix}}]: https://github.com/athena-framework/{{ kebabcase (index .Changes 0).Project }}/releases/tag/{{.Version}}\n  {{- range (customs .Changes \"PR\" | uniq) }}\n  [#{{.}}]: https://github.com/athena-framework/athena/pull/{{.}}\n  {{- end}}\nprojectsVersionSeparator: '/'\nprojects:\n  # Bundles\n  - label: Mercure Bundle\n    key: mercure-bundle\n    changelog: src/bundles/mercure/CHANGELOG.md\n    replacements:\n      - { path: src/bundles/mercure/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/bundles/mercure/src/athena-mercure_bundle.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/mercure-bundle/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n\n  # Components\n  - label: Clock\n    key: clock\n    changelog: src/components/clock/CHANGELOG.md\n    replacements:\n      - { path: src/components/clock/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/clock/src/athena-clock.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/clock/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Console\n    key: console\n    changelog: src/components/console/CHANGELOG.md\n    replacements:\n      - { path: src/components/console/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/console/src/athena-console.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/console/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Contracts\n    key: contracts\n    changelog: src/components/contracts/CHANGELOG.md\n    replacements:\n      - { path: src/components/contracts/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/contracts/src/athena-contracts.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/contracts/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Dependency Injection\n    key: dependency-injection\n    changelog: src/components/dependency_injection/CHANGELOG.md\n    replacements:\n      - { path: src/components/dependency_injection/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/dependency_injection/src/athena-dependency_injection.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/dependency_injection/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Dotenv\n    key: dotenv\n    changelog: src/components/dotenv/CHANGELOG.md\n    replacements:\n      - { path: src/components/dotenv/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/dotenv/src/athena-dotenv.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/dotenv/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Event Dispatcher\n    key: event-dispatcher\n    changelog: src/components/event_dispatcher/CHANGELOG.md\n    replacements:\n      - { path: src/components/event_dispatcher/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/event_dispatcher/src/athena-event_dispatcher.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/event_dispatcher/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Framework\n    key: framework\n    changelog: src/components/framework/CHANGELOG.md\n    replacements:\n      - { path: src/components/framework/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/framework/src/athena.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: docs/getting_started/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: HTTP\n    key: http\n    changelog: src/components/http/CHANGELOG.md\n    replacements:\n      - { path: src/components/http/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/http/src/athena-http.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/http/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: HTTP Kernel\n    key: http-kernel\n    changelog: src/components/http_kernel/CHANGELOG.md\n    replacements:\n      - { path: src/components/http_kernel/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/http_kernel/src/athena-http_kernel.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/http_kernel/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Image Size\n    key: image-size\n    changelog: src/components/image_size/CHANGELOG.md\n    replacements:\n      - { path: src/components/image_size/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/image_size/src/athena-image_size.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/image_size/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Mercue\n    key: mercure\n    changelog: src/components/mercure/CHANGELOG.md\n    replacements:\n      - { path: src/components/mercure/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/mercure/src/athena-mercure.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/mercure/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: MIME\n    key: mime\n    changelog: src/components/mime/CHANGELOG.md\n    replacements:\n      - { path: src/components/mime/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/mime/src/athena-mime.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/mime/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Negotiation\n    key: negotiation\n    changelog: src/components/negotiation/CHANGELOG.md\n    replacements:\n      - { path: src/components/negotiation/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/negotiation/src/athena-negotiation.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/negotiation/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Routing\n    key: routing\n    changelog: src/components/routing/CHANGELOG.md\n    replacements:\n      - { path: src/components/routing/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/routing/src/athena-routing.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/routing/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Serializer\n    key: serializer\n    changelog: src/components/serializer/CHANGELOG.md\n    replacements:\n      - { path: src/components/serializer/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/serializer/src/athena-serializer.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/serializer/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Spec\n    key: spec\n    changelog: src/components/spec/CHANGELOG.md\n    replacements:\n      - { path: src/components/spec/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/spec/src/athena-spec.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/spec/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\n  - label: Validator\n    key: validator\n    changelog: src/components/validator/CHANGELOG.md\n    replacements:\n      - { path: src/components/validator/shard.yml, find: '^version: .*', replace: 'version: {{.VersionNoPrefix}}' }\n      - { path: src/components/validator/src/athena-validator.cr, find: '  VERSION = \".*\"', replace: '  VERSION = \"{{.VersionNoPrefix}}\"' }\n      - { path: src/components/validator/docs/README.md, find: '    version: ~> .*', replace: '    version: ~> {{.Major}}.{{.Minor}}.0' }\nkinds:\n  - label: Changed\n    changeFormat: '- {{if eq .Custom.Breaking \"Yes\"}}**Breaking:** {{end}}{{.Body}} ([#{{.Custom.PR}}]) ({{.Custom.Author}}) <!-- {{.Custom.Username}} -->'\n    additionalChoices:\n      - type: enum\n        key: Breaking\n        label: Is it a breaking change?\n        enumOptions:\n          - Yes\n          - No\n  - label: Added\n  - label: Removed\n  - label: Fixed\ncustom:\n  - key: PR\n    type: int\n    minInt: 0\n    optional: false\n  - key: Author\n    type: string\n    optional: false\n  - key: Username\n    label: Github Username\n    type: string\n    optional: false\nnewlines:\n  beforeChangelogVersion: 1\n  afterKind: 1\n  beforeKind: 1\n  beforeFooterTemplate: 1\nenvPrefix: CHANGIE_\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.cr text eol=lf\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\n\nupdates:\n  - package-ecosystem: 'github-actions'\n    directory: '/'\n    # Updates tend to be released at the beginning of the month, but not exactly on the first.\n    # Add some delay to be able to catpure them\n    schedule:\n      interval: 'cron'\n      cronjob: '0 0 7 * *'\n    labels:\n      - 'kind:infrastructure'\n      - 'kind:maintenance'\n  - package-ecosystem: 'uv'\n    directory: '/'\n    schedule:\n      interval: 'monthly'\n    labels:\n      - 'kind:documentation'\n      - 'kind:maintenance'\n    groups:\n      documentation:\n        patterns:\n          - '*'\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Context\n\n<!-- Take a minute to add some context for posterity as to what this PR is doing and why -->\n<!-- Can be skipped in favor of an issue reference (`Resolves #xxx`) if the related issue has all the context -->\n\n## Changelog\n\n<!-- List out the high level changes this PR makes, especially those that are breaking -->\n\n---\n\n_Before merging, remember to add the `athena-framework/athena` prefix to the PR number in the PR title_\n"
  },
  {
    "path": ".github/workflows/build_and_publish_docs.yml",
    "content": "on:\n  workflow_dispatch:\n    inputs:\n      branch:\n        description: 'Which Cloudflare Pages branch (master | dev) to deploy the docs to'\n        type: string\n        required: true\n  workflow_call:\n    inputs:\n      branch:\n        description: 'Which Cloudflare Pages branch (master | dev) to deploy the docs to'\n        type: string\n        required: true\n\njobs:\n  build-and-publish-docs:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0\n      - name: Install Crystal\n        uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1.9.2\n      - name: Install Crystal Shard Dependencies\n        run: shards install --without-development\n        env:\n          SHARDS_OVERRIDE: ${{ inputs.branch == 'dev' && 'shard.dev.yml' || 'shard.prod.yml' }}\n      - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.0.1\n      - name: Build Docs\n        run: just build-docs\n      - name: Publish to Cloudflare Pages\n        uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0\n        with:\n          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}\n          command: pages deploy ./site --project-name=athenaframework --branch=${{ inputs.branch }}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  merge_group:\n  push:\n    branches:\n      - master # Allows codecov to receive current HEAD information for each commit merged into master\n  pull_request:\n    branches:\n      - master\n  schedule:\n    - cron: '15 1 * * *' # Nightly at 01:15\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  check_spelling:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Check Spelling\n        uses: crate-ci/typos@bbaefadf97b0ec5fdc942684b647f1a6ab250274 # v1.46.0\n  check_format:\n    strategy:\n      fail-fast: false\n      matrix:\n        crystal:\n          - latest\n          - nightly\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0\n      - name: Install Crystal\n        uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1.9.2\n        with:\n          crystal: ${{ matrix.crystal }}\n      - name: Check Format\n        run: just format\n  coding_standards:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0\n      - name: Install Crystal\n        uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1.9.2\n      - name: Install Dependencies\n        run: shards install\n        env:\n          SHARDS_OVERRIDE: shard.dev.yml\n      - name: Ameba\n        run: just ameba\n  test_compiled:\n    strategy:\n      fail-fast: false\n      matrix:\n        crystal:\n          - latest\n          - nightly\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0\n      - name: Install kcov\n        if: ${{ matrix.crystal == vars.COVERAGE_CHANNEL }}\n        run: |\n          sudo apt-get update &&\n          sudo apt-get install binutils-dev libssl-dev libcurl4-openssl-dev libelf-dev libstdc++-12-dev zlib1g-dev libdw-dev libiberty-dev\n          curl -L -o ./kcov.tar.gz https://github.com/SimonKagstrom/kcov/archive/refs/tags/v43.tar.gz &&\n          mkdir kcov-source &&\n          tar xzf kcov.tar.gz -C kcov-source --strip-components=1 &&\n          cd kcov-source &&\n          mkdir build &&\n          cd build &&\n          cmake .. &&\n          make -j$(nproc) &&\n          sudo make install\n      - name: Install Crystal\n        uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1.9.2\n        with:\n          crystal: ${{ matrix.crystal }}\n      - name: Install System Dependencies\n        run: sudo apt-get update && sudo apt install -y libmagic-dev\n      - name: Install Dependencies\n        run: shards install --skip-postinstall --skip-executables\n        env:\n          SHARDS_OVERRIDE: shard.dev.yml\n      - name: Compiled Specs\n        run: just test-compiled\n        shell: bash\n      - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0\n        if: ${{ matrix.crystal == vars.COVERAGE_CHANNEL && github.event_name != 'schedule' }} # Only want to upload coverage report once in the matrix\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          fail_ci_if_error: true\n          directory: coverage\n          files: '**/cov.xml,**/macro_coverage.*.codecov.json' # There is no `unreachable.codecov.json` file when running _only_ compiled specs\n          verbose: true\n      - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0\n        if: ${{ matrix.crystal == vars.COVERAGE_CHANNEL && github.event_name != 'schedule' }} # Only want to upload coverage report once in the matrix\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          fail_ci_if_error: true\n          directory: coverage\n          files: '**/junit.xml'\n          verbose: true\n          report_type: test_results\n  test_unit:\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - ubuntu-latest\n          - ubuntu-24.04-arm\n          - macos-latest\n          - windows-latest\n        crystal:\n          - latest\n          - nightly\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        if: github.event_name != 'pull_request'\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        if: github.event_name == 'pull_request'\n        with:\n          fetch-depth: 0\n      - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0\n      - name: Install kcov\n        if: ${{ matrix.os == 'ubuntu-latest' && matrix.crystal == vars.COVERAGE_CHANNEL }}\n        run: |\n          sudo apt-get update &&\n          sudo apt-get install binutils-dev libssl-dev libcurl4-openssl-dev libelf-dev libstdc++-12-dev zlib1g-dev libdw-dev libiberty-dev\n          curl -L -o ./kcov.tar.gz https://github.com/SimonKagstrom/kcov/archive/refs/tags/v43.tar.gz &&\n          mkdir kcov-source &&\n          tar xzf kcov.tar.gz -C kcov-source --strip-components=1 &&\n          cd kcov-source &&\n          mkdir build &&\n          cd build &&\n          cmake .. &&\n          make -j$(nproc) &&\n          sudo make install\n      - name: Install Crystal\n        uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1.9.2\n        with:\n          crystal: ${{ matrix.crystal }}\n      - name: Install System Dependencies\n        if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm'\n        run: sudo apt-get update && sudo apt install -y libmagic-dev\n      - name: Install System Dependencies\n        if: matrix.os == 'macos-latest'\n        run: brew install libmagic\n      - name: Install Dependencies\n        run: shards install --skip-postinstall --skip-executables\n        env:\n          SHARDS_OVERRIDE: shard.dev.yml\n      - name: Specs\n        run: just test-unit\n        shell: bash\n      - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0\n        if: ${{ matrix.os == 'ubuntu-latest' && matrix.crystal == vars.COVERAGE_CHANNEL && github.event_name != 'schedule' }} # Only want to upload coverage report once in the matrix\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          fail_ci_if_error: true\n          directory: coverage\n          files: '**/cov.xml,**/unreachable.codecov.json'\n          verbose: true\n      - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0\n        if: ${{ matrix.os == 'ubuntu-latest' && matrix.crystal == vars.COVERAGE_CHANNEL && github.event_name != 'schedule' }} # Only want to upload coverage report once in the matrix\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          fail_ci_if_error: true\n          directory: coverage\n          files: '**/junit.xml'\n          verbose: true\n          report_type: test_results\n"
  },
  {
    "path": ".github/workflows/sync.yml",
    "content": "name: Sync\n\non:\n  push:\n    branches:\n      - master\n\nconcurrency:\n  group: ${{ github.workflow }}\n  cancel-in-progress: false # ensure back-to-back merges don't cancel an in-progress sync\n\njobs:\n  # Sync changes in the merged PR to the read-only mirror repos.\n  sync:\n    runs-on: ubuntu-latest\n    outputs:\n      updated_shards: ${{ steps.subtree-sync.outputs.updated_shards }}\n      kind_docs: ${{ steps.subtree-sync.outputs.kind_docs }}\n      kind_release: ${{ steps.subtree-sync.outputs.kind_release }}\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0 # Required so `git subtree` can find its split commit\n          ssh-key: ${{ secrets.ATHENA_BOT_SSH_PRIV_KEY }}\n\n      # Workaround for git 2.52.0 regression breaking subtree split with --squash\n      # See: https://lore.kernel.org/git/176677910605.6.2281395015810449820.1087545551@dietrich.pub/T/\n      - name: Cache Git installation\n        id: cache-git\n        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5\n        with:\n          path: ~/git-custom\n          key: git-2.51.1-subtree-${{ runner.os }}-${{ runner.arch }}\n      - name: Build Git with subtree support\n        if: steps.cache-git.outputs.cache-hit != 'true'\n        run: |\n          set -euo pipefail\n\n          GIT_VERSION=\"2.51.1\"\n          INSTALL_DIR=\"$HOME/git-custom\"\n\n          # Install build dependencies\n          sudo apt-get update\n          sudo apt-get install -y libcurl4-gnutls-dev libexpat1-dev gettext libz-dev libssl-dev\n\n          # Download and extract\n          wget -q \"https://mirrors.edge.kernel.org/pub/software/scm/git/git-${GIT_VERSION}.tar.gz\"\n          tar -xzf \"git-${GIT_VERSION}.tar.gz\"\n          cd \"git-${GIT_VERSION}\"\n\n          # Build and install git\n          make -j$(nproc) prefix=\"$INSTALL_DIR\" all\n          make prefix=\"$INSTALL_DIR\" install\n\n          # Build and install contrib/subtree\n          cd contrib/subtree\n          make prefix=\"$INSTALL_DIR\" install\n      - name: Add custom Git to PATH\n        run: echo \"$HOME/git-custom/bin\" >> \"$GITHUB_PATH\"\n      - name: Sync Shards\n        id: subtree-sync\n        env:\n          GH_TOKEN: ${{ github.token }}\n          BEFORE_SHA: ${{ github.event.before }}\n          AFTER_SHA: ${{ github.event.after }}\n        run: |\n          set -euo pipefail\n\n          UPDATED_SHARDS=()\n\n          for shardDir in $(find src/ -maxdepth 3 -type f -name shard.yml | xargs -I{} dirname {} | sort); do\n            # The git repos uses hyphens instead of underscores.\n            REPO_NAME=$(basename \"$shardDir\" | tr '_' '-')\n            if [[ \"$shardDir\" == src/bundles/* ]]; then\n              REPO_NAME=\"${REPO_NAME}-bundle\"\n            fi\n\n            REPO_URL=\"git@github.com:athena-framework/$REPO_NAME.git\"\n\n            if ! $(git diff --quiet --exit-code $BEFORE_SHA $AFTER_SHA -- \"$shardDir\"); then\n              echo \"Syncing: $REPO_NAME\"\n              git remote add \"$REPO_NAME\" \"$REPO_URL\" &> /dev/null || true\n              git fetch --quiet \"$REPO_NAME\"\n              git subtree push --prefix=\"$shardDir\" \"$REPO_NAME\" \"master\"\n\n              UPDATED_SHARDS+=(\"\\\"$REPO_NAME\\\"\")\n            fi\n          done\n\n          JSON_ARR=$(IFS=,; echo \"[${UPDATED_SHARDS[*]}]\")\n          echo \"Changed shards: $JSON_ARR\"\n          echo \"updated_shards=$JSON_ARR\" >> \"$GITHUB_OUTPUT\"\n\n          PR_LABELS=$(gh api --jq '.[0].labels | map(.name)' /repos/athena-framework/athena/commits/${{ github.event.after }}/pulls)\n\n          IS_KIND_DOCUMENTATION=$([ \"null\" != \"$(jq 'index(\"kind:documentation\")' <<< $PR_LABELS)\" ] && echo \"true\" || echo \"false\")\n          IS_KIND_RELEASE=$([ \"null\" != \"$(jq 'index(\"kind:release\")' <<< $PR_LABELS)\" ] && echo \"true\" || echo \"false\")\n\n          echo \"kind_docs=$IS_KIND_DOCUMENTATION\" >> \"$GITHUB_OUTPUT\"\n          echo \"kind_release=$IS_KIND_RELEASE\" >> \"$GITHUB_OUTPUT\"\n\n  release:\n    needs:\n      - sync\n    if: needs.sync.outputs.updated_shards != '[]' && needs.sync.outputs.kind_release == 'true'\n    strategy:\n      fail-fast: false\n      matrix:\n        shard: ${{ fromJson(needs.sync.outputs.updated_shards) }}\n    uses: ./.github/workflows/tag_and_create_release.yml\n    secrets: inherit\n    with:\n      shard: ${{ matrix.shard }}\n\n  # Trigger a re-build of the docs after all shards were released.\n  build-docs:\n    needs:\n      - release\n    uses: ./.github/workflows/build_and_publish_docs.yml\n    secrets: inherit\n    with:\n      branch: master\n\n  # Cherry picks changes into `docs` branch of each changed shard(s) for PRs with the `kind:documentation` label\n  pick-docs:\n    runs-on: ubuntu-latest\n    needs:\n      - sync\n    if: needs.sync.outputs.updated_shards != '[]' && needs.sync.outputs.kind_docs == 'true'\n    strategy:\n      fail-fast: false\n      matrix:\n        shard: ${{ fromJson(needs.sync.outputs.updated_shards) }}\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          repository: athena-framework/${{ matrix.shard }}\n          ref: docs\n          ssh-key: ${{ secrets.ATHENA_BOT_SSH_PRIV_KEY }}\n      - name: Cherry pick commit\n        run: |\n          set -euo pipefail\n\n          git config user.name \"${{ vars.BOT_USER_NAME }}\"\n          git config user.email \"${{ vars.BOT_USER_EMAIL }}\"\n\n          NEW_COMMIT=$(git ls-remote \"git@github.com:athena-framework/${{ matrix.shard }}.git\" HEAD | awk '{ print $1}')\n          git fetch origin $NEW_COMMIT\n          git cherry-pick $NEW_COMMIT\n          git push origin docs\n\n  # If there were shard related doc updates, trigger a re-build after all shards were synced.\n  build-shard-docs:\n    needs:\n      - sync # This needs to also depend on `sync` so we cn use its outputs even if it's redundant because of the dependency on `pick-docs`.\n      - pick-docs\n    if: needs.sync.outputs.kind_docs == 'true' && needs.sync.outputs.updated_shards != '[]'\n    uses: ./.github/workflows/build_and_publish_docs.yml\n    secrets: inherit\n    with:\n      branch: master\n\n  # If there were no shard related doc updates, simply trigger a re-build after `sync` job to handle updates to the root docs.\n  build-root-docs:\n    needs:\n      - sync\n    if: needs.sync.outputs.kind_docs == 'true' && needs.sync.outputs.updated_shards == '[]'\n    uses: ./.github/workflows/build_and_publish_docs.yml\n    secrets: inherit\n    with:\n      branch: master\n\n  # Build and deploy a development version of the docs to allow viewing docs for all un-released changes in `master`\n  build-dev-docs:\n    uses: ./.github/workflows/build_and_publish_docs.yml\n    secrets: inherit\n    with:\n      branch: dev\n"
  },
  {
    "path": ".github/workflows/tag_and_create_release.yml",
    "content": "on:\n  workflow_dispatch:\n    inputs:\n      shard:\n        description: 'Name of the shard to release'\n        type: string\n        required: true\n  workflow_call:\n    inputs:\n      shard:\n        description: 'Name of the shard to release'\n        type: string\n        required: true\n\njobs:\n  tag-and-create-release:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0\n        with:\n          ssh-private-key: ${{ secrets.ATHENA_BOT_SSH_PRIV_KEY }}\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          repository: athena-framework/${{ inputs.shard }}\n          ssh-key: ${{ secrets.ATHENA_BOT_SSH_PRIV_KEY }}\n          path: shard\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          path: root\n      - name: Setup Changie\n        uses: miniscruff/changie-action@11bcad388e7973948cbcecb10863baf024d5f607 # v3.0.0\n        with:\n          version: v1.22.1\n      - name: Get the latest version\n        working-directory: root\n        id: current-release\n        run: |\n          TAG=$(changie latest --project \"${{ inputs.shard }}\" | cut -d'/' -f2)\n          echo \"tag=$TAG\" >> \"$GITHUB_OUTPUT\"\n      - name: Tag release\n        working-directory: shard\n        run: |\n          set -euo pipefail\n\n          git config gpg.format ssh\n          git config user.signingkey \"${{ vars.BOT_USER_SIGNING_KEY }}\"\n          git config user.name \"${{ vars.BOT_USER_NAME }}\"\n          git config user.email \"${{ vars.BOT_USER_EMAIL }}\"\n\n          # Convert from GH repo name format into human readable format\n          SHARD_NAME=$(echo \"${{ inputs.shard }}\" | tr '-' ' ' | sed -e 's/\\b./\\u\\0/g')\n          MESSAGE=\"Athena $SHARD_NAME ${{ steps.current-release.outputs.tag }}\"\n\n          git tag -asm \"$MESSAGE\" ${{ steps.current-release.outputs.tag }}\n          git push --quiet origin ${{ steps.current-release.outputs.tag }}\n\n          # Be sure to reset `docs` branch back to current state of `master` as a release assumes the previously cherry-picked commits are now inherently included\n          git branch --quiet --force docs master\n          git push --quiet origin docs --force\n      - name: Create Release\n        working-directory: root\n        env:\n          GH_TOKEN: ${{ secrets.SYNC_TOKEN }}\n        run: |\n          # Cuts off first 2 lines since version number and date are redundant in this context.\n          # Then replace Author names with GH usernames for use in the GH release notes.\n          # See https://github.com/miniscruff/changie/discussions/610#discussioncomment-13917611 for reference.\n          tail -n+3 \".changes/${{ inputs.shard }}/${{ steps.current-release.outputs.tag }}.md\" | \\\n          perl -0777 -pe 's{\\([^)]+\\)\\s*<!--\\s*([^>]*\\S)\\s*-->}{ \"(\" . join(\", \", map { s/^\\s+|\\s+$//g; s/^@?/@/; $_ } grep { length } split(/\\s*,\\s*/, $1)) . \")\" }ge' | \\\n          gh release create ${{ steps.current-release.outputs.tag }} \\\n            --repo athena-framework/${{ inputs.shard }} \\\n            --verify-tag \\\n            --title ${{ steps.current-release.outputs.tag }} \\\n            --notes-file -\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.dwarf\n/.shards/\n/bin/\n/lib/\n/logs/\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in application that uses them\n/shard.lock\n\n__pycache__\n.cache\n.venv\nsite/\n\ncoverage/\n\n# Ignore `lib` symlink related to building docs\n# This includes bundle docs, due to mkdocs-material project plugin limitations\n/src/components/**/lib\n/src/bundles/**/lib\n"
  },
  {
    "path": ".python-version",
    "content": "3.13\n"
  },
  {
    "path": ".typos.toml",
    "content": "[default]\nextend-ignore-re = [\n  \"(?Rm)^.*#\\\\s*spellchecker:disable-line$\", # Allow disabling specific lines\n  \"=[0-9A-F \\n\\r]{2}\", # Disable checking Quoted-Printable encoded strings\n]\n\n[default.extend-words]\nreferer = \"referrer\"\nASPEC = \"ASPEC\"\n\n[files]\nextend-exclude = [\n  \"src/components/routing/spec/fixtures/route_provider/*\",\n  \"src/components/mime/src/types/data.cr\",\n  \"src/components/mime/spec/fixtures/*\",\n]\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nFirst off, thank you for taking the time to contribute! Athena, and many other open source projects, would not be the same without you!\n\nThe following is intended to be a living document describing the guidelines for contributing to Athena, and its shards.\nAthena makes use of the monorepo pattern, with each shard having its own read only repository.\nAs such, all contributions should directed towards this repository.\n\n## Start Here\n\nFind something that isn't working as expected? Have an idea for a new feature/enhancement? Want to improve the documentation?\n\nIf you answer \"Yes\" to any of these, you've come to the right place!\nThe first step is to search through the current [issues](https://github.com/athena-framework/athena/issues) and [pull requests](https://github.com/athena-framework/athena/pulls) to see if it has already been reported and/or resolved.\nIf your search comes up empty then feel free to create an issue, or if you're still not sure if you should make one, stop by the [Discord](https://discord.gg/TmDVPb3dmr) server to ask just to be sure; even if the answer is most likely always going to be yes.\n\n## Issue Tracker\n\nThe [issue tracker](https://github.com/athena-framework/athena/issues) is the heart of the Athena. Use it for bugs, questions, proposals, and feature requests.\n\nPlease always **open a new issue before sending a pull request** if you want to add a new feature to Athena, unless it is a minor obvious fix, or is in relation to an already open & approved issue.\nThis reduces the likelihood of wasted effort, and ensures the end result is robust by being able to work out implementation details _before_ the work is started.\n\n## Local Development\n\nBefore staring any local development, be sure to [fork](https://github.com/athena-framework/athena/fork) the repo, then create a branch to use for the related approved issue you're working on.\n\nDue to Athena's usage of a monorepo, the same single repo can be used to contribute code to all shards.\n\nIn addition to Crystal itself, Athena makes use of [just](https://just.systems/man/en/introduction.html) as its command runner.\n`just` provides a simple way of executing common commands needed for development.\n\nOnce you have it installed, and have cloned the monorepo, first install all the shard dependencies by running:\n\n```sh\njust install\n```\n\nAnd that's it, you are now ready to start coding!\nFrom here there are some additional optional tools that will come in handy:\n\n1. [typos](https://github.com/crate-ci/typos) - Source code spell checker, used as part of the `spellcheck` recipe.\n1. [watchexec](https://github.com/watchexec/watchexec) - Executes commands in response to file modifications, used as part of the `watch` and `watch-spec` recipes.\n1. [kcov](https://github.com/SimonKagstrom/kcov) - Code coverage tool, used to generate coverage reports/files as part of the `test` recipes.\n1. [changie](https://changie.dev/) - Changelog management tool, used as part of the `change` recipe.\n1. [uv](https://docs.astral.sh/uv/) - Python package manager, used for the `docs` related recipes.\n\n**TIP:** Running `just` will provide a summary of available recipes.\n\n### Development\n\nBecause of Athena's usage of a monorepo some interactions may be different than a normal shard.\nMost things can be done from the root of the repo; no need to `cd` to whatever shard you're working on; can just go through `just`.\n\nFor exploratory work, the suggested workflow is to have your code in the related shard's entry point file.\nE.g. `src/components/clock/src/athena-clock.cr` for the `clock` shard.\nFrom here you can run `just watch clock` and that will re-run the file when changes are made.\nThis makes it simple to play around with early implementations before there is proper test coverage.\n\n#### Testing\n\nSimilar to development itself, running the specs are also done through `just`: `just test clock` would run the spec suite for that shard, and generate coverage information if you have `kcov` installed.\nThe `watch-test` recipe can come in handy to provide quicker feedback while the tests are under development.\n\n##### Athena Spec\n\nMany Athena shards make use of [Athena Spec](https://athenaframework.org/Spec/) for their unit/integration tests.\nThis library provides an alternate DSL that is 100% compatible with the standard library's `Spec` module.\nI.e. they can be used together seamlessly, using whatever DSL is more appropriate for what is being tested.\nBeing familiar with the base [ASPEC::TestCase](https://athenaframework.org/Spec/TestCase/) type will not only make reading the specs easier, but writing them as well.\nIt comes with various features to make the tests simpler, reusable, and extensible.\nYou may even want to use it in your own projects :wink:.\n\n### Linting\n\nBeyond testing, Athena makes use of various forms of linting, including:\n\n* [ameba](https://github.com/crystal-ameba/ameba) for static code analysis\n* [typos](https://github.com/crate-ci/typos) for spell checking\n* The Crystal [formatter](https://crystal-lang.org/reference/guides/writing_shards.html#coding-style) for code formatting\n\nAll of these can be executed at once via the `just lint` recipe, but may also be ran individual as needed via their related `just` recipe.\n\n### Documentation\n\nAthena's [documentation](https://athenaframework.org/) site may be built locally via the `just build-docs` recipe.\nAlternatively, a live-updating server may be started via the `just serve-docs` recipe.\n\n## Opening a PR\n\nAt this point the code on your branch should have a passing test suite, including linting/spellchecking, and updated documentation if applicable. From here the only step left is to open a PR.\n\n> **NOTE:** Once the PR is opened, please avoid force-pushing to that branch.\n\nAthena comes with a PR template that should be filled out; being sure to reference the issue number in the context section. E.g. `Resolves #xxx`.\nThe changelog section should include all changes, both internal and external being sure to highlight breaking changes by prefixing the line with `**Breaking:**`.\nAdditionally, changes that affect end users should also have a `changie` change file. These can most easily be created by following along the prompts of `just change`.\nProject maintainers can add the file(s) themselves if needed to move things along; just being sure to give proper attribution in the change file.\n\n> **NOTE:** As of now you'll need to open the PR _before_ creating the change file in order to know what the PR number is.\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 George Dietrich\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": "# Athena\n\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/framework.svg)](https://github.com/athena-framework/framework/releases)\n\nA monorepo representing the ecosystem of reusable, independent shards provided by Athena.\nSee [Athena Framework](https://github.com/athena-framework/framework) for the web framework created using these shards.\n\n## Documentation\n\nCheckout the [External Docs](https://athenaframework.org).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n\n## Contributors\n\n- [George Dietrich](https://github.com/blacksmoke16) - creator and maintainer\n"
  },
  {
    "path": "UPGRADING.md",
    "content": "# Upgrading\n\nThe Athena ecosystem generally follows the [semver](https://semver.org/) pattern, but with some nuances due to its pre-1.0 status.\nMinor releases are reserved for major breaking changes, usually along side new large features/refactors.\nPatch releases contain backwards compatible features, bug fixes, and small breaking changes that are unlikely to affect the average user.\nIn either case, the changes you need to make in order to upgrade will be documented in files like this one; located within each component's [repository](https://github.com/orgs/athena-framework/repositories?q=topic%3Acomponent&type=source&language=crystal&sort=name) as applicable.\n"
  },
  {
    "path": "codecov.yml",
    "content": "comment:\n  layout: 'condensed_header, components, condensed_files, condensed_footer'\n\ncoverage:\n  status:\n    project:\n      default:\n        target: auto # Prevent total coverage from decreasing due to a PR\n    patch:\n      default:\n        target: 100% # Enforce all _new_ code is fully tested\n\ncomponent_management:\n  individual_components:\n    # Bundles\n    - component_id: mercure_bundle\n      name: Mercure Bundle\n      paths:\n        - src/bundles/mercure/**\n\n    # Components\n    - component_id: clock_component\n      name: Clock Component\n      paths:\n        - src/components/clock/**\n    - component_id: console_component\n      name: Console Component\n      paths:\n        - src/components/console/**\n    - component_id: dependency_injection_component\n      name: Dependency Injection Component\n      paths:\n        - src/components/dependency_injection/**\n    - component_id: dotenv_component\n      name: Dotenv Component\n      paths:\n        - src/components/dotenv/**\n    - component_id: event_dispatcher_component\n      name: Event Dispatcher Component\n      paths:\n        - src/components/event_dispatcher/**\n    - component_id: framework_component\n      name: Framework Component\n      paths:\n        - src/components/framework/**\n    - component_id: http_component\n      name: HTTP Component\n      paths:\n        - src/components/http/**\n    - component_id: http_kernel_component\n      name: HTTP Kernel Component\n      paths:\n        - src/components/http_kernel/**\n    - component_id: image_size_component\n      name: Image Size Component\n      paths:\n        - src/components/image_size/**\n    - component_id: mercure_component\n      name: Mercure Component\n      paths:\n        - src/components/mercure/**\n    - component_id: mime_component\n      name: MIME Component\n      paths:\n        - src/components/mime/**\n    - component_id: negotiation_component\n      name: Negotiation Component\n      paths:\n        - src/components/negotiation/**\n    - component_id: routing_component\n      name: Routing Component\n      paths:\n        - src/components/routing/**\n    - component_id: serializer_component\n      name: Serializer Component\n      paths:\n        - src/components/serializer/**\n    - component_id: spec_component\n      name: Spec Component\n      paths:\n        - src/components/spec/**\n    - component_id: validator_component\n      name: Validator Component\n      paths:\n        - src/components/validator/**\n"
  },
  {
    "path": "docs/README.md",
    "content": "## Athena\n\nAthena is a collection of general-purpose, robust, independent, and reusable components with the goal of powering a software ecosystem.\nThese include:\n\n* [Clock](/Clock/) (`ACLK`) - Decouples applications from the system clock\n* [Console](/Console/) (`ACON`) - Allows the creation of CLI based commands\n* [Contracts](/Contracts/) (`ACTR`) - A set of abstractions extracted out of the Athena components\n* [DependencyInjection](/DependencyInjection/) (`ADI`) - Robust dependency injection service container framework\n* [Dotenv](/Dotenv/) - Registers environment variables from a `.env` file\n* [EventDispatcher](/EventDispatcher/) (`AED`) - A Mediator and Observer pattern event library\n* [Framework](/Framework/) (`ATH`) - Integrates the components into a single cohesive, flexible, and modular framework\n* [HTTP](/HTTP/) (`AHTTP`) - Shared common HTTP abstractions/utilities\n* [HTTPKernel](/HTTPKernel/) (`AHK`) - Provides a structured process for converting a Request into a Response\n* [ImageSize](/ImageSize/) (`AIS`) - Measures the size of various image formats\n* [Mercure](/Mercure/) (`AMC`) - Allows easily pushing updates to web browsers and other HTTP clients using the Mercure protocol\n* [MIME](/MIME/) (`AMIME`) - Allows manipulating `MIME` messages\n* [Negotiation](/Negotiation/) (`ANG`) - Framework agnostic content negotiation library\n* [Routing](/Routing/) (`ART`) - A performant and robust HTTP based routing library/framework\n* [Serializer](/Serializer/) (`ASR`) - Object (de)serialization library\n* [Spec](/Spec/) (`ASPEC`) - Common/helpful [Spec](https://crystal-lang.org/api/Spec.html) compliant testing utilities\n* [Validator](/Validator/) (`AVD`) - Object/value validation library\n\nThese components may be used on their own to aid in existing projects or integrated into existing (or new) frameworks.\nAdditionally, some bundles are also provided that provide opt-in integrations of select components:\n\n* [MercureBundle](/MercureBundle/) (`ABM`) - Integrations the Mercure component into the framework.\n\nTIP: Each component may also define additional shortcut aliases. Check the `Aliases` page of each component in the [API Reference](./api_reference.md) for more information.\n\n## Athena Framework\n\nAthena also provides the [Framework](./getting_started/README.md) component that integrates select components into a single cohesive, flexible, and modular framework.\nIt is designed in such a way to be non-intrusive and not require a strict organizational convention in regards to how a project is setup;\nthis allows it to use a minimal amount of setup boilerplate while not preventing it for more complex projects.\nNot every component needs to be used or understood to start using the framework, only those which are required for the task at hand.\n\n### Feature Highlights\n\nAthena Framework has quite a few unique features that set it a part from other Crystal frameworks:\n\n* Follows the SOLID principles to encourage good software design\n* Architected in such a way to allow maximum flexibility without needing to fight against the framework\n* Uses annotations as a means of extension/customization\n* Built-in testing utilities\n\nTIP: The [demo](https://github.com/athena-framework/demo) application serves as a good example of what an application using the framework could look like.\n\n## Resources\n\n* [Discord Server](https://discord.gg/TmDVPb3dmr)\n* [GitHub Repository](https://github.com/athena-framework/athena)\n"
  },
  {
    "path": "docs/api_reference.md",
    "content": "Links to the API docs of each component may be found in this section.\nThese can be a good reference for more in-dept, component specific information, or how when using the component outside of the framework.\n"
  },
  {
    "path": "docs/bundle_reference.md",
    "content": "Bundles integrate components into the framework by registering services, configuring defaults, and wiring up dependencies via the [dependency injection](/DependencyInjection) component.\nEach bundle defines a schema that describes its available configuration options, allowing behavior to be customized without modifying code.\nSee [Getting Started > Configuration](./getting_started/configuration.md#bundles) for more information on the role bundles play in Athena.\n\nThe built-in [Framework Bundle](/Framework/Bundle/) schema is documented in the framework API docs.\nThird-party and standalone bundles, such as the [Mercure Bundle](/MercureBundle), are documented in this section.\n"
  },
  {
    "path": "docs/css/index.css",
    "content": "/* https://mkdocstrings.github.io/crystal/styling.html#recommended-styles */\n\n/* Indentation of sub-items */\ndiv.doc-contents:not(.first) {\n  padding-left: 15px;\n  border-left: 4px solid rgba(230, 230, 230);\n}\n\n/* Additional Customizations */\n\nbody {\n    font-size: 1rem;\n}\n\n/* Increased spacing between macro & instance methods. */\n.doc-macro + .doc-macro,\n.doc-instance_method + .doc-instance_method {\n  margin-top: 3em;\n}\n\n.md-typeset pre>code::-webkit-scrollbar {\n  height: 0.4em;\n}\n/* Slightly more compact headings */\nh1.doc-heading {\n  font-size: 1.3rem;\n}\nh3.doc-heading {\n  font-size: 0.85rem;\n  background: var(--md-code-bg-color);\n  border-radius: 2px;\n}\n\nh3.schema-heading {\n  margin: 0;\n}\n\n.schema-default, .schema-type {\n  display: inline;\n}\n\n.schema-type p {\n  display: inline;\n}\n\n.schema-default p {\n  display: inline;\n}\n\n/* Make content grid a bit wider (but never wider than the screen) */\n.md-grid {\n  max-width: min(80rem, 100vw);\n}\n\n/* Make sure page content doesn't get too wide on big 4k monitors */\n.md-content {\n    max-width: 85ch;\n}\n"
  },
  {
    "path": "docs/css/monorepo.css",
    "content": "/* Hide latest release since the monorepo is the old framework repo and still has tags */\n.md-source__fact--version {\n    display: none;\n}\n"
  },
  {
    "path": "docs/getting_started/README.md",
    "content": "Athena Framework does not have any other dependencies outside of Crystal and Shards.\nIt is designed in such a way to be non-intrusive and not require a strict organizational convention in regards to how a project is setup;\nthis allows it to use a minimal amount of setup boilerplate while not preventing it for more complex projects.\n\n## Install Athena Framework\n\nAdd the framework component to your `shard.yml`:\n\n```yaml\ndependencies:\n  athena:\n    github: athena-framework/framework\n    version: ~> 0.22.0\n```\n\nThen run `shards install`.\nThis will install the framework component and its required component dependencies.\nFinally require it via `require \"athena\"`, then are all set to starting using the framework, starting with [Routing & HTTP](./routing.md).\n\nTIP: Check out the [skeleton](https://github.com/athena-framework/skeleton) template repository to get up and running quickly.\n"
  },
  {
    "path": "docs/getting_started/commands.md",
    "content": "The Athena Framework comes with a built-in integration with the [Athena::Console](/Console) component.\nThis integration can be a way to define alternate entry points into your business logic,\nsuch as for use with scheduled jobs (Cron, Airflow, etc), or one-off internal/administrative things (running migrations, creating users, etc)\nall the while sharing the same dependencies due to it also leveraging [dependency injection](../why_athena.md#dependency-injection).\n\n## Basic Usage\n\nSimilar to [event listeners](./middleware.md#event-listeners), console commands can simply be registered as a service to be automatically registered.\nIf using the preferred [ACONA::AsCommand](/Console/Annotations/AsCommand) annotation, they are registered in a lazy fashion, meaning only the command(s) you execute will actually be instantiated.\n\n```crystal\n@[ADI::Register]\n@[ACONA::AsCommand(\"admin:create-user\", description: \"Creates a new internal user\")]\nclass AdminCreateUser < ACON::Command\n  # A constructor can be defined to leverage existing services if applicable\n  #def initialize(\n  #  @some_service : MyService\n  #)\n  #  # Just be sure to call `super()`!\n  #  super()\n  #end\n\n  # Configure the command by adding arguments, options, aliases, etc.\n  protected def configure : Nil\n    self\n      .argument(\"id\", :required, \"The employee's ID\")\n      .argument(\"name\", :required, \"The user's name\")\n      .argument(\"email\", :optional, \"The user's email. Assumed to be first.last if not provided\")\n      .option(\"admin\", nil, :none, \"If the user should be created as an internal admin\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    # Provides a standardized format for how to display text in the terminal\n    style = ACON::Style::Athena.new input, output\n\n    input.argument \"id\", Int32   # => 12\n    name = input.argument \"name\" # => \"George Dietrich\"\n    input.argument \"email\"       # => nil\n    input.option \"admin\", Bool   # => true\n\n    # Implement your business logic\n\n    style.success \"Successfully created a user for #{name}!\"\n\n    # Note the command executed successfully\n    Status::SUCCESS\n  end\nend\n```\n\nFrom here, if the application was created using the [skeleton](https://github.com/athena-framework/skeleton), commands can be executed via `shards run console -- admin:create-user 12 \"George Dietrich\" --admin`.\nOtherwise [ATH.run_console](/Framework/#Athena::Framework.run_console) can be used for your [entrypoint](/Console/#entrypoint) file.\n\nNOTE: During *development* the console binary needs to re-build the application in order to have access to the changes made since last execution.\nWhen deploying a *production* console binary, or if not doing any new console command dev, build it with the `--release` flag for increased performance.\n\n## Built-in Commands\n\nThe framework also comes with some helpful built-in commands to either help with debugging, or provide framework specific features.\nSee each command within the [ATH::Commands](/Framework/Commands) namespace for more information.\n"
  },
  {
    "path": "docs/getting_started/configuration.md",
    "content": "Some features need to be configured;\neither to enable/control how they work, or to customize the default functionality.\n\nThe [ATH.configure](/Framework/top_level/#Athena::Framework:configure(config)) macro is the primary entrypoint for configuring Athena Framework applications.\nIt is used in conjunction with the related [bundle schema](/Framework/Bundle/Schema/Cors/Defaults/) that defines the possible configuration properties:\n\n```crystal\nATH.configure({\n  framework: {\n    cors: {\n      enabled:  true,\n      defaults: {\n        allow_credentials: true,\n        allow_origin: [\"https://app.example.com\"],\n        expose_headers: [\"X-Transaction-ID X-Some-Custom-Header\"],\n      },\n    },\n  },\n})\n```\n\nIn this example we enable the [CORS Listener](/Framework/Listeners/CORS), as well as configure it to function as we desire.\nHowever you may be wondering \"how do I know what configuration properties are available?\" or \"what is that 'bundle schema' thing mentioned earlier?\".\nFor that we need to introduce the concept of a `Bundle`.\n\n## Bundles\n\nIt should be well known by now that the components that make up Athena's ecosystem are independent and usable outside of the Athena Framework itself.\nHowever because they are made with the assumption that the entire framework will not be available, there has to be something that provides the tighter integration into the rest of the framework that makes it all work together so nicely.\n\nBundles in the Athena Framework provide the mechanism by which external code can be integrated into the rest of the framework.\nThis primarily involves wiring everything up via the [Athena::DependencyInjection](/DependencyInjection) component.\nBut it also ties back into the configuration theme by allowing the user to control _how_ things are wired up and/or function at runtime.\n\nWhat makes the bundle concept so powerful and flexible is that it operates at the compile time level.\nE.g. if feature(s) are disabled in the configuration, then the types related to those feature(s) will not be included in the resulting binary at all.\nSimilarly, the configuration values can be accessed/used as constructor arguments to the various services, something a runtime approach would not allow.\n\nTODO: Expand upon bundle internals and how to create custom bundles.\n\n### Schemas\n\nEach bundle is responsible for defining a \"schema\" that represents the possible configuration properties that relate to the services provided by that bundle.\nEach bundle also has a name that is used to namespace the configuration passed to `ATH.configure`.\nFrom there, the keys maps to the downcase snakecased of the types found within the bundle's schema.\nFor example, the [Framework Bundle](/Framework/Bundle) used in the previous example, exposes `cors` and `format_listener` among others as part of its schema.\n\nNOTE: Bundles and schemas are not something the average end user is going to need to define/manage themselves other than register/configure to fit their needs.\n\n#### Validation\n\nThe compile time nature of bundles also extends to how their schemas are validated.\nBundles will raise a compile time error if the provided configuration values are invalid according to its schema.\nFor example:\n\n```crystal\n 10 | allow_credentials: 10,\n                          ^\nError: Expected configuration value 'framework.cors.defaults.allow_credentials' to be a 'Bool', but got 'Int32'.\n```\n\nThis also works for nested values:\n\n```crystal\n 10 | allow_origin:      [10, \"https://app.example.com\"] of String,\n                           ^\nError: Expected configuration value 'framework.cors.defaults.allow_origin[0]' to be a 'String', but got 'Int32'.\n```\n\nOr if the schema defines a value that is not nilable nor has a default:\n\n```crystal\n 10 | property some_property : String\n               ^------------\nError: Required configuration property 'framework.some_property : String' must be provided.\n```\n\nIt can also call out unexpected keys:\n\n```crystal\n 10 | foo:      \"bar\",\n                 ^\nError: Encountered unexpected property 'framework.cors.foo' with value '\"bar\"'.\n```\n\nHash configuration values are unchecked so are best used for unstructured data.\nIf you have a fixed set of related configuration, consider using [object_of](/DependencyInjection/Extension/Schema/#Athena::DependencyInjection::Extension::Schema:object_of(name,*)).\n\n#### Multi-Environment\n\nIn most cases, the configuration for each bundle is likely going to vary one environment to another.\nValues that change machine to machine should ideally be leveraging environmental variables.\nHowever, there are also cases where the underlying configuration should be different.\nE.g. locally use an in-memory cache while using redis in other environments.\n\nTo handle this, `ATH.configure` may be called multiple times, with the last call taking priority.\nThe configuration is deep merged together as well, so only the configuration you wish to alter needs to be defined.\nHowever hash/array/namedTuple values are not.\nNormal compile time logic may be used to make these conditional as well.\nE.g. basing things off `--release` or `--debug` flags vs the environment.\n\n```crystal\nATH.configure({\n  framework: {\n    cors: {\n      defaults: {\n        allow_credentials: true,\n        allow_origin:      [\"https://app.example.com\"],\n        expose_headers:    [\"X-Transaction-ID\", \"X-Debug-Header\"],\n      },\n    },\n  },\n})\n\n# Exclude the debug header in prod, but retain the other two configuration property values\n{% if env(Athena::ENV_NAME) == \"prod\" %}\nATH.configure({\n  framework: {\n    cors: {\n      defaults: {\n        expose_headers:    [\"X-Transaction-ID\"],\n      },\n    },\n  },\n})\n{% end %}\n\n# Do this other thing if in a non-release build\n{% unless flag? \"release\" %}\nATH.configure({...})\n{% end %}\n```\n\nTIP: Consider abstracting the additional `ATH.configure` calls to their own files, and `require` them.\nThis way things stay pretty organized, without needing large conditional logic blocks.\n\n## Parameters\n\nSometimes the same configuration value is used in several places within `ATH.configure`.\nInstead of repeating it, you can define it as a \"parameter\", which represents reusable configuration values.\nParameters are intended for values that do not change between machines, and control the application's behavior, e.g. the sender of notification emails, what features are enabled, or other high level application level values.\n\nParameters should _NOT_ be used for values that rarely change, such as the max amount of items to return per page.\nThese types of values are better suited to being a [constant](https://crystal-lang.org/reference/syntax_and_semantics/constants.html) within the related type.\nSimilarly, infrastructure related values that change from one machine to another, e.g. development machine to production server, should be defined using environmental variables.\n\nParameters can be defined using the special top level `parameters` key within `ATH.configure`.\n\n```crystal\nATH.configure({\n  parameters: {\n    # The parameter name is an arbitrary string,\n    # but is suggested to use some sort of prefix to differentiate your parameters\n    # from the built-in framework parameters, as well as other bundles.\n    \"app.admin_email\": \"admin@example.com\",\n\n    # Boolean param\n    \"app.enable_v2_protocol\": true,\n\n    # Collection param\n    \"app.supported_locales\": [\"en\", \"es\", \"de\"],\n  },\n})\n```\n\nThe parameter value may be any primitive type, including strings, bools, hashes, arrays, etc.\nFrom here they can be used when configuring a bundle via enclosing the name of the parameter within `%`.\nFor example:\n\n```crystal\nATH.configure({\n  some_bundle: {\n    email: \"%app.admin_email%\",\n  },\n})\n```\n\nTIP: Parameters may also be [injected](/DependencyInjection/Register/#Athena::DependencyInjection::Register--parameters) directly into services via their constructor.\n\n## Custom Annotations\n\nAthena integrates the [Athena::DependencyInjection](/DependencyInjection) component's ability to define custom annotation configurations.\nThis feature allows developers to define custom annotations, and the data that should be read off of them, then apply/access the annotations on [ATH::Controller](/Framework/Controller) and/or [AHK::Action](/HTTPKernel/Action)s.\n\nThis is a powerful feature that allows for almost limitless flexibility/customization.\nSome ideas include: storing some value in the request attributes and raise an exception or invoke some external service; all based on the presence/absence of it, a value read off of it, or either/both of those in-conjunction with an external service.\nFor example:\n\n```crystal\nrequire \"athena\"\n\n# Define our configuration annotation with an optional `name` argument.\n# A default value can also be provided, or made not nilable to be considered required.\nADI.configuration_annotation MyAnnotation, name : String? = nil\n\n# Define and register our listener that will do something based on our annotation.\n@[ADI::Register]\nclass MyAnnotationListener\n  def initialize(\n    @annotation_resolver : ATH::AnnotationResolver,\n  ); end\n\n  @[AEDA::AsEventListener]\n  def on_view(event : AHK::Events::View) : Nil\n    # Represents all custom annotations applied to the current AHK::Action + controller class.\n    ann_configs = @annotation_resolver.action_annotations(event.request)\n\n    # Check if this action has the annotation\n    unless ann_configs.has? MyAnnotation\n      # Do something based on presence/absence of it.\n      # Would be executed for `ExampleController#one` since it does not have the annotation applied.\n    end\n\n    my_ann = ann_configs[MyAnnotation]\n\n    # Access data off the annotation.\n    if my_ann.name == \"Fred\"\n      # Do something if the provided name is/is not some value.\n      # Would be executed for `ExampleController#two` since it has the annotation applied, and name value equal to \"Fred\".\n    end\n  end\nend\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"one\")]\n  def one : Int32\n    1\n  end\n\n  @[ARTA::Get(\"two\")]\n  @[MyAnnotation(name: \"Fred\")]\n  def two : Int32\n    2\n  end\nend\n\nATH.run\n```\n\n### Pagination\n\n<!-- TODO: Move this to a cookbook/how-to type of page/section? -->\n\nA good example use case for custom annotations is the creation of a `Paginated` annotation that can be applied to controller actions to have them be paginated via the listener. Generic pagination can be implemented via listening on the [view](./middleware.md#4-view-event) event which exposes the value returned via the related controller action.\n\n```crystal\n\n# Define our configuration annotation with the default pagination values.\n# These values can be overridden on a per endpoint basis.\nADI.configuration_annotation Paginated, page : Int32 = 1, per_page : Int32 = 100, max_per_page : Int32 = 1000\n\n# Define and register our listener that will handle paginating the response.\n@[ADI::Register]\nstruct PaginationListener\n  private PAGE_QUERY_PARAM     = \"page\"\n  private PER_PAGE_QUERY_PARAM = \"per_page\"\n\n  def initialize(\n    @annotation_resolver : ATH::AnnotationResolver,\n  ); end\n\n  # Use a high priority to ensure future listeners are working with the paginated data\n  @[AEDA::AsEventListener(priority: 255)]\n  def on_view(event : AHK::Events::View) : Nil\n    # Return if the endpoint is not paginated.\n    return unless (pagination = @annotation_resolver.action_annotations(event.request)[Paginated]?)\n\n    # Return if the action result is not able to be paginated.\n    return unless (action_result = event.action_result).is_a? Indexable\n\n    request = event.request\n\n    # Determine pagination values; first checking the request's query parameters,\n    # using the default values in the `Paginated` object if not provided.\n    page = request.query_params[PAGE_QUERY_PARAM]?.try &.to_i || pagination.page\n    per_page = request.query_params[PER_PAGE_QUERY_PARAM]?.try &.to_i || pagination.per_page\n\n    # Raise an exception if `per_page` is higher than the max.\n    raise AHK::Exception::BadRequest.new \"Query param 'per_page' should be '#{pagination.max_per_page}' or less.\" if per_page > pagination.max_per_page\n\n    # Paginate the resulting data.\n    # In the future a more robust pagination service could be injected\n    # that could handle types other than `Indexable`, such as\n    # ORM `Collection` objects.\n    end_index = page * per_page\n    start_index = end_index - per_page\n\n    # Paginate and set the action's result.\n    event.action_result = action_result[start_index...end_index]\n  end\nend\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"values\")]\n  @[Paginated(per_page: 2)]\n  def get_values : Array(Int32)\n    (1..10).to_a\n  end\nend\n\nATH.run\n\n# GET /values # => [1, 2]\n# GET /values?page=2 # => [3, 4]\n# GET /values?per_page=3 # => [1, 2, 3]\n# GET /values?per_page=3&page=2 # => [4, 5, 6]\n```\n"
  },
  {
    "path": "docs/getting_started/error_handling.md",
    "content": "## HTTP Exceptions\n\nException handling in the Athena Framework is similar to exception handling in any Crystal program, with the addition of a new unique exception type, [AHK::Exception::HTTPException](/HTTPKernel/Exception/HTTPException).\nCustom `HTTP` errors can also be defined by inheriting from `AHK::Exception::HTTPException` or a child type.\nA use case for this could be allowing additional data/context to be included within the exception.\n\nNon `AHK::Exception::HTTPException` exceptions are represented as a `500 Internal Server Error`.\n\nWhen an exception is raised, the framework emits the [Exception](./middleware.md#8-exception-handling) event to allow an opportunity for it to be handled.\nBy default these exceptions will return a `JSON` serialized version of the exception, via [AHK::ErrorRenderer](/HTTPKernel/ErrorRenderer), that includes the message and code; with the proper response status set.\nIf the exception goes unhandled, i.e. no listener sets an [AHTTP::Response](/HTTP/Response) on the event, then the request is finished and the exception is re-raised.\n\n```crystal\nrequire \"athena\"\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"/divide/{num1}/{num2}\")]\n  def divide(num1 : Int32, num2 : Int32) : Int32\n    num1 // num2\n  end\n\n  @[ARTA::Get(\"/divide_rescued/{num1}/{num2}\")]\n  def divide_rescued(num1 : Int32, num2 : Int32) : Int32\n    num1 // num2\n    # Rescue a non `AHK::Exception::HTTPException`\n  rescue ex : DivisionByZeroError\n    # in order to raise an `AHK::Exception::HTTPException` to provide a better error message to the client.\n    raise AHK::Exception::BadRequest.new \"Invalid num2:  Cannot divide by zero\"\n  end\nend\n\nATH.run\n\n# GET /divide/10/0          # => {\"code\":500,\"message\":\"Internal Server Error\"}\n# GET /divide_rescued/10/0  # => {\"code\":400,\"message\":\"Invalid num2:  Cannot divide by zero\"}\n# GET /divide_rescued/10/10 # => 1\n```\n\n## Logging\n\nLogging is handled via Crystal's [Log](https://crystal-lang.org/api/Log.html) module. Athena Framework logs when a request matches a controller action, as well as any exception. This of course can be augmented with additional application specific messages.\n\n```bash\n2022-01-08T20:44:18.134423Z   INFO - athena.routing: Server has started and is listening at http://0.0.0.0:3000\n2022-01-08T20:44:19.773376Z   INFO - athena.routing: Matched route 'example_controller_divide' -- route: \"example_controller_divide\", route_parameters: {\"_route\" => \"example_controller_divide\", \"_controller\" => \"ExampleController#divide\", \"num1\" => \"10\", \"num2\" => \"0\"}, request_uri: \"/divide/10/0\", method: \"GET\"\n2022-01-08T20:44:19.892748Z  ERROR - athena.routing: Uncaught exception #<DivisionByZeroError:Division by 0> at /usr/lib/crystal/int.cr:141:7 in 'check_div_argument'\nDivision by 0 (DivisionByZeroError)\n  from /usr/lib/crystal/int.cr:141:7 in 'check_div_argument'\n  from /usr/lib/crystal/int.cr:105:5 in '//'\n  from src/components/framework/src/athena.cr:206:5 in 'divide'\n  from src/components/framework/src/ext/routing/annotation_route_loader.cr:8:5 in '->'\n  from /usr/lib/crystal/primitives.cr:266:3 in 'execute'\n  from src/components/framework/src/route_handler.cr:76:16 in 'handle_raw'\n  from src/components/framework/src/route_handler.cr:19:5 in 'handle'\n  from src/components/framework/src/athena.cr:161:27 in '->'\n  from /usr/lib/crystal/primitives.cr:266:3 in 'process'\n  from /usr/lib/crystal/http/server.cr:515:5 in 'handle_client'\n  from /usr/lib/crystal/http/server.cr:468:13 in '->'\n  from /usr/lib/crystal/primitives.cr:266:3 in 'run'\n  from /usr/lib/crystal/fiber.cr:98:34 in '->'\n  from ???\n\n2022-01-08T20:45:10.803001Z   INFO - athena.routing: Matched route 'example_controller_divide_rescued' -- route: \"example_controller_divide_rescued\", route_parameters: {\"_route\" => \"example_controller_divide_rescued\", \"_controller\" => \"ExampleController#divide_rescued\", \"num1\" => \"10\", \"num2\" => \"0\"}, request_uri: \"/divide_rescued/10/0\", method: \"GET\"\n2022-01-08T20:45:10.923945Z   WARN - athena.routing: Uncaught exception #<Athena::Framework::Exception::BadRequest:Invalid num2:  Cannot divide by zero> at src/components/framework/src/athena.cr:215:5 in 'divide_rescued'\nInvalid num2:  Cannot divide by zero (Athena::Framework::Exception::BadRequest)\n  from src/components/framework/src/athena.cr:215:5 in 'divide_rescued'\n  from src/components/framework/src/ext/routing/annotation_route_loader.cr:8:5 in '->'\n  from /usr/lib/crystal/primitives.cr:266:3 in 'execute'\n  from src/components/framework/src/route_handler.cr:76:16 in 'handle_raw'\n  from src/components/framework/src/route_handler.cr:19:5 in 'handle'\n  from src/components/framework/src/athena.cr:161:27 in '->'\n  from /usr/lib/crystal/primitives.cr:266:3 in 'process'\n  from /usr/lib/crystal/http/server.cr:515:5 in 'handle_client'\n  from /usr/lib/crystal/http/server.cr:468:13 in '->'\n  from /usr/lib/crystal/primitives.cr:266:3 in 'run'\n  from /usr/lib/crystal/fiber.cr:98:34 in '->'\n  from ???\n\n2022-01-08T20:45:14.132652Z   INFO - athena.routing: Matched route 'example_controller_divide_rescued' -- route: \"example_controller_divide_rescued\", route_parameters: {\"_route\" => \"example_controller_divide_rescued\", \"_controller\" => \"ExampleController#divide_rescued\", \"num1\" => \"10\", \"num2\" => \"10\"}, request_uri: \"/divide_rescued/10/10\", method: \"GET\"\n```\n\n#### Customization\n\nBy default the Athena Framework utilizes the default [Log::Formatter](https://crystal-lang.org/api/Log/Formatter.html) and [Log::Backend](https://crystal-lang.org/api/Log/Backend.html)s Crystal defines. This of course can be customized via interacting with Crystal's [Log](https://crystal-lang.org/api/Log.html) module. It is also possible to control what exceptions, and with what severity, will be logged by redefining the `log_exception` method within [AHK::Listeners::Error](/HTTPKernel/Listeners/Error).\n\nTIP: Since `AHK::Listeners::Error` logs already include the error message and first line of the trace, consider defining a custom [Log Formatter](https://crystal-lang.org/api/Log/Formatter.html) that excludes the `exception` to have shorter, single line error logs in console:\n```crystal\nLog.define_formatter SingleLineFormatter, \"#{timestamp} #{severity} - #{source(after: \": \")}#{message}\" \\\n                                          \"#{data(before: \" -- \")}#{context(before: \" -- \")}\"\n\n# 2024-03-04T05:30:29.329041Z   INFO - athena.framework: Server has started and is listening at http://0.0.0.0:3000\n# 2024-03-04T05:30:37.568264Z   INFO - athena.framework: Matched route 'view_controller_bar' -- route: \"view_controller_bar\", route_parameters: {\"_route\" => \"view_controller_bar\", # \"_controller\" => \"ViewController#bar\"}, request_uri: \"/bar\", method: \"GET\"\n# 2024-03-04T05:30:40.280070Z   INFO - athena.framework: Matched route 'view_controller_foo' -- route: \"view_controller_foo\", route_parameters: {\"_route\" => \"view_controller_foo\", # \"_controller\" => \"ViewController#foo\"}, request_uri: \"/foo\", method: \"GET\"\n# 2024-03-04T05:30:40.351541Z  ERROR - athena.framework: Uncaught exception #<Athena::Framework::Exception::Logic:Failed to serialize response body. Did you forget to include # either `JSON::Serializable` or `ASR::Serializable`?> at src/components/framework/src/view/view_handler.cr:166:21 in 'init_response'\n# 2024-03-04T05:30:41.281275Z   INFO - athena.framework: Matched route 'view_controller_foo' -- route: \"view_controller_foo\", route_parameters: {\"_route\" => \"view_controller_foo\", # \"_controller\" => \"ViewController#foo\"}, request_uri: \"/foo\", method: \"GET\"\n# 2024-03-04T05:30:41.282632Z  ERROR - athena.framework: Uncaught exception #<Athena::Framework::Exception::Logic:Failed to serialize response body. Did you forget to include # either `JSON::Serializable` or `ASR::Serializable`?> at src/components/framework/src/view/view_handler.cr:166:21 in 'init_response'\n# 2024-03-04T05:30:43.886367Z   INFO - athena.framework: Matched route 'view_controller_bar' -- route: \"view_controller_bar\", route_parameters: {\"_route\" => \"view_controller_bar\", # \"_controller\" => \"ViewController#bar\"}, request_uri: \"/bar\", method: \"GET\"\n```\n"
  },
  {
    "path": "docs/getting_started/middleware.md",
    "content": "At a high level the Athena Framework's job is *to interpret a request and create the appropriate response based on your application logic*.\nConceptually this could be broken down into three steps:\n\n1. Consume the request\n2. Apply application logic to determine what the response should be\n3. Return the response\n\nSteps 1 and 3 are handled via Crystal's [HTTP::Server](https://crystal-lang.org/api/HTTP/Server.html), while step 2 is where the framework fits in.\n\n## Events\n\nAthena Framework is an event based framework, meaning it emits various events via the [Event Dispatcher](/EventDispatcher) component during the life-cycle of a request.\nThese events are listened on internally in order to handle each request; custom listeners on these events can also be registered.\nThe flow of a request, and the related events that are dispatched, is depicted below in a visual format:\n\n![High Level Request Life-cycle Flow](../img/Athena.png)\n\n### 1. Request Event\n\nThe very first event that is dispatched is the [AHK::Events::Request](/HTTPKernel/Events/Request) event and can have a variety of listeners. The primary purpose of this event is to create an [AHTTP::Response](/HTTP/Response/) directly, or to add information to the requests' attributes; a simple key/value store tied to request instance accessible via [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes).\n\nIn some cases the listener may have enough information to return an [AHTTP::Response](/HTTP/Response/) immediately. An example of this would be the [ATH::Listeners::CORS](/Framework/Listeners/CORS/) listener. If enabled it is able to return a `CORS` preflight response even before routing is invoked.\n\nWARNING: If an [AHTTP::Response](/HTTP/Response/) is returned at this stage, the flow of the request skips directly to the [response](#5-response-event) event. Future `Request` event listeners will not be invoked either.\n\nAnother use case for this event is populating additional data into the request's attributes; such as the locale or format of the request.\n\n!!! example \"Request event in the Athena Framework\"\n    This is the event that [AHK::Listeners::Routing](/HTTPKernel/Listeners/Routing/) listens on to determine which [ATH::Controller](/Framework/Controller/)/[AHK::Action](/HTTPKernel/Action/) pair should handle the request.\n\n    See [ATH::Controller](/Framework/Controller/) for more details on routing.\n\n### 2. Action Event\n\nThe next event to be dispatched is the [AHK::Events::Action](/HTTPKernel/Events/Action/) event, assuming a response was not already returned within the [request](#1-request-event) event.\nThis event is dispatched after the related controller/action pair is determined, but before it is executed.\nThis event is intended to be used when a listener requires information from the related [AHK::Action](/HTTPKernel/Action/); such as reading [custom annotations](./configuration.md#custom-annotations).\n\n### 3. Invoke the Controller Action\n\nThis next step is not an event, but a important concept within the Athena Framework nonetheless; executing the controller action related to the current request.\n\n#### Argument Resolution\n\nBefore the controller action can be invoked, the arguments, if any, to pass to it need to be determined.\nThis is achieved via an [AHK::Controller::ArgumentResolverInterface](/HTTPKernel/Controller/ArgumentResolverInterface/) that facilitates gathering all the arguments.\nOne or more [ATHR::Interface](/Framework/Controller/ValueResolvers/Interface/) will then be used to resolve each specific argument's value.\n\nCheckout [ATH::Controller::ValueResolvers](/Framework/Controller/ValueResolvers/) for a summary of the built-in resolvers, and the order in which they are invoked.\nCustom value resolves may be created & registered to extend this functionality.\n\nTODO: An additional event could possibly be added after the arguments have been resolved, but before invoking the controller action.\n\n#### Execute the Controller Action\n\nThe job of a controller action is to apply business/application logic to build a response for the related request; such as an HTML page, a JSON string, or anything else. How/what exactly this should be is up to the developer creating the application.\n\n#### Handle the Response\n\nThe type of the value returned from the controller action determines what happens next. If the value is an [AHTTP::Response](/HTTP/Response/), then it is used as is, skipping directly to the [response](#5-response-event) event. However, if the value is _NOT_ an `AHTTP::Response`, then the [view](#4-view-event) is dispatched (since the framework _needs_ an `AHTTP::Response` in order to have something to send back to the client).\n\n### 4. View Event\n\nThe [AHK::Events::View](/HTTPKernel/Events/View/) event is only dispatched when the controller action does _NOT_ return an [AHTTP::Response](/HTTP/Response/). The purpose of this event is to turn the controller action's return value into an `AHTTP::Response`.\n\nAn [ATH::View](/Framework/View/) may be used to customize the response, e.g. setting a custom response status and/or adding additional headers; while keeping the controller action response data intact.\n\nThis event is intended to be used as a \"View\" layer; allowing scalar values/objects to be returned while listeners convert that value to the expected format (e.g. JSON, HTML, etc.). See the [negotiation](./routing.md#content-negotiation) component for more information on this feature.\n\n!!! example \"View event in the Athena Framework\"\n    By default the framework will JSON serialize any non [AHTTP::Response](/HTTP/Response/) values.\n\n### 5. Response Event\n\nThe end goal of the Athena Framework is to return an [AHTTP::Response](/HTTP/Response/) back to the client; which might be created within the [request](#1-request-event) event, returned from the related controller action, or set within the [view](#4-view-event) event. Regardless of how the response was created, the [AHK::Events::Response](/HTTPKernel/Events/Response/) event is dispatched directly after.\n\nThe intended use case for this event is to allow for modifying the response object in some manner. Common examples include: add/edit headers, add cookies, change/compress the response body.\n\n### 6. Return the Response\n\nThe raw [HTTP::Server::Response](https://crystal-lang.org/api/HTTP/Server/Response.html) object is never directly exposed. The reasoning for this is to allow listeners to mutate the response before it is returned as mentioned in the [response](#5-response-event) event section. If the raw response object was exposed, whenever any data is written to it it'll immediately be sent to the client and the status/headers will be locked; as mentioned in the Crystal API docs:\n\n> The response `#status` and `#headers` must be configured before writing the response body. Once response output is written, changing the `#status` and `#headers` properties has no effect.\n\nEach [AHTTP::Response](/HTTP/Response/) has a [AHTTP::Response::Writer](/HTTP/Response/Writer/) instance that determines _how_ the response should be written to the raw response's IO. By default it is written directly, but can be customized via the [response](#5-response-event) event, such as for compression.\n\n### 7. Terminate Event\n\nThe final event to be dispatched is the [AHK::Events::Terminate](/HTTPKernel/Events/Terminate/) event. This is event is dispatched _after_ the response has been sent to the user.\n\nThe intended use case for this event is to perform some \"heavy\" action after the user has received the response; as to not affect the response time of the request. E.x. queuing up emails or logs to be sent/written after a successful request.\n\n### 8. Exception Handling\n\nIf an exception is raised at anytime while a request is being handled, the [AHK::Events::Exception](/HTTPKernel/Events/Exception/) is dispatched. The purpose of this event is to convert the exception into an [AHTTP::Response](/HTTP/Response/). This is globally handled via an [AHK::ErrorRendererInterface](/HTTPKernel/ErrorRendererInterface/), with the default being to JSON serialize the exception.\n\nIt is also possible to handle specific error states differently by registering multiple exception listeners to handle each case. An example of this could be to invoke some special logic only if the exception is of a specific type.\n\n## Event Listeners\n\nUnlike other frameworks, Athena Framework leverages event based middleware instead of a pipeline based approach.\nThe primary use case for event listeners is to tap into the life-cycle of the request, such as adding common headers, setting state extracted from the request, or whatever else the application requires.\nThese can be created by creating a type annotated with [ADI::Register](/DependencyInjection/Register), then annotating one or more methods with [AEDA::AsEventListener](/EventDispatcher/Annotations/AsEventListener).\n\n```crystal\nrequire \"athena\"\n\n@[ADI::Register]\nclass CustomListener\n  @[AEDA::AsEventListener]\n  def on_response(event : AHK::Events::Response) : Nil\n    event.response.headers[\"FOO\"] = \"BAR\"\n  end\nend\n\nclass ExampleController < ATH::Controller\n  get \"/\" do\n    \"Hello World\"\n  end\nend\n\nATH.run\n\n# GET / # => Hello World (with `FOO => BAR` header)\n```\n\nSimilarly, the framework itself is implemented using the same features available to the users.\nThus it is very easy to run specific listeners before/after the built-in ones if so desired.\n\nTIP: Check out the `debug:event-dispatcher` [command](./commands.md) for an easy way to see all the listeners and the order in which they are executed.\n\nTIP: A single event listener may listen on multiple events. Instance variables can be used to share state between the events.\n\nWARNING: The \"type\" of the listener has an effect on its behavior!\nWhen a `struct` service is retrieved or injected into a type, it will be a copy of the one in the SC (passed by value).\nThis means that changes made to it in one type, will *NOT* be reflected in other types.\nA `class` service on the other hand will be a reference to the one in the SC. This allows it to share state between services.\n\n## Custom Events\n\nUsing events can be a helpful design pattern to allow for code that is easily extensible.\nAn event represents something _has happened_ where nobody may be interested in it, or in other words there may be zero or more listeners listening on a given event.\nA more concrete example is an event could be dispatched after some core piece of application logic.\nFrom here it would be easy to tap into when this logic is executed to perform some other follow up action, without increasing the complexity of the type that performs the core action.\nThis also adheres to the [single responsibility](../why_athena.md#single-responsibility) principle.\n\n```crystal\nrequire \"athena\"\n\n# Define a custom event\nclass MyEvent < AED::Event\n  property value : Int32\n\n  def initialize(@value : Int32); end\nend\n\n# Define a listener that listens our the custom event.\n@[ADI::Register]\nclass CustomEventListener\n  @[AEDA::AsEventListener]\n  def call(event : MyEvent) : Nil\n    event.value *= 10\n  end\nend\n\n# Register a controller as a service,\n# injecting the event dispatcher to handle processing our value.\n@[ADI::Register]\nclass ExampleController < ATH::Controller\n  def initialize(@event_dispatcher : AED::EventDispatcherInterface); end\n\n  @[ARTA::Get(\"/{value}\")]\n  def get_value(value : Int32) : Int32\n    event = MyEvent.new value\n\n    @event_dispatcher.dispatch event\n\n    event.value\n  end\nend\n\nATH.run\n\n# GET /10 # => 100\n```\n"
  },
  {
    "path": "docs/getting_started/routing.md",
    "content": "## Controllers\n\nThe Athena Framework is a MVC based framework, as such, the logic to handle a given route is defined within an [ATH::Controller](/Framework/Controller).\nAthena Framework takes an annotation based approach to routing.\nAn annotation, such as `ARTA::Get` is applied to an instance method of a controller class, which will be executed when that endpoint receives a request.\n\n### Creating a Route\n\nIn Athena Framework, controllers are simply classes and route actions are simply methods.\nThis means they can be documented/tested as you would any Crystal class/method.\nHowever see the [testing](./testing.md#testing-controllers) section for how to best test a controller.\n\n```crystal\nrequire \"athena\"\n\n# Define a controller\nclass ExampleController < ATH::Controller\n  # Define an action to handle the related route\n  @[ARTA::Get(\"/\")]\n  def index : String\n    \"Hello World\"\n  end\n\n  # The macro DSL can also be used\n  get \"/\" do\n    \"Hello World\"\n  end\nend\n\n# Run the server\nATH.run\n\n# GET / # => Hello World\n```\n\nRouting is handled via the [Athena::Routing](/Routing) component.\nIt provides a flexible and robust foundation for handling determining which route should match a given request.\n\nTIP: Check out the `debug:router` [command](./commands.md) to view all of the routes the framework is aware of within your application.\n\n### Raw Response\n\nAn [AHTTP::Response](/HTTP/Response) can be used to fully customize the response; such as returning a specific status code, or adding some one-off headers.\n\n```crystal\nrequire \"athena\"\nrequire \"mime\"\n\nclass ExampleController < ATH::Controller\n  # A GET endpoint returning an `AHTTP::Response`.\n  # Can be used to return raw data, such as HTML or CSS etc, in a one-off manner.\n  @[ARTA::Get(\"/index\")]\n  def index : AHTTP::Response\n    AHTTP::Response.new(\n      \"<h1>Welcome to my website!</h1>\",\n      headers: HTTP::Headers{\"content-type\" => MIME.from_extension(\".html\")}\n    )\n  end\nend\n\nATH.run\n\n# GET /index # => \"<h1>Welcome to my website!</h1>\"\n```\n\nA [View](./middleware.md#4-view-event) event is emitted if the returned value is _NOT_ an [AHTTP::Response](/HTTP/Response).\nBy default, non `AHTTP::Response`s are JSON serialized.\nHowever, this event can be listened on to customize how the value is serialized.\nMore on this in the [Content Negotiation](#content-negotiation) section.\n\n### Route Parameters\n\nArguments are converted to their expected types if possible, otherwise an error response is automatically returned.\nThe values are provided directly as method arguments, thus preventing the need for `env.params.url[\"name\"]` and any boilerplate related to it.\nJust like normal method arguments, default values can be defined.\nThe method's return type adds some type safety to ensure the expected value is being returned.\n\n```crystal\nrequire \"athena\"\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"/add/{value1}/{value2}\")]\n  def add(value1 : Int32, value2 : Int32) : Int32\n    value1 + value2\n  end\nend\n\nATH.run\n\n# GET /add/2/3    # => 5\n# GET /add/foo/12 # => {\"code\":400,\"message\":\"Required parameter 'value1' with value 'foo' could not be converted into a valid 'Int32'\"}\n```\n\nTIP: For more complex conversions, consider creating a [Value Resolver](/Framework/Controller/ValueResolvers/Interface) to encapsulate the logic.\n\n#### Query Parameters\n\n[ATHA::MapQueryParameter](/Framework/Annotations/MapQueryParameter) can be used to map a query parameter directly to a controller action parameter.\n\n```crystal\nrequire \"athena\"\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"/\")]\n  def index(@[ATHA::MapQueryParameter] page : Int32) : Int32\n    page\n  end\nend\n\nATH.run\n\n# GET /          # => {\"code\":404,\"message\":\"Missing query parameter: 'page'.\"}\n# GET /?page=10  # => 10\n# GET /?page=bar # => {\"code\":404,\"message\":\"Invalid query parameter: 'page'.\"}\n```\n\nThis works well enough for one-off parameters.\nHowever [ATHA::MapQueryString](/Framework/Annotations/MapQueryString) can be used to the request's query string into a DTO type, much like how `JSON::Serializable` works for example.\nIn addition to making it easier to reuse, it also allows for enhanced validation of the query parameters via the [`Athena::Validator`](/Validator/#validating-objects) component.\n\n### Raw Request\n\nRestricting an action argument to [AHTTP::Request](/HTTP/Request) will provide the raw request object.\nThis can be useful to access data directly off the request object, such as consuming the request's body.\nThis approach is fine for simple or one-off endpoints.\n\nTIP: Check out [ATHR::RequestBody](/Framework/Controller/ValueResolvers/RequestBody) for a better way to handle this.\n\n```crystal\nrequire \"athena\"\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Post(\"/data\")]\n  def data(request : AHTTP::Request) : String\n    raise AHK::Exception::BadRequest.new \"Request body is empty.\" unless body = request.body\n\n    JSON.parse(body).as_h[\"name\"].as_s\n  end\nend\n\nATH.run\n\n# POST /data body: {\"id\":1,\"name\":\"Jim\"} # => Jim\n```\n\n### Streaming Response\n\nBy default `AHTTP::Response` content is written all at once to the response's `IO`.\nHowever in some cases the content may be too large to fit into memory. In this case an [AHTTP::StreamedResponse](/HTTP/StreamedResponse) may be used to stream the content back to the client.\n\n```crystal\nrequire \"athena\"\nrequire \"mime\"\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(path: \"/users\")]\n  def users : AHTTP::Response\n    AHTTP::StreamedResponse.new headers: HTTP::Headers{\"content-type\" => \"application/json; charset=UTF-8\"} do |io|\n      User.all.to_json io\n    end\n  end\nend\n\nATH.run\n\n# GET /athena/users\" # => [{\"id\":1,...},...]\n```\n\n## File Uploads\n\nAthena supports the [opt-in](/Framework/Bundle/Schema/FileUploads/) feature of populating [AHTTP::Request#files](/HTTP/Request/#Athena::HTTP::Request#files)\nbased on the files included in a `multipart/form-data` file upload request.\nA [HTTP::FormData::Part](https://crystal-lang.org/api/HTTP/FormData/Part.html) without a *filename* is considered to be just a normal textual field and will be added to [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes).\nThese values can be provided to the controller action in the same way route parameters can.\n\n```crystal\nrequire \"athena\"\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Post(path: \"/avatar\")]\n  def avatar(request : AHTTP::Request) : String\n    request.files[\"profile_picture\"][0].client_original_name\n  end\nend\n\nATH.configure({\n  framework: {\n    file_uploads: {\n      enabled: true,\n    },\n  },\n})\n\nATH.run\n\n# POST /avatar\" (multipart/form-data request with `profile_picture` key pointing to the `pic.png` file) # => pic.png\n```\n\nTIP: Check out [ATHA::MapUploadedFile](/Framework/Annotations/MapUploadedFile/) for a better way to handle this.\n\n## File Response\n\nAn [AHTTP::BinaryFileResponse](/HTTP/BinaryFileResponse) may be used to return static files/content.\nThis response type handles caching, partial requests, and setting the relevant headers.\nThe Athena Framework also supports downloading of dynamically generated content by using an [AHTTP::Response](/HTTP/Response) with the [content-disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header.\n[AHTTP::HeaderUtils.make_disposition](/HTTP/HeaderUtils/#Athena::HTTP::HeaderUtils.make_disposition(disposition,filename,fallback_filename)) can be used to easily build the header.\n\n```crystal\nrequire \"athena\"\nrequire \"mime\"\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(path: \"/data/export\")]\n  def data_export : AHTTP::Response\n    content = # ...\n\n    AHTTP::Response.new(\n      content,\n      headers: HTTP::Headers{\n        \"content-disposition\" => ATH::HeaderUtils.make_disposition(:attachment, \"data.csv\"),\n        \"content-type\" => MIME.from_extension(\".csv\")\n      }\n    )\n  end\nend\n\nATH.run\n```\n\n### Static Files\n\nStatic files can also be served from an Athena application.\nThis can be achieved by combining an [AHTTP::BinaryFileResponse](/HTTP/BinaryFileResponse) with the [request](./middleware.md#1-request-event) event;\nchecking if the request's path represents a file/directory within the application's public directory and returning the file if so.\n\n```crystal\n# Register a request event listener to handle returning static files.\n@[ADI::Register]\nstruct StaticFileListener\n  # This could be parameter if the directory changes between environments.\n  private PUBLIC_DIR = Path.new(\"public\").expand\n\n  # Run this listener with a very high priority so it is invoked before any application logic.\n  @[AEDA::AsEventListener(priority: 256)]\n  def on_request(event : AHK::Events::Request) : Nil\n    # Fallback if the request method isn't intended for files.\n    # Alternatively, a 405 could be thrown if the server is dedicated to serving files.\n    return unless event.request.method.in? \"GET\", \"HEAD\"\n\n    original_path = event.request.path\n    request_path = URI.decode original_path\n\n    # File path cannot contains '\\0' (NUL).\n    if request_path.includes? '\\0'\n      raise AHK::Exception::BadRequest.new \"File path cannot contain NUL bytes.\"\n    end\n\n    request_path = Path.posix request_path\n    expanded_path = request_path.expand \"/\"\n\n    file_path = PUBLIC_DIR.join expanded_path.to_kind Path::Kind.native\n\n    is_dir = Dir.exists? file_path\n    is_dir_path = original_path.ends_with? '/'\n\n    event.response = if request_path != expanded_path || is_dir && !is_dir_path\n                       redirect_path = expanded_path\n                       if is_dir && !is_dir_path\n                         redirect_path = expanded_path.join \"\"\n                       end\n\n                       # Request is a directory but acting as a file,\n                       # redirect to the actual directory URL.\n                       AHTTP::RedirectResponse.new redirect_path\n                     elsif File.file? file_path\n                       AHTTP::BinaryFileResponse.new file_path\n                     else\n                       # Nothing to do.\n                       return\n                     end\n  end\nend\n```\n\n## URL Generation\n\nA common use case, especially when rendering `HTML`, is generating links to other routes based on a set of provided parameters.\nWhen in the context of a request, the scheme and hostname of a [ART::Generator::ReferenceType::ABSOLUTE_URL](/Routing/Generator/ReferenceType/#Athena::Routing::Generator::ReferenceType::ABSOLUTE_URL) defaults to `http` and `localhost` respectively, if they could not be extracted from the request.\n\n### In Controllers\n\nThe parent [ATH::Controller](/Framework/Controller) type provides some helper methods for generating URLs within the context of a controller.\n\n```crystal\nrequire \"athena\"\n\nclass ExampleController < ATH::Controller\n  # Define a route to redirect to, explicitly naming this route `add`.\n  # The default route name is controller + method down snake-cased; e.x. `example_controller_add`.\n  @[ARTA::Get(\"/add/{value1}/{value2}\", name: \"add\")]\n  def add(value1 : Int32, value2 : Int32, negative : Bool = false) : Int32\n    sum = value1 + value2\n    negative ? -sum : sum\n  end\n\n  # Define a route that redirects to the `add` route with fixed parameters.\n  @[ARTA::Get(\"/\")]\n  def redirect : AHTTP::RedirectResponse\n    # Generate a link to the other route.\n    url = self.generate_url \"add\", value1: 8, value2: 2\n\n    url # => /add/8/2\n\n    # Redirect to the user to the generated url.\n    self.redirect url\n\n    # Or could have used a method that does both\n    self.redirect_to_route \"add\", value1: 8, value2: 2\n  end\nend\n\nATH.run\n\n# GET / # => 10\n```\n\nNOTE: Passing arguments to `#generate_url` that are not part of the route definition are included within the query string of the generated URL.\n```crystal\nself.generate_url \"blog\", page: 2, category: \"Crystal\"\n# The \"blog\" route only defines the \"page\" parameter; the generated URL is:\n# /blog/2?category=Crystal\n```\n\n### In Services\n\nA service can define a constructor parameter typed as [ART::Generator::Interface](/Routing/Generator/Interface) in order to obtain the `router` service:\n\n```crystal\n@[ADI::Register]\nclass SomeService\n  def initialize(@url_generator : ART::Generator::Interface); end\n\n  def some_method : Nil\n    sign_up_page = @url_generator.generate \"sign_up\"\n\n    # ...\n  end\nend\n```\n\n### In Commands\n\nGenerating URLs in [commands](./commands.md) works the same as in a service.\nHowever, commands are not executed in an HTTP context.\nBecause of this, absolute URLs will always generate as `http://localhost/` instead of your actual host name.\n\nThe solution to this is to configure the [framework.router.default_uri](/Framework/Bundle/Schema/Router/#Athena::Framework::Bundle::Schema::Router#default_uri) configuration value.\nThis'll ensure URLs generated within commands have the proper host.\n\n```crystal\nATH.configure({\n  framework: {\n    router: {\n      default_uri: \"https://example.com/my/path\",\n    },\n  },\n})\n```\n\n## WebSockets\n\nCurrently due to Athena Framework's [architecture](./middleware.md#events), WebSockets are not directly supported.\nHowever the framework does allow prepending [HTTP::Handler](https://crystal-lang.org/api/HTTP/Handler.html) to the internal server.\nThis could be used to leverage the standard library's [HTTP::WebSocketHandler](https://crystal-lang.org/api/HTTP/WebSocketHandler.html) handler\nor a third party library such as https://github.com/cable-cr/cable.\n\n```crystal\nrequire \"athena\"\n\n# ...\n\nws_handler = HTTP::WebSocketHandler.new do |ws, ctx|\n  ws.on_ping { ws.pong ctx.request.path }\nend\n\nATH.run prepend_handlers: [ws_handler]\n```\n\nAlternatively, the [Athena::Mercure](/Mercure) component may be used as a replacement of the more common websocket use cases.\n\n## Content Negotiation\n\nAs mentioned earlier, controller action responses are JSON serialized if the controller action does _NOT_ return an [AHTTP::Response](/HTTP/Response).\nThe [Negotiation](/Negotiation) component enhances the view layer of the Athena Framework by enabling [content negotiation](https://tools.ietf.org/html/rfc7231#section-5.3) support; making it possible to write format agnostic controllers by placing a layer of abstraction between the controller and generation of the final response content.\nOr in other words, allow having the same controller action be rendered based on the request's [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) header and the format priority configuration.\n\n### Format Priority\n\nThe content negotiation logic is disabled by default, but can be easily enabled via the related [bundle configuration](./configuration.md).\nContent negotiation configuration is represented by an array of [rules](/Framework/Bundle/Schema/FormatListener/#Athena::Framework::Bundle::Schema::FormatListener#rules) used to describe allowed formats, their priorities, and how things should function if a unsupported format is requested.\n\nFor example, say we configured things like:\n\n```crystal\nATH.configure({\n  framework: {\n    format_listener: {\n      enabled: true,\n      rules:   [\n        # Setting fallback_format to json means that instead of considering\n        # the next rule in case of a priority mismatch, json will be used.\n        {priorities: [\"json\", \"xml\"], host: /api\\.example\\.com/, fallback_format: \"json\"},\n\n        # Setting fallback_format to false means that instead of considering\n        # the next rule in case of a priority mismatch, a 406 will be returned.\n        {path: /^\\/image/, priorities: [\"jpeg\", \"gif\"], fallback_format: false},\n\n        # Setting fallback_format to nil (or not including it) means that\n        # in case of a priority mismatch the next rule will be considered.\n        {path: /^\\/admin/, priorities: [\"xml\", \"html\"]},\n\n        # Setting a priority to */* basically means any format will be matched.\n        {priorities: [\"text/html\", \"*/*\"], fallback_format: \"html\"},\n      ],\n    },\n  },\n})\n```\n\nAssuming an `accept` header with the value `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json`: a request made to `/foo` from the `api.example.com` hostname; the request format would be `json`. If the request was not made from that hostname; the request format would be `html`. The rules can be as complex or as simple as needed depending on the use case of your application.\n\n### View Handler\n\nThe [ATH::View::ViewHandler](/Framework/View/ViewHandler) is responsible for generating an [AHTTP::Response](/HTTP/Response) in the format determined by the [ATH::Listeners::Format](/Framework/Listeners/Format), otherwise falling back on the request's [format](/HTTP/Request/#Athena::HTTP::Request#format(mime_type)), defaulting to `json`.\nThe view handler has a options that may also be [configured](./configuration.md) via the [ATH::Bundle::Schema::ViewHandler](/Framework/Bundle/Schema/ViewHandler) schema.\n\n```crystal\nATH.configure({\n  framework: {\n    view_handler: {\n      # The HTTP::Status to use if there is no response body, defaults to 204.\n      empty_content_status: :im_a_teapot,\n\n      # If `nil` values should be serialized, defaults to false.\n      serialize_nil: true\n    },\n  },\n})\n```\n\n## Views\n\nAn [ATH::View](/Framework/View) is intended to act as an in between returning raw data and an [AHTTP::Response](/HTTP/Response).\nIn other words, it still invokes the [view](./middleware.md#4-view-event) event, but allows customizing the response's status and headers.\nConvenience methods are defined in the base controller type to make creating views easier. E.g. [ATH::Controller#view](/Framework/Controller/#Athena::Framework::Controller#view(data,status,headers)).\n\n### View Format Handlers\n\nBy default the Athena Framework uses `json` as the default response format.\nHowever it is possible to extend the [ATH::View::ViewHandler](/Framework/View/ViewHandler) to support additional, and even custom, formats.\nThis is achieved by creating an [ATH::View::FormatHandlerInterface](/Framework/View/FormatHandlerInterface) instance that defines the logic needed to turn an [ATH::View](/Framework/View) into an [AHTTP::Response](/HTTP/Response).\n\nThe implementation can be as simple/complex as needed for the given format.\nOfficial handlers could be provided in the future for common formats such as `html`, probably via an integration with some form of tempting engine utilizing [custom annotations](./configuration.md#custom-annotations) to specify the format.\n\n### Adding/Customizing Formats\n\n[AHTTP::Request::FORMATS](/HTTP/Request/#Athena::HTTP::Request::FORMATS) represents the formats supported by default.\nHowever this list is not exhaustive and may need altered application to application; such as [registering](/HTTP/Request/#Athena::HTTP::Request.register_format(format,mime_types)) new formats.\n\n#### Example\n\nThe following is a demonstration of how the various negotiation features can be used in conjunction. The example includes:\n\n1. Defining a custom [ATH::View::ViewHandler](/Framework/View/ViewHandler) for the `csv` format.\n1. Enabling content negotiation, supporting `json` and `csv` formats, falling back to `json`.\n1. An endpoint returning an [ATH::View](/Framework/View) that sets a custom HTTP status.\n\n```crystal\nrequire \"athena\"\nrequire \"csv\"\n\n# An interface to denote a type can provide its data in CSV format.\n#\n# An easier/more robust implementation can probably be thought of,\n# however this is mainly for demonstration purposes.\nmodule CSVRenderable\n  abstract def to_csv(builder : CSV::Builder) : Nil\nend\n\n# Define an example entity type.\nrecord User, id : Int64, name : String, email : String do\n  include CSVRenderable\n  include JSON::Serializable\n\n  # Define the headers this type has.\n  def self.headers : Enumerable(String)\n    {\n      \"id\",\n      \"name\",\n      \"email\",\n    }\n  end\n\n  def to_csv(builder : CSV::Builder) : Nil\n    # Add the related values based on `self.`\n    builder.row @id, @name, @email\n  end\nend\n\n# Register our handler as a service.\n@[ADI::Register]\nclass CSVFormatHandler\n  # Implement the interface.\n  include ATH::View::FormatHandlerInterface\n\n  # :inherit:\n  def call(view_handler : ATH::View::ViewHandlerInterface, view : ATH::ViewBase, request : AHTTP::Request, format : String) : AHTTP::Response\n    view_data = view.data\n\n    headers = if view_data.is_a? Enumerable\n                typeof(view_data.first).headers\n              else\n                view_data.class.headers\n              end\n\n    data = if view_data.is_a? Enumerable\n             view_data\n           else\n             {view_data}\n           end\n\n    # Assume each item has the same headers.\n    content = CSV.build do |csv|\n      csv.row headers\n\n      data.each do |r|\n        r.to_csv csv\n      end\n    end\n\n    # Return an AHTTP::Response with the rendered CSV content.\n    # Athena handles setting the proper content-type header based on the format.\n    # But could be overridden here if so desired.\n    AHTTP::Response.new content\n  end\n\n  # :inherit:\n  def format : String\n    \"csv\"\n  end\nend\n\nATH.configure({\n  framework: {\n    format_listener: {\n      enabled: true,\n      rules:   [\n        # Allow json and csv formats, falling back on json if an unsupported format is requested.\n        {priorities: [\"json\", \"csv\"], fallback_format: \"json\"}\n      ]\n    },\n  }\n})\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"/users\")]\n  def get_users : ATH::View(Array(User))\n    self.view([\n      User.new(1, \"Jim\", \"jim@example.com\"),\n      User.new(2, \"Bob\", \"bob@example.com\"),\n      User.new(3, \"Sally\", \"sally@example.com\"),\n    ], status: :im_a_teapot)\n  end\nend\n\nATH.run\n```\n"
  },
  {
    "path": "docs/getting_started/testing.md",
    "content": "One of the benefits of using the Athena Framework is testing is considered a first class citizen.\nBoth the framework and the components themselves provides testing utilities to help ensure your code is working as expected.\n\nA small amount of setup is required to make use of the testing features provided by the framework.\nIf you created your project via the [skeleton](https://github.com/athena-framework/skeleton) template repository, then everything is ready for use out of the box.\nOtherwise, ensure that your `spec/spec_helper.cr` file includes the following requires/method calls, in this order:\n\n```crystal\nrequire \"spec\"\nrequire \"../src/main\" # Or whatever the name of your entrypoint file is called\nrequire \"athena/spec\"\n\n# ...\n\nASPEC.run_all\n```\n\nWARNING: It's important that your main entrypoint file is required _before_ `athena/spec`.\n\n## TestCase\n\nAt the core is the [Athena::Spec](/Spec) component, with [ASPEC::TestCase](/Spec/TestCase) being the primary type.\n`ASPEC::TestCase` provides an alternative DSL for creating tests compliant with the stdlib's [Spec](https://crystal-lang.org/api/Spec.html) module.\n\nNOTE: `ASPEC::TestCase` is _NOT_ a standalone testing framework, but is fully intended to be mixed with standard `describe`, `it`, and/or `pending` blocks depending on which approach makes the most sense for what is being tested.\n\nThe primary benefit of this approach is that logic is more easily shared/reused as compared to the normal block based approach.\nI.e. a component can provide a base test case type that can be inherited from, a few methods implemented, and tada.\nFor example, [AVD::Spec::ConstraintValidatorTestCase](/Validator/Spec/ConstraintValidatorTestCase).\n\n```crystal\nstruct ExampleSpec < ASPEC::TestCase\n  def test_add : Nil\n    (1 + 2).should eq 3\n  end\nend\n```\n\nTIP: The [ASPEC::TestCase::DataProvider](/Spec/TestCase/DataProvider) and [ASPEC::TestCase::TestWith](/Spec/TestCase/TestWith) annotations can make testing similar code with different inputs super easy!\n\n## Testing Services\n\nTesting a type/service is best done in isolation, using mocked versions of its dependencies to ensure that specific type is working as expected.\nIn most cases this can be as simple as defining a private class that includes/implements an interface along with additional inputs for asserting it was called as expected.\nIn other cases, the related component may provide these out of the box, such as:\n\n* [AED::Spec::TracableEventDispatcher](/EventDispatcher/Spec/TracableEventDispatcher) For testing types that depend upon a [AED::EventDispatcherInterface](/EventDispatcher/EventDispatcherInterface)\n* [ACLK::Spec::MockClock](/Clock/Spec/MockClock) For testing time sensitive types\n* [AVD::Spec::FailingConstraint](/Validator/Spec/FailingConstraint) For testing invalid constraint related logic\n\nCheckout the `Spec` namespace of each component in the [API Reference](../api_reference.md) for more examples.\n\n## Testing Controllers\n\nWhile testing a service in isolation is a good starting point; it does not make the most sense for all types of services.\nA perfect example of this are [ATH::Controller](/Framework/Controller)s.\nControllers are best tested in conjunction with the various moving parts that make them function.\n\nTo make this as easy as possible, the framework provides [ATH::Spec::APITestCase](/Framework/Spec/APITestCase) and provides many helpful `HTTP` related [expectations](/Framework/Spec/Expectations/HTTP).\n\n```crystal\nrequire \"athena\"\nrequire \"athena/spec\"\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"/add/{value1}/{value2}\")]\n  def add(value1 : Int32, value2 : Int32, @[ATHA::MapQueryParameter] negative : Bool = false) : Int32\n    sum = value1 + value2\n    negative ? -sum : sum\n  end\nend\n\nstruct ExampleControllerTest < ATH::Spec::APITestCase\n  def test_add_positive : Nil\n    self.get(\"/add/5/3\").body.should eq \"8\"\n  end\n\n  def test_add_negative : Nil\n    self.get(\"/add/5/3?negative=true\").body.should eq \"-8\"\n  end\nend\n\n# Run all test case tests.\nASPEC.run_all\n```\n\n## Testing Commands\n\nSimilar to controllers, [commands](./commands.md) also have additional moving parts that need to accounted for when testing.\nThe [ACON::Spec::CommandTester](/Console/Spec/CommandTester) type can be used to simplify this:\n\n```crystal\ndescribe AddCommand do\n  describe \"#execute\" do\n    it \"without negative option\" do\n      tester = ACON::Spec::CommandTester.new AddCommand.new\n      tester.execute value1: 10, value2: 7\n      tester.display.should eq \"The sum of the values is: 17\\n\"\n    end\n\n    it \"with negative option\" do\n      tester = ACON::Spec::CommandTester.new AddCommand.new\n      tester.execute value1: -10, value2: 5, \"--negative\": nil\n      tester.display.should eq \"The sum of the values is: 5\\n\"\n    end\n  end\nend\n```\n"
  },
  {
    "path": "docs/getting_started/validation.md",
    "content": "The [Athena::Validator](/Validator) component adds a robust/flexible validation framework.\nThis component is also mostly optional, but is leveraged for the super useful [ATHR::RequestBody](/Framework/Controller/ValueResolvers/RequestBody) resolver type to ensure only valid data make it into the system.\nThis component can also be used to define validation requirements for [ATH::Params::ParamInterface](/Framework/Params/ParamInterface)s.\n\n## Custom Constraints\n\nIn addition to the general information for defining [Custom Constraints](/Validator/#Athena::Validator--custom-constraints), the validator component defines a specific type for defining service based constraint validators: `AVD::ServiceConstraintValidator`.\nThis type should be inherited from instead of `AVD::ConstraintValidator` _IF_ the validator for your custom constraint needs to be a service, E.x.\n\n```crystal\nclass Athena::Validator::Constraints::CustomConstraint < AVD::Constraint\n  # ...\n\n  @[ADI::Register]\n  struct Validator < AVD::ServiceConstraintValidator\n    def initialize(...); end\n\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::CustomConstraint) : Nil\n      # ...\n    end\n  end\nend\n```\n"
  },
  {
    "path": "docs/guides/README.md",
    "content": "This section of the documentation includes various guides for high level features that don't fit anywhere else.\n"
  },
  {
    "path": "docs/guides/proxies.md",
    "content": "It's usually considered a best practice to run an application behind a reverse proxy or load balancer.\nFor the most part, this doesn't cause any problems with Athena.\nBut, when a request passes through a proxy, certain request information is sent using either the standard `forwarded` header or `x-forwarded-*` headers.\nFor example, instead of reading the request's [`#remote_address`](https://crystal-lang.org/api/HTTP/Request.html#remote_address%3ASocket%3A%3AAddress%7CNil-instance-method) (which will now be the IP address of your reverse proxy), the scheme of the original request will be stored in a standard `Forwarded: proto=\"...\"` header or a `x-forwarded-proto` header.\n\nIf you don't configure Athena to look for these headers, you'll get incorrect information about the request, such as if the client is connecting via HTTPS, the client's port and the hostname being requested.\n\n## Trusted Proxies\n\nTo solve this problem, you need to tell Athena which IP addresses belong to proxies you trust, and which headers the proxy uses to send information.\nThis can be accomplished via the [`framework.trusted_proxies`](/Framework/Bundle/Schema/#Athena::Framework::Bundle::Schema#trusted_proxies) and [`framework.trusted_headers`](http://localhost:8000/Framework/Bundle/Schema/#Athena::Framework::Bundle::Schema#trusted_headers) configuration properties respectively.\n\n```crystal\nATH.configure({\n  framework: {\n    # The IP address (or range) of your proxy.\n    trusted_proxies: [\"192.0.0.1\", \"10.0.0.0/8\"],\n\n    # Trust only `x-forwarded-port` and `x-forwarded-proto` headers.\n    trusted_headers: AHTTP::Request::ProxyHeader[:forwarded_port, :forwarded_proto]\n  },\n})\n```\n\nDANGER: Enabling the [AHTTP::Request::ProxyHeader::FORWARDED_HOST](/HTTP/Request/ProxyHeader/#Athena::HTTP::Request::ProxyHeader::FORWARDED_HOST) option exposes the application to [HTTP Host header attacks](https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html).\nMake sure the proxy really sends an `x-forwarded-host` header to avoid client supplied ones being passed through.\n\nWARNING: The \"trusted proxies\" feature does not work as expected when using the [nginx realip module](https://nginx.org/en/docs/http/ngx_http_realip_module.html).\nDisable that module when serving Athena applications.\n\n## Dynamic IPs\n\nSome proxies do not have static IP addresses or even a range that you can target with CIDR notation.\nIn this case, you need to - _very carefully_ - trust _all_ proxies.\n\n1. Ensure your web server(s) do _NOT_ respond to traffic from _ANY_ clients other than your load balancer.\n1. Once you're guaranteed that traffic will only come from your trusted proxies, configure Athena to _always_ trust incoming request:\n\n```crystal\nATH.configure({\n  framework: {\n    # The `\"REMOTE_ADDRESS\"` string will be replaced by the IP address from the request's `#remote_address`.\n    trusted_proxies: [\"127.0.0.1\", \"REMOTE_ADDRESS\"],\n  },\n})\n```\n\nThat's it! It's critical that you prevent traffic from all non-trusted sources. If you allow outside traffic, they could \"spoof\" their true IP address and other information.\n\n<!-- ## Reverse Proxy in a Subpath -->\n\n## Custom Headers\n\nSome reverse proxies do not use the common `x-forwarded-*` header names and may force you to use a custom header.\nIn such cases you can use the [`framework.trusted_header_overrides`](/Framework/Bundle/Schema/#Athena::Framework::Bundle::Schema#trusted_header_overrides) configuration property to handle this:\n\n```crystal\nATH.configure({\n  framework: {\n    # Tell Athena to look for `cloudfront-forwarded-proto` instead of the default `x-forwarded-proto`.\n    trusted_header_overrides: {\n      :forwarded_proto => \"cloudfront-forwarded-proto\",\n    },\n  },\n})\n```\n"
  },
  {
    "path": "docs/index.cr",
    "content": "# This file is included by each component when building docs.\n# Can be used to create things in the docs that are common to all components.\n# Currently it is just used to ensure the top level `Athena` module exists.\n\nmodule Athena; end\n"
  },
  {
    "path": "docs/templates/crystal/material/schema.html",
    "content": "{% macro render_members(members, obj, heading_level, parent_id, parent_short_name) %}\n  {% for member in members %}\n    {% filter heading(heading_level, id=\"%s.%s\" % (parent_id, member['name']), class=\"doc schema-heading\", toc_label=\"%s.%s\" % (parent_short_name, member['name'])) -%}\n      {{ member['name'] | code_highlight(title='', language=\"crystal\", inline=True) }}\n    {%- endfilter %}\n\n    <div class=\"schema-type\">\n      <strong>type: </strong>{{ member['type'] |convert_markdown_ctx(obj, heading_level+1, obj.abs_id) }}\n    </div>\n\n      <div class=\"schema-default\">\n        {% if member['default'] != '``' %}\n          <strong>default: </strong>{{ member['default'] |convert_markdown_ctx(obj, heading_level+1, obj.abs_id) }}\n        {% else %}\n          <strong>Required</strong>\n        {% endif %}\n      </div>\n\n    <div class=\"doc doc-contents {% if root %}first{% endif %}\">\n      {{ member['doc'] | convert_markdown_ctx(obj, heading_level+1, obj.abs_id) }}\n    </div>\n\n    {% if 'members' in member %}\n      <div class=\"schema-members\">\n        <p>This property consists of an object with the following properties:</p>\n        <blockquote style=\"color: inherit;\">\n          {{ render_members(member['members'], obj, heading_level+1, \"%s.%s\" % (parent_id, member['name']), \"%s.%s\" % (parent_short_name, member['name'])) }}\n        </blockquote>\n      </div>\n    {% endif %}\n    {% if not loop.last %}<hr>{% endif %}\n  {% endfor %}\n{% endmacro %}\n\n{% for param in (obj.constants['CONFIG_DOCS'].value|from_json) %}\n  {% if loop.first %}\n    <h2>Configuration Properties</h2>\n  {% endif %}\n\n  {% set obj = obj.instance_methods.__getitem__(param['name']) %}\n  <div class=\"doc doc-object doc-method doc-doc-instance_method\">\n    {% filter heading(heading_level+2, id=obj.abs_id, class=\"doc schema-heading\", toc_label=obj.short_name) -%}\n      {{ param['name'] | code_highlight(title='', language=\"crystal\", inline=True) }}\n    {%- endfilter %}\n    <div class=\"schema-type\">\n      <strong>type: </strong>{{ param['type'] |convert_markdown_ctx(obj, heading_level+2, obj.abs_id) }}\n    </div>\n\n\n      <div class=\"schema-default\">\n        {% if param['default'] != '``' %}\n          <strong>default: </strong>{{ param['default'] |convert_markdown_ctx(obj, heading_level+2, obj.abs_id) }}\n        {% else %}\n          <strong>Required</strong>\n        {% endif %}\n      </div>\n\n    <div class=\"doc doc-contents {% if root %}first{% endif %}\">\n      {% if obj.doc %}{{ obj.doc | convert_markdown_ctx(obj, heading_level, obj.abs_id) }}{% endif %}\n    </div>\n\n    {% if 'members' in param %}\n      <div class=\"schema-members\">\n        {% if 'Array' in param['type'] %}\n          <p>This property consists of an array of objects with the following properties:</p>\n        {% elif 'Hash' in param['type'] %}\n          <p>This property consists of a map of key/value pairs where each value has the following properties:</p>\n        {% else %}\n          <p>This property consists of an object with the following properties:</p>\n        {% endif %}\n\n        <blockquote style=\"color: inherit;\">\n          {{ render_members(param['members'], obj, heading_level+3, obj.abs_id, obj.short_name) }}\n        </blockquote>\n      </div>\n    {% endif %}\n  </div>\n  {% if not loop.last %}<hr>{% endif %}\n{% endfor %}\n"
  },
  {
    "path": "docs/templates/crystal/material/type.html",
    "content": "{{ log.debug() }}\n\n<div class=\"doc doc-object doc-type {{ obj.kind }}\">\n{% if \"Athena::DependencyInjection::Extension::Schema\" in obj.included_modules  %}\n  <div class=\"doc doc-contents {% if root %}first{% endif %}\">\n    {% if obj.parent %}\n      {% filter heading(heading_level, id=obj.abs_id, class=\"doc doc-heading\", toc_label=obj.name) -%}\n        <code>{{ obj.full_name }}</code>\n      {%- endfilter %}\n    {% endif %}\n\n    {% if obj.doc %}{{ obj.doc |convert_markdown_ctx(obj, heading_level, obj.abs_id) }}{% endif %}\n\n    {% include \"schema.html\" with context %}\n  </div>\n{% else %}\n  {% if obj.parent %}\n  {% filter heading(heading_level, id=obj.abs_id, class=\"doc doc-heading\", toc_label=obj.name) -%}\n    {% if obj.is_abstract %}abstract {% endif %}{{ obj.kind }} <code>{{ obj.full_name }}</code>\n    {% if obj.superclass %}\n      <br/><small>inherits <code>{{ obj.superclass |reference }}</code></small>\n    {% endif %}\n  {%- endfilter %}\n  {% endif %}\n\n  <div class=\"doc doc-contents {% if root %}first{% endif %}\">\n    {% if obj.doc %}{{ obj.doc |convert_markdown_ctx(obj, heading_level, obj.abs_id) }}{% endif %}\n\n    {% with root = False, heading_level = heading_level + 1 %}\n      <div class=\"doc doc-children\">\n        {% if obj.kind == \"alias\" %}\n          <h{{ heading_level }}>Alias definition</h{{ heading_level }}>\n          {{ obj.aliased |code_highlight(language=\"crystal\", inline=True) }}\n        {% endif %}\n\n        {% for title, sub in [\n            (\"Included modules\", obj.included_modules),\n            (\"Extended modules\", obj.extended_modules),\n            (\"Direct known subclasses\", obj.subclasses),\n            (\"Direct including types\", obj.including_types),\n        ] %}\n          {% if sub %}\n            <h{{ heading_level }}>{{ title }}</h{{ heading_level }}>\n            {% for other in sub %}\n              <code>{{ other |reference }}</code>\n            {% endfor %}\n          {% endif %}\n        {% endfor %}\n\n        {% if obj.constants %}\n          {% if obj.kind == \"enum\" %}\n            {% filter heading(heading_level, id=obj.abs_id ~ \"-members\") %}Members{% endfilter %}\n          {% else %}\n            {% filter heading(heading_level, id=obj.abs_id ~ \"-constants\") %}Constants{% endfilter %}\n          {% endif %}\n          {% with heading_level = heading_level + 1 %}\n            {% for obj in obj.constants %}\n              {% include \"constant.html\" with context %}\n            {% endfor %}\n          {% endwith %}\n        {% endif %}\n\n        {% for title, sub in [\n            (\"Constructors\", obj.constructors),\n            (\"Class methods\", obj.class_methods),\n            (\"Methods\", obj.instance_methods),\n            (\"Macros\", obj.macros),\n        ] %}\n          {% if sub %}\n            {% filter heading(heading_level, id=obj.abs_id ~ \"-\" ~ title.lower().replace(\" \", \"-\")) %}{{ title }}{% endfilter %}\n            {% with heading_level = heading_level + 1 %}\n              {% for obj in sub %}\n                {% include \"method.html\" with context %}\n              {% endfor %}\n            {% endwith %}\n          {% endif %}\n        {% endfor %}\n      </div>\n    {% endwith %}\n  </div>\n\n  {% for obj in obj.types %}\n    {% include \"type.html\" with context %}\n  {% endfor %}\n</div>\n{% endif %}\n"
  },
  {
    "path": "docs/why_athena.md",
    "content": "##  Creating \"good\" Software\n\nWhen creating an application, actually writing the code is often the easiest part. Designing a system that will be readable, maintainable, testable, and extensible on the other hand is a much more challenging task. The features of the Athena Framework encourage creating such software. However it does not do much good without also understanding the _why_ behind the way it is designed the way it is. Let's take a moment to explore how the features mentioned in the introduction can lead to \"good\" software design.\n\nWARNING: As with anything in the software world, \"good\" software is subjective. The design decision/suggestions on this page are intended to be educational and provide \"best practices\" guidelines. They are _NOT_ the only way to use the framework nor prescriptive. Do whatever makes the most sense for your project.\n\n### SOLID Principles\n\nThe [SOLID](https://en.wikipedia.org/wiki/SOLID) principles are applicable to any Object Oriented Programming (OOP) language. They play a big part in the underlying architecture of the Athena Framework, and the overall ecosystem of Athena itself. There are plenty of resources online to learn more about all of the principles, but this section will focus on that of the _Dependency Inversion_ and _Single Responsibility_ principles and how an [Inversion of Control (IoC)](https://en.wikipedia.org/wiki/Inversion_of_control) service container orchestrates it all via [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection).\n\n#### Single Responsibility\n\nJust as the name implies, this principle suggests that each type should have only a single primary purpose. Having types with specialized focuses has various benefits including:\n\n* Easier to test\n* Less coupling due to lower amount of dependencies it requires\n* Easier to read and search for\n\nA more concrete example of this could be say there is a class representing an article:\n\n```crystal\nclass Article\n  property title : String\n  property author : String\n  property body : String\n\n  def initialize(@title : String, @author : String, @body : String); end\n\n  def includes_word?(word : String) : Bool\n    @body.includes? word\n  end\n\n  # ...\nend\n```\n\nThis type currently only has a single purpose which is representing an article. It also exposes some helper methods related to querying information about each article which are also valid under this principle. However, if a new method was added to persist the article to some location, the class would now no longer have just one purpose, thus violating the single responsibility principle.\n\nIn this example, it would be better to add _another_ type, say `ArticlePersister` to handle this functionality:\n\n```crystal\n@[ADI::Register]\nclass ArticlePersister\n  def persist(article : Article) : Nil\n    # ...\n  end\nend\n```\n\n##### Services\n\nA sharp eye will notice this type was created with the [ADI::Register](/DependencyInjection/Register/) annotation applied to it. This registers the type as a service, which is essentially just a useful object that could be used by other services. Not all types are services though, such as the `Article` type. This is because it only stores data within the domain of the application and does not provide any useful functionality on its own. More on this topic in the [dependency injection](#dependency-injection) section.\n\n#### Dependency Inversion\n\nThis principle states that code should \"Depend upon abstractions, [not] concretions.\" In other words, services should depend upon interfaces instead of concrete types. This not only makes the depending services more flexible since different implementations of the interface could be used, but also makes testing easier since mock implementations could also be used. In Crystal, an interface is nothing more than a module with abstract defs that can be included within another type in order to force the including type to define its methods.The example from the previous principle can be used to demonstrate.\n\nThe `ArticlePersister` can be used to persist an article. For example say there is another service in which an article should be persisted. This could be a controller action, a console command, some sort of async consumer, etc. The easiest way to handle persisting of the article would be to do something like:\n\n```crystal\n@[ADI::Register]\nclass MyService\n  def execute\n    article = # ...\n    persister = ArticlePersister.new\n\n    persister.persist article\n  end\nend\n```\n\nHowever this has some problems since it tightly couples `MyService` to the `ArticlePersister` service. Not super ideal.\n\n```crystal\ndef initialize\n  @persister = ArticlePersister.new\nend\n```\n\nMoving the persister into an instance variable created within the constructor is a bit better but also suffers from the same issue. The ideal solution here would be to provide an `ArticlePersister` instance to `MyService` when it is instantiated:\n\n```crystal\ndef initialize(\n  @persister : ArticlePersister\n); end\n```\n\nThe same behavior as before can also be retained, even when using this new pattern. This will use the provided instance, or fall back on a default implementation if no custom instance is provided:\n\n```crystal\ndef initialize(\n  persister : ArticlePersister? = nil\n)\n  @persister = persister || ArticlePersister.new\nend\n```\n\nBoth of these latter two examples remove the tight coupling between the two services. However there is still one thing that is less than ideal. It should be possible to persist an article in multiple places. Meaning it needs to allow for more than one implementation of `ArticlePersister` that handles different locations, such as one for a database and another for the local filesystem. The best way to handle this would be to create an interface module for this type:\n\n```crystal\nmodule ArticlePersisterInterface\n  abstract def persist(article : Article) : Nil\nend\n```\n\nFrom here the constructor of `MyService` should be updated to be:\n\n```crystal\ndef initialize(\n  @persister : ArticlePersisterInterface\n); end\n```\n\nAdditionally, a [`@[ADI::AsAlias]`](https://athenaframework.org/DependencyInjection/AsAlias/) must be applied to the class.\nAlso be sure to include the interface within `ArticlePersister`:\n\n```crystal\n@[ADI::Register]\nclass ArticlePersister\n  include ArticlePersisterInterface\n\n  def persist(article : Article) : Nil\n    # ...\n  end\nend\n```\n\nWhile this is a bit of extra boilerplate, it is an incredibly powerful pattern. It enables `MyService` to persist an article to anywhere, depending on what implementation instance it is instantiated with. The same pattern can be extended to make testing the service much easier. A mock implementation of `ArticlePersisterInterface` can be used to assert `MyService` calls with the proper arguments without testing more than is required.\n\n### Flexibility\n\nAthena Framework is very flexible in that it is able to support both simple and complex use cases by adapting to the needs of the application without getting in the way of customizations the user wants to make. This is accomplished by providing all the components to the user, but not requiring they be used. If an application does not need to validate anything, the [Athena::Validator](/Validator/) component can just be ignored. But if the need ever arises it is there and well integrated into the framework.\n\n#### Dependency Injection\n\nAthena Framework includes an IoC Service Container that manages services automatically. Any service, or a useful type, annotated with [ADI::Register](/DependencyInjection/Register/), can be used in another service by defining a constructor typed to the desired service. For example:\n\n```crystal\nrequire \"athena\"\n\n# Register an example service that provides a name string.\n@[ADI::Register]\nclass NameProvider\n  def name : String\n    \"World\"\n  end\nend\n\n# Register another service that depends on the previous service and provides a value.\n@[ADI::Register]\nclass ValueProvider\n  def initialize(@name_provider : NameProvider); end\n\n  def value : String\n    \"Hello \" + @name_provider.name\n  end\nend\n\n# Register a service controller that depends upon the ValueProvider.\n@[ADI::Register]\nclass ExampleController < ATH::Controller\n  def initialize(@value_provider : ValueProvider); end\n\n  @[ARTA::Get(\"/\")]\n  def get_value : String\n    @value_provider.value\n  end\nend\n\nATH.run\n\n# GET / # => \"Hello World\"\n```\n\nIt is worth noting again that while dependency injection is a big part of the framework, it is not necessarily required to fully understand it in order to use the framework, but like the other components, it is there if needed. Checkout [ADI::Register](/DependencyInjection/Register/), especially the [aliasing services](/DependencyInjection/Register/#Athena::DependencyInjection::Register--aliasing-services) section.\n\nAthena Framework is almost fully overridable/customizable in part since it embraces dependency injection. Want to globally customize how errors are rendered? Create a service implementing [AHK::ErrorRendererInterface](/HTTPKernel/ErrorRendererInterface/) and make it an alias of the interface:\n\n```crystal\n@[ADI::Register]\n@[ADI::AsAlias] # Defaults to first included module ending in `Interface`\nclass MyCustomErrorRenderer\n  include AHK::ErrorRendererInterface\n\n  # :inherit:\n  def render(exception : ::Exception) : AHTTP::Response\n    AHTTP::Response.new ...\n  end\nend\n```\n\nAthena Framework will pick this up and use it instead of the built in version without any other required configuration changes. The same concept applies to many different features within the framework that have their own interface/default implementation.\n\n#### Middleware\n\nUnlike other frameworks, Athena Framework leverages event based middleware instead of a pipeline based approach. This enables a lot of flexibility in that there is nothing extra that needs to be done to register the listener other than creating a service for it:\n\n```crystal\n@[ADI::Register]\nclass CustomListener\n  @[AEDA::AsEventListener]\n  def on_response(event : AHK::Events::Response) : Nil\n    event.response.headers[\"FOO\"] = \"BAR\"\n  end\nend\n```\n\nSimilarly, the framework itself is implemented using the same features available to the users. Thus it is very easy to run specific listeners before/after the built-in ones if so desired.\n\nTIP: Check out the `debug:event-dispatcher` command for an easy way to see all the listeners and the order in which they are executed.\n\n### Annotations\n\nOne of the more unique aspects of Athena Framework, and the Athena ecosystem, is its use of [annotations](https://crystal-lang.org/reference/syntax_and_semantics/annotations/index.html) as a means of configuring the framework. While not everyone may like their syntax, the benefits they provide are undeniable. The main benefit being they keep the code close to where it is used. The route of a controller action is declared directly above the method that handles it and not in some other file. Metadata associated with a specific service/route is also right there with the type itself.\n\n#### Point of Extension\n\nA common way to do certain things in other frameworks is the use of macro DSLs specific to each framework. While it can work well, it makes it harder to expand upon/customize. Given annotations are a core Crystal language construct, there nothing special needed to access the annotations themselves. This can be especially useful for third party code to have a tighter integration while also being totally agnostic of what framework the code is even used in.\n\n#### User Defined Annotations\n\nOne of the most powerful features Athena Framework offers is that of custom user defined annotations which provide almost an infinite amount of use cases. These annotations could be applied to controller classes and/or controller actions to expose additional information to other services, such as event listeners or [ATHR::Interfaces](/Framework/Controller/ValueResolvers/Interface/) to customize their behavior on a case by case basis.\n\n```crystal\nrequire \"athena\"\n\n# Define our configuration annotation with an optional `name` argument.\n# A default value can also be provided, or made not nilable to be considered required.\nADI.configuration_annotation MyAnnotation, name : String? = nil\n\n# Define and register our listener that will do something based on our annotation.\n@[ADI::Register]\nclass MyAnnotationListener\n  def initialize(\n    @annotation_resolver : ATH::AnnotationResolver,\n  ); end\n\n  @[AEDA::AsEventListener]\n  def on_view(event : AHK::Events::View) : Nil\n    # Represents all custom annotations applied to the current AHK::Action + controller class.\n    ann_configs = @annotation_resolver.action_annotations(event.request)\n\n    # Check if this action has the annotation\n    unless ann_configs.has? MyAnnotation\n      # Do something based on presence/absence of it.\n      # Would be executed for `ExampleController#one` since it does not have the annotation applied.\n    end\n\n    my_ann = ann_configs[MyAnnotation]\n\n    # Access data off the annotation.\n    if my_ann.name == \"Fred\"\n      # Do something if the provided name is/is not some value.\n      # Would be executed for `ExampleController#two` since it has the annotation applied, and name value equal to \"Fred\".\n    end\n  end\nend\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"one\")]\n  def one : Int32\n    1\n  end\n\n  @[ARTA::Get(\"two\")]\n  @[MyAnnotation(name: \"Fred\")]\n  def two : Int32\n    2\n  end\nend\n\nATH.run\n```\n\n## Primary Use Cases\n\nWhile the components that make up Athena Framework can be used within a wide range of applications, the framework itself is best suited for a few main types, including HTTP REST APIs, CLI Applications, or a combination of both. Since both types of entry points leverage dependency injection, services can be used in both contexts, allowing the majority of code to be reused.\n\n### HTTP REST API\n\nAt its core, Athena Framework is a MVC web application framework. It can be used to serve any kind of content, but best lends itself to creating RESTful JSON APIs due to the features explained in the previous section, as well as due its native JSON support:\n\n* Objects returned from the controller are JSON serialized by default\n* Native support for both [ASR::Serializable](/Serializer/Serializable) and [JSON::Serializable](https://crystal-lang.org/api/JSON/Serializable.html)\n* Native support for DTOs to deserialize and validate, see [ATHR::RequestBody](/Framework/Controller/ValueResolvers/RequestBody/)\n\n```crystal\nrequire \"athena\"\n\nstruct UserCreate\n  include AVD::Validatable\n  include JSON::Serializable\n\n  @[Assert::NotBlank]\n  @[Assert::Email(:html5)]\n  getter email : String\n\n  # ...\nend\n\nclass UserController < ATH::Controller\n  @[ARTA::Post(\"/user\")]\n  @[ATHA::View(status: :created)]\n  def new_user(\n    @[ATHA::MapRequestBody]\n    user_create : UserCreate\n  ) : UserCreate\n    # Use the provided UserCreate instance to create an actual User DB record.\n    # For purposes of this example, just return the instance.\n\n    user_create\n  end\nend\n\nATH.run\n\n# POST /user body: {\"email\":\"athenaframework.org\"} # =>\n# {\n#   \"code\": 422,\n#   \"message\": \"Validation failed\",\n#   \"errors\": [\n#     {\n#       \"property\": \"email\",\n#       \"message\": \"This value is not a valid email address.\",\n#       \"code\": \"ad9d877d-9ad1-4dd7-b77b-e419934e5910\"\n#     }\n#   ]\n# }\n\n# POST /user body: {\"email\":\"contact@athenaframework.org\"} # => {\"email\":\"contact@athenaframework.org\"}\n```\n\n### CLI Applications\n\nAthena Framework can also be used to build CLI based applications. These could either be used directly by the end user, used for internal administrative tasks, or invoked on a schedule via `cron` or something similar.\n\n```crystal\n@[ACONA::AsCommand(\"app:create-user\")]\n@[ADI::Register]\nclass CreateUserCommand < ACON::Command\n  protected def configure : Nil\n    # ...\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    # Implement all the business logic here.\n\n    # Indicates the command executed successfully.\n    Status::SUCCESS\n  end\nend\n```\n\n```shell\n$ ./bin/console\nAthena 0.18.0\n\nUsage:\n  command [options] [arguments]\n\nOptions:\n  -h, --help            Display help for the given command. When no command is given display help for the list command\n  -q, --quiet           Do not output any message\n  -V, --version         Display this application version\n      --ansi|--no-ansi  Force (or disable --no-ansi) ANSI output\n  -n, --no-interaction  Do not ask any interactive question\n  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\n\nAvailable commands:\n  help                    Display help for a command\n  list                    List commands\n app\n  app:create-user\n debug\n  debug:event-dispatcher  Display configured listeners for an application\n  debug:router            Display current routes for an application\n  debug:router:match      Simulate a path match to see which route, if any, would handle it\n```\n\nCheckout the [Console](/Console/) component for more information.\n"
  },
  {
    "path": "gen_doc_stubs.py",
    "content": "# Generates virtual doc files for the mkdocs site.\n# You can also run this script directly to actually write out those files, as a preview.\n\nimport json\nfrom typing import Any\n\nimport markdown as md\nimport mkdocs_gen_files\n\nhandler = mkdocs_gen_files.config[\"plugins\"][\"mkdocstrings\"].get_handler(\"crystal\")\n\n# get the `update_env` method of the handler\nupdate_env = handler.update_env\n\n\n# override the `update_env` method of the handler\ndef patched_update_env(config: dict[str, Any]) -> None:\n    update_env(config)\n\n    def from_json(data):\n        return json.loads(data.removesuffix(\"of Nil\"))\n\n    # patch the filter\n    handler.env.filters[\"from_json\"] = from_json\n\n\n# patch the method\nhandler.update_env = patched_update_env\n\nroot = handler.collector.root\n\n# Determine which namespace this project owns based on site_url\nsite_url = mkdocs_gen_files.config.get(\"site_url\", \"\")\nlocal_ns = site_url.rstrip(\"/\").rsplit(\"/\", 1)[-1] if site_url else None\n\n# Base URL for cross-project links (e.g. \"https://athenaframework.org/\")\nbase_url = site_url.rstrip(\"/\").rsplit(\"/\", 1)[0] + \"/\" if site_url else \"\"\n\n# Get autorefs plugin for registering external type URLs\nautorefs = mkdocs_gen_files.config[\"plugins\"].get(\"autorefs\")\n\n# Source path prefixes for filtering local vs external aliases\nsource_prefixes = [dest.src_path for dest in root.source_locations]\n\nfor type in root.lookup(\"Athena\").walk_types():\n    parts = type.abs_id.split(\"::\")\n    type_ns = parts[1] if len(parts) > 1 else None\n\n    if local_ns and type_ns and type_ns != local_ns and autorefs:\n        # External type: register full URL so autorefs treats it as external\n        external_url = base_url + \"/\".join(parts[1:]) + \"/\"\n        autorefs.register_url(type.abs_id, external_url)\n        continue\n\n    # Athena::Validator::Violation -> Validator/Violation/index.md\n    filename = \"/\".join(parts[2:] + [\"index.md\"])\n\n    # Rename the root `index.md` to `top_level.md` so that the user lands on the introduction page instead of the root component module docs.\n    # But only do this for non-framework components as the site itself is the contextual docs for the framework.\n    if type.full_name != \"Athena::Framework\" and filename == \"index.md\":\n        filename = \"top_level.md\"\n\n    with mkdocs_gen_files.open(filename, \"w\") as f:\n        f.write(f\"# ::: {type.abs_id}\\n\\n\")\n\n    if type.locations:\n        mkdocs_gen_files.set_edit_path(filename, type.locations[0].url)\n\nfor type in root.types:\n    # Write the entry of a top-level alias (e.g. `AED`) to its appropriate section.\n    if type.kind == \"alias\":\n        # Only write aliases whose source is local to this project\n        if source_prefixes and type.locations:\n            is_local = any(\n                loc.filename.startswith(prefix)\n                for loc in type.locations\n                for prefix in source_prefixes\n            )\n            if not is_local:\n                continue\n\n        # Athena::Validator::Annotations -> Validator/aliases.md\n        with mkdocs_gen_files.open(\"aliases.md\", \"a\") as f:\n            f.write(f\"::: {type.abs_id}\\n\\n\")\n"
  },
  {
    "path": "justfile",
    "content": "# Configuration\n\nOUTPUT_DIR := './site'\n\n# Binaries\n# Scoped to the justfile so do not need to be exported\n\nUV := 'uv'\n\n# Needs to be exported so that the `spec` component can pick up on the customized $CRYSTAL env var.\n\nexport CRYSTAL := 'crystal'\n\n_default:\n    @just --list --unsorted\n\n# Installs Crystal shard dependencies\n[group('dev')]\ninstall:\n    SHARDS_OVERRIDE=shard.dev.yml shards update\n\n# Run shard entrypoint with live reload\n[group('dev')]\nwatch shard type='component':\n    watchexec --restart --watch=src/ --emit-events-to=none --clear --no-project-ignore -- {{ CRYSTAL }} run src/{{ type }}s/{{ shard }}/src/{{ if shard == 'framework' { 'athena' } else { 'athena-' + shard } }}{{ if type == 'bundle' { '_bundle' } else { '' } }}.cr\n\n# Run tests with live reload\n[group('dev')]\nwatch-test shard type='component':\n    watchexec --restart --watch=src/ --emit-events-to=none --clear --no-project-ignore -- {{ CRYSTAL }} spec src/{{ type }}s/{{ shard }}/\n\n# Run test suite; `type` is ignored when running test suite for all shards\n[group('dev')]\ntest shard='all' type='component':\n    ./scripts/test.sh {{ shard }} all {{ type }}\n\n# Run unit tests only; `type` is ignored when running test suite for all shards\n[group('dev')]\ntest-unit shard='all' type='component':\n    ./scripts/test.sh {{ shard }} unit {{ type }}\n\n# Run compiled tests only; `type` is ignored when running test suite for all shards\n[group('dev')]\ntest-compiled shard='all' type='component':\n    ./scripts/test.sh {{ shard }} compiled {{ type }}\n\n# Run all linters (format + ameba + spellcheck)\n[group('check')]\nlint: spellcheck format ameba\n\n# Check Crystal formatting\n[group('check')]\nformat:\n    {{ CRYSTAL }} tool format --check\n\n# Fix Crystal formatting issues\n[group('check')]\nformat-fix:\n    {{ CRYSTAL }} tool format\n\n# Run Ameba static analysis\n[group('check')]\nameba:\n    ./bin/ameba\n\n# Run typos spellchecker\n[group('check')]\nspellcheck:\n    typos\n\n# Build the docs\n[group('docs')]\nbuild-docs: _symlink_lib\n    DISABLE_MKDOCS_2_WARNING=true NO_MKDOCS_2_WARNING=1 {{ UV }} run --frozen mkdocs build -d {{ OUTPUT_DIR }}\n\n# Serve live-preview of the docs\n[group('docs')]\nserve-docs: _symlink_lib\n    DISABLE_MKDOCS_2_WARNING=true NO_MKDOCS_2_WARNING=1 {{ UV }} run --frozen mkdocs serve --livereload\n\n# Clean docs build artifacts\n[group('docs')]\nclean-docs:\n    rm -rf {{ OUTPUT_DIR }}\n    find src/ -type d -name \"site\" -exec rm -rf {} +\n\n# Create a new change file\n[group('administrative')]\nchange:\n    changie new\n\n# Batch change files into a version changelog\n[group('administrative')]\nbatch project version='patch':\n    changie batch --project {{ project }} {{ version }}\n\n# Merge pending changelogs into CHANGELOG.md\n[group('administrative')]\nmerge:\n    changie merge\n\n# Upgrade Python dependencies\n[group('administrative')]\nupgrade:\n    {{ UV }} lock --upgrade\n\n# Clean build artifacts and docs\n[group('administrative')]\nclean: clean-docs\n    rm -rf .venv\n\n_symlink_lib:\n    @ for shardDir in $(find -L src/ -maxdepth 3 -type f -name shard.yml | xargs -I{} dirname {} | sort); do \\\n      ln --force --verbose --symbolic {{ (invocation_directory_native() / 'lib') }} \"$shardDir/lib\"; \\\n    done\n"
  },
  {
    "path": "mkdocs-common.yml",
    "content": "theme:\n  name: material\n  palette:\n    - media: '(prefers-color-scheme: light)'\n      scheme: default\n      primary: black\n      accent: red\n      toggle:\n        icon: material/weather-sunny\n        name: Switch to dark theme\n    - media: '(prefers-color-scheme: dark)'\n      scheme: slate\n      primary: black\n      accent: red\n      toggle:\n        icon: material/weather-night\n        name: Switch to light theme\n  features:\n    - navigation.sections\n    - navigation.instant\n    - content.code.copy\n\nuse_directory_urls: true\n\nstrict: false\n\nvalidation:\n  omitted_files: warn\n\n  # TODO: Make this `relative_to_docs` when/if mkdocs-material projects plugin supports it\n  absolute_links: ignore\n  unrecognized_links: warn\n  not_found: warn\n  anchors: warn\n\nextra_css:\n  - ../../../css/index.css\n\nwatch:\n  - src/\n  - ../../../mkdocs-common.yml\n  - ../../../docs/templates\n\nextra:\n  homepage: /\n\nmarkdown_extensions:\n  - admonition\n  - callouts\n  - pymdownx.highlight\n  - pymdownx.magiclink\n  - pymdownx.saneheaders\n  - pymdownx.superfences:\n      custom_fences:\n        - name: mermaid\n          class: mermaid\n          format: !!python/name:pymdownx.superfences.fence_code_format\n  - deduplicate-toc\n  - toc:\n      permalink: '#'\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "INHERIT: ./mkdocs-common.yml\n\nsite_name: Athena\nsite_url: https://athenaframework.org/\nrepo_url: https://github.com/athena-framework/athena\n\nextra_css:\n  - css/index.css\n  - css/monorepo.css\n\nplugins:\n  - search\n  - section-index\n  - projects:\n      projects_dir: src/components\n\nwatch:\n  - ./mkdocs-common.yml\n\nnav:\n  - Introduction: README.md\n  - Why Athena: why_athena.md\n  - Getting Started:\n      - getting_started/README.md\n      - Routing & HTTP: getting_started/routing.md\n      - Configuration: getting_started/configuration.md\n      - Middleware: getting_started/middleware.md\n      - Error Handling: getting_started/error_handling.md\n      - Commands: getting_started/commands.md\n      - Validation: getting_started/validation.md\n      - Testing: getting_started/testing.md\n  - Guides:\n      - guides/README.md\n      - Proxies & Load Balancers: guides/proxies.md\n  - Bundle Reference:\n      - bundle_reference.md\n      - project://mercure-bundle\n  - API Reference:\n      - api_reference.md\n      - project://clock\n      - project://console\n      - project://contracts\n      - project://dependency_injection\n      - project://dotenv\n      - project://event_dispatcher\n      - project://framework\n      - project://http\n      - project://http_kernel\n      - project://image_size\n      - project://mercure\n      - project://mime\n      - project://negotiation\n      - project://routing\n      - project://serializer\n      - project://spec\n      - project://validator\n\nextra:\n  social:\n    - icon: fontawesome/brands/github\n      link: https://github.com/athena-framework\n    - icon: fontawesome/brands/discord\n      link: https://discord.gg/TmDVPb3dmr\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"athena-docs\"\nversion = \"0.0.0\"\ndescription = \"Documentation build dependencies for the Athena ecosystem\"\nrequires-python = \">=3.13\"\nlicense = {file = \"LICENSE\"}\nclassifiers = [\n    \"Private :: Do Not Upload\",\n]\ndependencies = [\n    \"mkdocs~=1.6.1\",\n    \"mkdocs-material~=9.7.0\",\n    \"mkdocstrings-crystal~=0.3.9\",\n    \"mkdocs-gen-files~=0.6.1\",\n    \"mkdocs-literate-nav~=0.6.2\",\n    \"mkdocs-section-index>=0.3.10\",\n]\n\n[tool.uv]\nrequired-version = \"~=0.10.0\"\n"
  },
  {
    "path": "scripts/test.sh",
    "content": "#!/usr/bin/env bash\n\n# $1 shard name\n# $2 shard type\nfunction runSpecs() (\n  set -e\n  $CRYSTAL spec \"${DEFAULT_BUILD_OPTIONS[@]}\" \"${DEFAULT_OPTIONS[@]}\" \"src/$2/$1/spec\"\n)\n\n# Runtime coverage generation logic based on https://hannes.kaeufler.net/posts/measuring-code-coverage-in-crystal-with-kcov.\n# Additionally generates a coverage report for unreachable code.\n#\n# Compiled time code generates a macro code coverage report for the entire shard, and each compiled sub-process spec.\n#\n# $1 shard name\n# $2 shard type\nfunction runSpecsWithCoverage() (\n  set -e\n  SHARD_NAME=$1\n  SHARD_TYPE=$2\n  SHARD_PATH=\"$SHARD_TYPE/$SHARD_NAME\"\n  COVERAGE_BIN_PATH=\"./coverage/$SHARD_TYPE/bin/$SHARD_NAME\"\n\n  rm -rf \"./coverage/$SHARD_PATH\"\n  mkdir -p \"./coverage/$SHARD_PATH\" \"./coverage/$SHARD_TYPE/bin\"\n\n  # Build spec binary that covers entire `spec/` directory to run coverage against.\n  echo \"require \\\"../../../src/$SHARD_PATH/spec/**\\\"\" > \"$COVERAGE_BIN_PATH.cr\" && \\\n  $CRYSTAL build \"${DEFAULT_BUILD_OPTIONS[@]}\" \"$COVERAGE_BIN_PATH.cr\" -o \"$COVERAGE_BIN_PATH\" && \\\n  ATHENA_SPEC_COVERAGE_OUTPUT_DIR=\"$(realpath ./coverage/$SHARD_PATH/)\" \\\n    kcov $(if $IS_CI != \"true\"; then echo \"--cobertura-only\"; fi) \\\n      --clean \\\n      --include-path=\"./src/$SHARD_PATH\"\\\n      \"./coverage/$SHARD_PATH\"\\\n      \"$COVERAGE_BIN_PATH\"\\\n      --junit_output=\"./coverage/$SHARD_PATH/junit.xml\"\\\n      \"${DEFAULT_OPTIONS[@]}\"\n\n  if [ \"$SPEC_TYPE\" != \"unit\" ]\n  then\n    # Generate macro coverage report.\n    # The report itself is sent to STDOUT while other output is sent to STDERR.\n    # We can ignore STDERR since those failures would be captured as part of running the specs themselves.\n    $CRYSTAL tool macro_code_coverage --no-color \"$COVERAGE_BIN_PATH.cr\" > \"./coverage/$SHARD_PATH/macro_coverage.root.codecov.json\"\n  fi\n\n  # Only runtime code can be unreachable.\n  if [ \"$SPEC_TYPE\" != \"compiled\" ]\n  then\n    $CRYSTAL tool unreachable --no-color --format=codecov \"$COVERAGE_BIN_PATH.cr\" > \"./coverage/$SHARD_PATH/unreachable.codecov.json\"\n  fi\n)\n\nDEFAULT_BUILD_OPTIONS=(-Dstrict_multi_assign -Dpreview_overload_order --error-on-warnings)\nDEFAULT_OPTIONS=(--order=random)\nCRYSTAL=${CRYSTAL:=crystal}\nHAS_KCOV=$(if command -v \"kcov\" &>/dev/null; then echo \"true\"; else echo \"false\"; fi)\nIS_CI=${CI:=\"false\"}\n\n# Runs the specs for all, or optionally a single shard.\n# Optionally generates code coverage report data as well.\n#\n# $1 - (optional) shard name to runs specs for, or \"all\". Defaults to \"all\".\n# $2 - (optional) \"type\" of specs to run: \"unit\", \"compiled\", or \"all\". Defaults to \"all\".\n# $3 - (optional) \"type\" of the shard: \"component\", \"bundle\". Defaults to \"component\".\n\nSHARD=${1-all}\nSPEC_TYPE=${2-all}\nSHARD_TYPE=${3-component}s\n\nif [ \"$SPEC_TYPE\" == \"unit\" ]\nthen\n  DEFAULT_OPTIONS+=(\"--tag=~compiled\")\nelif [ \"$SPEC_TYPE\" == \"compiled\" ]\nthen\n  DEFAULT_OPTIONS+=(\"--tag=compiled\")\nelif [ \"$SPEC_TYPE\" != \"all\" ]\nthen\n  echo \"Second argument must be 'unit', 'compiled', or 'all' got '${2}'.\"\n  exit 1\nfi\n\nEXIT_CODE=0\n\nif [ \"$SHARD\" != \"all\" ]\nthen\n  if [ \"$HAS_KCOV\" = \"true\" ]\n  then\n    runSpecsWithCoverage \"$SHARD\" \"$SHARD_TYPE\"\n  else\n    runSpecs \"$SHARD\" \"$SHARD_TYPE\"\n  fi\n  exit $?\nfi\n\n# If we got this far we need to run specs for all shards, so cannot just rely on `$SHARD_TYPE`\nfor shardPath in $(find src/ -maxdepth 3 -type f -name shard.yml | xargs -I{} dirname {} | sed 's|^src/||' | sort); do\n  type=${shardPath%/*}\n  name=${shardPath#*/}\n\n  echo \"::group::$shardPath\"\n\n  if [ \"$HAS_KCOV\" = \"true\" ]\n  then\n    runSpecsWithCoverage \"$name\" \"$type\"\n  else\n    runSpecs \"$name\" \"$type\"\n  fi\n\n  if [ $? -eq 1 ]; then\n    EXIT_CODE=1\n  fi\n\n  echo \"::endgroup::\"\ndone\n\nexit $EXIT_CODE\n"
  },
  {
    "path": "shard.dev.yml",
    "content": "# $ SHARDS_OVERRIDE=shard.dev.yml shards update\ndependencies:\n  athena:\n    path: ./src/components/framework\n  athena-clock:\n    path: ./src/components/clock\n  athena-console:\n    path: ./src/components/console\n  athena-contracts:\n    path: ./src/components/contracts\n  athena-dependency_injection:\n    path: ./src/components/dependency_injection\n  athena-dotenv:\n    path: ./src/components/dotenv\n  athena-event_dispatcher:\n    path: ./src/components/event_dispatcher\n  athena-http:\n    path: ./src/components/http\n  athena-http_kernel:\n    path: ./src/components/http_kernel\n  athena-image_size:\n    path: ./src/components/image_size\n  athena-mercure:\n    path: ./src/components/mercure\n  athena-mercure_bundle:\n    path: ./src/bundles/mercure\n  athena-mime:\n    path: ./src/components/mime\n  athena-negotiation:\n    path: ./src/components/negotiation\n  athena-routing:\n    path: ./src/components/routing\n  athena-serializer:\n    path: ./src/components/serializer\n  athena-spec:\n    path: ./src/components/spec\n  athena-validator:\n    path: ./src/components/validator\n"
  },
  {
    "path": "shard.prod.yml",
    "content": "# Used for prod builds of the docs to ensure API docs can be updated w/o a dedicated release\n# $ SHARDS_OVERRIDE=shard.prod.yml shards update\ndependencies:\n  athena:\n    github: athena-framework/framework\n    branch: docs\n  athena-clock:\n    github: athena-framework/clock\n    branch: docs\n  athena-console:\n    github: athena-framework/console\n    branch: docs\n  athena-contracts:\n    github: athena-framework/contracts\n    branch: docs\n  athena-dependency_injection:\n    github: athena-framework/dependency-injection\n    branch: docs\n  athena-dotenv:\n    github: athena-framework/dotenv\n    branch: docs\n  athena-event_dispatcher:\n    github: athena-framework/event-dispatcher\n    branch: docs\n  athena-http:\n    github: athena-framework/http\n    branch: docs\n  athena-http_kernel:\n    github: athena-framework/http-kernel\n    branch: docs\n  athena-image_size:\n    github: athena-framework/image-size\n    branch: docs\n  athena-mercure:\n    github: athena-framework/mercure\n    branch: docs\n  athena-mercure_bundle:\n    github: athena-framework/mercure-bundle\n    branch: docs\n  athena-mime:\n    github: athena-framework/mime\n    branch: docs\n  athena-negotiation:\n    github: athena-framework/negotiation\n    branch: docs\n  athena-routing:\n    github: athena-framework/routing\n    branch: docs\n  athena-serializer:\n    github: athena-framework/serializer\n    branch: docs\n  athena-spec:\n    github: athena-framework/spec\n    branch: docs\n  athena-validator:\n    github: athena-framework/validator\n    branch: docs\n"
  },
  {
    "path": "shard.yml",
    "content": "name: athena-ecosystem\nversion: 0.0.0\n\n# Min Crystal version required to run the test suite/develop on Athena.\ncrystal: ~> 1.17\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/athena\n\ndocumentation: https://athenaframework.org\n\ndescription: |\n  An ecosystem of reusable, independent components.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\ndependencies:\n  athena:\n    github: athena-framework/framework\n  athena-clock:\n    github: athena-framework/clock\n  athena-console:\n    github: athena-framework/console\n  athena-contracts:\n    github: athena-framework/contracts\n  athena-dependency_injection:\n    github: athena-framework/dependency-injection\n  athena-dotenv:\n    github: athena-framework/dotenv\n  athena-event_dispatcher:\n    github: athena-framework/event-dispatcher\n  athena-http:\n    github: athena-framework/http\n  athena-http_kernel:\n    github: athena-framework/http-kernel\n  athena-image_size:\n    github: athena-framework/image-size\n  athena-mercure:\n    github: athena-framework/mercure\n  athena-mercure_bundle:\n    github: athena-framework/mercure-bundle\n  athena-mime:\n    github: athena-framework/mime\n  athena-negotiation:\n    github: athena-framework/negotiation\n  athena-routing:\n    github: athena-framework/routing\n  athena-serializer:\n    github: athena-framework/serializer\n  athena-spec:\n    github: athena-framework/spec\n  athena-validator:\n    github: athena-framework/validator\n\ndevelopment_dependencies:\n  ameba:\n    github: crystal-ameba/ameba\n    version: ~> 1.6.3\n  athena-spec:\n    github: athena-framework/spec\n    version: ~> 0.4.0\n"
  },
  {
    "path": "src/bundles/mercure/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/bundles/mercure/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/bundles/mercure/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.1.0] - 2026-04-19\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/mercure-bundle/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/bundles/mercure/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/bundles/mercure/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2026 George Dietrich\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": "src/bundles/mercure/README.md",
    "content": "# MercureBundle\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/workflows/CI/badge.svg)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/mercure-bundle.svg)](https://github.com/athena-framework/mercure-bundle/releases)\n\nIntegrates the Athena Mercure component into the framework.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/MercureBundle).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/bundles/mercure/shard.yml",
    "content": "name: athena-mercure_bundle\n\nversion: 0.1.0\n\ncrystal: ~> 1.19\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/mercure-bundle\n\ndocumentation: https://athenaframework.org/MercureBundle\n\ndescription: |\n  Integrates the Athena Mercure component into the framework\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\ndependencies:\n  athena-dependency_injection:\n    github: athena-framework/dependency-injection\n    version: ~> 0.4.0\n  athena-http_kernel:\n    github: athena-framework/http-kernel\n    version: ~> 0.1.0\n  athena-mercure:\n    github: athena-framework/mercure\n    version: ~> 0.1.0\n"
  },
  {
    "path": "src/bundles/mercure/spec/authorization_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct BundleAuthorizationTest < ASPEC::TestCase\n  def test_set_cookie : Nil\n    token_factory = AMC::Spec::AssertingTokenFactory.new(\n      \"JWT\",\n      [\"foo\"],\n      [\"bar\"],\n      {\"x-foo\" => \"baz\"},\n    )\n\n    authorization = ABM::Authorization.new new_hub_registry(token_factory: token_factory)\n    request = new_request(headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n\n    authorization.set_cookie request, [\"foo\"], [\"bar\"], {\"x-foo\" => \"baz\"}\n    token_factory.called?.should be_true\n\n    cookies = request.attributes.get?(\"_mercure_authorization_cookies\", Hash(String, ::HTTP::Cookie)).should_not be_nil\n    cookies[\"\"].value.should_not be_empty\n  end\n\n  def test_set_cookie_stores_in_request_attributes : Nil\n    authorization = ABM::Authorization.new new_hub_registry\n    request = new_request(headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n\n    authorization.set_cookie request\n\n    cookies = request.attributes.get?(\"_mercure_authorization_cookies\", Hash(String, ::HTTP::Cookie)).should_not be_nil\n    cookies.size.should eq 1\n  end\n\n  def test_set_cookie_prevents_duplicate : Nil\n    authorization = ABM::Authorization.new new_hub_registry\n    request = new_request(headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n\n    expect_raises AMC::Exception::Runtime, \"The 'mercureAuthorization' cookie for the 'default hub' has already been set.\" do\n      authorization.set_cookie request\n      authorization.set_cookie request\n    end\n  end\n\n  def test_clear_cookie : Nil\n    authorization = ABM::Authorization.new new_hub_registry\n    request = new_request(headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n\n    authorization.clear_cookie request\n\n    cookies = request.attributes.get?(\"_mercure_authorization_cookies\", Hash(String, ::HTTP::Cookie)).should_not be_nil\n    cookies[\"\"].value.should be_empty\n  end\n\n  def test_create_cookie : Nil\n    authorization = ABM::Authorization.new new_hub_registry\n    request = new_request(headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n\n    cookie = authorization.create_cookie request\n    cookie.name.should eq \"mercureAuthorization\"\n    cookie.value.should_not be_empty\n  end\nend\n"
  },
  {
    "path": "src/bundles/mercure/spec/bundle_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compiles code, line: line, preamble: <<-'CR'\n    require \"./spec_helper.cr\"\n\n    @[ADI::Register(public: true)]\n    class MercureConsumer\n      def initialize(\n        @hub : AMC::Hub::Interface,\n        @authorization : ABM::Authorization,\n        @discovery : ABM::Discovery,\n      ); end\n    end\n  CR\nend\n\ndescribe ABM, tags: \"compiled\" do\n  it \"registers services for a hub with a jwt secret\" do\n    assert_compiles <<-'CR'\n      ADI.configure({\n        mercure: {\n          hubs: {\n            default: {\n              url: \"https://hub.example.com/.well-known/mercure\",\n              jwt: {\n                secret:    \"looooooooooooongenoughtestsecret\",\n                publish:   [\"*\"],\n                subscribe: [\"https://example.com/books/{id}\"],\n              },\n            },\n          },\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            sh = ADI::ServiceContainer::SERVICE_HASH\n\n            # Hub service\n            hub = sh[\"mercure_hub_default\"]\n            params = hub[\"parameters\"]\n\n            # JWT factory service\n            factory = sh[\"mercure_hub_default_jwt_factory\"]\n\n            # Token provider service (factory-based)\n            provider = sh[\"mercure_hub_default_jwt_provider\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ hub[\"class\"].resolve == Athena::Mercure::Hub }}, \"Expected hub class to be Athena::Mercure::Hub\")\n          ASPEC.compile_time_assert(\\{{ params[\"url\"][\"value\"] == \"https://hub.example.com/.well-known/mercure\" }}, \"Expected hub url to match configuration\")\n          # Token factory should be set\n          ASPEC.compile_time_assert(\\{{ params[\"token_factory\"][\"value\"] != nil }}, \"Expected token_factory to be set\")\n          ASPEC.compile_time_assert(\\{{ factory[\"class\"].resolve == Athena::Mercure::TokenFactory::JWT }}, \"Expected factory class to be Athena::Mercure::TokenFactory::JWT\")\n          ASPEC.compile_time_assert(\\{{ provider[\"class\"].resolve == Athena::Mercure::TokenProvider::Factory }}, \"Expected provider class to be Athena::Mercure::TokenProvider::Factory\")\n          # Hub registry\n          ASPEC.compile_time_assert(\\{{ sh[\"mercure_hub_registry\"][\"class\"].resolve == Athena::Mercure::Hub::Registry }}, \"Expected registry class to be Athena::Mercure::Hub::Registry\")\n          # Authorization\n          ASPEC.compile_time_assert(\\{{ sh[\"mercure_authorization\"][\"class\"].resolve == Athena::MercureBundle::Authorization }}, \"Expected auth class to be Athena::MercureBundle::Authorization\")\n          # Discovery\n          ASPEC.compile_time_assert(\\{{ sh[\"mercure_discovery\"][\"class\"].resolve == Athena::MercureBundle::Discovery }}, \"Expected discovery class to be Athena::MercureBundle::Discovery\")\n        end\n      end\n    CR\n  end\n\n  it \"registers services for a hub with a static jwt value\" do\n    assert_compiles <<-'CR'\n      ADI.configure({\n        mercure: {\n          hubs: {\n            default: {\n              url: \"https://hub.example.com/.well-known/mercure\",\n              jwt: {\n                value: \"eyJhbGciOiJIUzI1NiJ9.static-token\",\n              },\n            },\n          },\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            sh = ADI::ServiceContainer::SERVICE_HASH\n\n            hub = sh[\"mercure_hub_default\"]\n            params = hub[\"parameters\"]\n\n            # Static token provider\n            provider = sh[\"mercure_hub_default_jwt_provider\"]\n          %}\n          # Token factory should be nil for static token\n          ASPEC.compile_time_assert(\\{{ params[\"token_factory\"][\"value\"] == nil.id }}, \"Expected token_factory to be nil\")\n          ASPEC.compile_time_assert(\\{{ provider[\"class\"].resolve == Athena::Mercure::TokenProvider::Static }}, \"Expected provider class to be Athena::Mercure::TokenProvider::Static\")\n        end\n      end\n    CR\n  end\n\n  it \"uses the first hub as default when default_hub is not set\" do\n    assert_compiles <<-'CR'\n      ADI.configure({\n        mercure: {\n          hubs: {\n            my_hub: {\n              url: \"https://hub.example.com/.well-known/mercure\",\n              jwt: {\n                secret: \"looooooooooooongenoughtestsecret\",\n              },\n            },\n          },\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            default_hub = ADI::ServiceContainer::SERVICE_HASH[\"mercure_hub_registry\"][\"parameters\"][\"default_hub\"][\"value\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ default_hub.stringify =~ /mercure_hub_my_hub/ }}, \"Expected default hub to be my_hub\")\n        end\n      end\n    CR\n  end\n\n  it \"respects explicit default_hub setting\" do\n    assert_compiles <<-'CR'\n      ADI.configure({\n        mercure: {\n          hubs: {\n            first: {\n              url: \"https://first.example.com/.well-known/mercure\",\n              jwt: {\n                secret: \"looooooooooooongenoughtestsecret\",\n              },\n            },\n            second: {\n              url: \"https://second.example.com/.well-known/mercure\",\n              jwt: {\n                secret: \"looooooooooooongenoughtestsecret\",\n              },\n            },\n          },\n          default_hub: \"second\",\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            default_hub = ADI::ServiceContainer::SERVICE_HASH[\"mercure_hub_registry\"][\"parameters\"][\"default_hub\"][\"value\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ default_hub.stringify =~ /mercure_hub_second/ }}, \"Expected default hub to be second\")\n        end\n      end\n    CR\n  end\n\n  it \"retains named aliases for all hubs in a multi-hub configuration\" do\n    assert_compiles <<-'CR'\n      ADI.configure({\n        mercure: {\n          hubs: {\n            first: {\n              url: \"https://first.example.com/.well-known/mercure\",\n              jwt: {\n                secret: \"looooooooooooongenoughtestsecret\",\n              },\n            },\n            second: {\n              url: \"https://second.example.com/.well-known/mercure\",\n              jwt: {\n                secret: \"looooooooooooongenoughtestsecret\",\n              },\n            },\n          },\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            aliases = ADI::ServiceContainer::ALIASES[Athena::Mercure::Hub::Interface]\n\n            # Named aliases for both hubs should be present, plus the unnamed default\n            first_alias = aliases.find { |a| a[\"name\"].id == \"first\" }\n            second_alias = aliases.find { |a| a[\"name\"].id == \"second\" }\n            default_alias = aliases.find { |a| a[\"name\"] == nil }\n          %}\n          ASPEC.compile_time_assert(\\{{ first_alias[\"id\"].stringify =~ /mercure_hub_first/ }}, \"Expected first alias id to match mercure_hub_first\")\n          ASPEC.compile_time_assert(\\{{ second_alias[\"id\"].stringify =~ /mercure_hub_second/ }}, \"Expected second alias id to match mercure_hub_second\")\n          ASPEC.compile_time_assert(\\{{ default_alias[\"id\"].stringify =~ /mercure_hub_first/ }}, \"Expected default alias to be first hub\")\n        end\n      end\n    CR\n  end\n\n  it \"passes cookie_lifetime from configuration\" do\n    assert_compiles <<-'CR'\n      ADI.configure({\n        mercure: {\n          hubs: {\n            default: {\n              url: \"https://hub.example.com/.well-known/mercure\",\n              jwt: {\n                secret: \"looooooooooooongenoughtestsecret\",\n              },\n            },\n          },\n          default_cookie_lifetime: 2.hours,\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            lifetime = ADI::ServiceContainer::SERVICE_HASH[\"mercure_authorization\"][\"parameters\"][\"cookie_lifetime\"][\"value\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ lifetime.stringify == \"2.hours\" }}, \"Expected cookie_lifetime to be 2.hours\")\n        end\n      end\n    CR\n  end\nend\n"
  },
  {
    "path": "src/bundles/mercure/spec/discovery_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct DiscoveryTest < ASPEC::TestCase\n  def test_add_link : Nil\n    discovery = ABM::Discovery.new new_hub_registry\n    request = new_request\n\n    discovery.add_link request\n\n    links = request.attributes.get? \"_links\", Array(String)\n    links.should eq [%(<https://example.com/.well-known/mercure>; rel=\"mercure\")]\n  end\n\n  def test_add_link_with_named_hub : Nil\n    hub = AMC::Spec::MockHub.new(\"https://hub1.example.com/.well-known/mercure\", AMC::TokenProvider::Static.new(\"JWT\")) { \"ID\" }\n\n    registry = AMC::Hub::Registry.new(hub, {\"hub1\" => hub.as(AMC::Hub::Interface)})\n    discovery = ABM::Discovery.new registry\n    request = new_request\n\n    discovery.add_link request, \"hub1\"\n\n    links = request.attributes.get? \"_links\", Array(String)\n    links.should eq [%(<https://hub1.example.com/.well-known/mercure>; rel=\"mercure\")]\n  end\n\n  def test_add_link_accumulates_multiple_links : Nil\n    hub1 = AMC::Spec::MockHub.new(\"https://hub1.example.com/.well-known/mercure\", AMC::TokenProvider::Static.new(\"JWT\")) { \"ID\" }\n    hub2 = AMC::Spec::MockHub.new(\"https://hub2.example.com/.well-known/mercure\", AMC::TokenProvider::Static.new(\"JWT\")) { \"ID\" }\n\n    registry = AMC::Hub::Registry.new(hub1, {\n      \"hub1\" => hub1.as(AMC::Hub::Interface),\n      \"hub2\" => hub2.as(AMC::Hub::Interface),\n    })\n\n    discovery = ABM::Discovery.new registry\n    request = new_request\n\n    discovery.add_link request, \"hub1\"\n    discovery.add_link request, \"hub2\"\n\n    links = request.attributes.get? \"_links\", Array(String)\n    links.should eq [\n      %(<https://hub1.example.com/.well-known/mercure>; rel=\"mercure\"),\n      %(<https://hub2.example.com/.well-known/mercure>; rel=\"mercure\"),\n    ]\n  end\n\n  def test_add_link_skips_preflight_request : Nil\n    discovery = ABM::Discovery.new new_hub_registry\n    request = new_request(method: \"OPTIONS\", headers: ::HTTP::Headers{\"access-control-request-method\" => \"GET\"})\n\n    discovery.add_link request\n\n    request.attributes.has?(\"_links\").should be_false\n  end\nend\n"
  },
  {
    "path": "src/bundles/mercure/spec/listeners/add_link_header_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct AddLinkHeaderListenerTest < ASPEC::TestCase\n  def test_no_links_attribute : Nil\n    event = new_response_event\n\n    ABM::Listeners::AddLinkHeader.new.on_response event\n\n    event.response.headers[\"link\"]?.should be_nil\n  end\n\n  def test_single_link : Nil\n    request = new_request\n    request.attributes.set \"_links\", [%(<https://hub.example.com/.well-known/mercure>; rel=\"mercure\")], Array(String)\n\n    event = new_response_event(request: request)\n\n    ABM::Listeners::AddLinkHeader.new.on_response event\n\n    event.response.headers.get(\"link\").should eq [%(<https://hub.example.com/.well-known/mercure>; rel=\"mercure\")]\n  end\n\n  def test_multiple_links : Nil\n    request = new_request\n    request.attributes.set \"_links\", [\n      %(<https://hub1.example.com/.well-known/mercure>; rel=\"mercure\"),\n      %(<https://hub2.example.com/.well-known/mercure>; rel=\"mercure\"),\n    ], Array(String)\n\n    event = new_response_event(request: request)\n\n    ABM::Listeners::AddLinkHeader.new.on_response event\n\n    event.response.headers.get(\"link\").should eq [\n      %(<https://hub1.example.com/.well-known/mercure>; rel=\"mercure\"),\n      %(<https://hub2.example.com/.well-known/mercure>; rel=\"mercure\"),\n    ]\n  end\n\n  def test_preserves_existing_link_headers : Nil\n    request = new_request\n    request.attributes.set \"_links\", [%(<https://hub.example.com/.well-known/mercure>; rel=\"mercure\")], Array(String)\n\n    response = AHTTP::Response.new\n    response.headers.add \"link\", %(<https://example.com>; rel=\"preload\")\n\n    event = new_response_event(request: request, response: response)\n\n    ABM::Listeners::AddLinkHeader.new.on_response event\n\n    event.response.headers.get(\"link\").should eq [\n      %(<https://example.com>; rel=\"preload\"),\n      %(<https://hub.example.com/.well-known/mercure>; rel=\"mercure\"),\n    ]\n  end\nend\n"
  },
  {
    "path": "src/bundles/mercure/spec/listeners/set_cookie_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct SetCookieListenerTest < ASPEC::TestCase\n  def test_no_cookies_attribute : Nil\n    event = new_response_event\n\n    ABM::Listeners::SetCookie.new.on_response event\n\n    event.response.headers.cookies.should be_empty\n  end\n\n  def test_applies_cookies_to_response : Nil\n    request = new_request\n    cookies = Hash(String, ::HTTP::Cookie).new\n    cookies[\"\"] = ::HTTP::Cookie.new(\"mercureAuthorization\", \"token123\", path: \"/\")\n    request.attributes.set \"_mercure_authorization_cookies\", cookies, Hash(String, ::HTTP::Cookie)\n\n    event = new_response_event(request: request)\n\n    ABM::Listeners::SetCookie.new.on_response event\n\n    event.response.headers.cookies[\"mercureAuthorization\"].value.should eq \"token123\"\n  end\n\n  def test_removes_attribute_after_processing : Nil\n    request = new_request\n    cookies = Hash(String, ::HTTP::Cookie).new\n    cookies[\"\"] = ::HTTP::Cookie.new(\"mercureAuthorization\", \"token123\", path: \"/\")\n    request.attributes.set \"_mercure_authorization_cookies\", cookies, Hash(String, ::HTTP::Cookie)\n\n    event = new_response_event(request: request)\n\n    ABM::Listeners::SetCookie.new.on_response event\n\n    request.attributes.has?(\"_mercure_authorization_cookies\").should be_false\n  end\n\n  def test_applies_multiple_cookies : Nil\n    request = new_request\n    cookies = Hash(String, ::HTTP::Cookie).new\n    cookies[\"\"] = ::HTTP::Cookie.new(\"mercureAuthorization\", \"default-token\", path: \"/\")\n    cookies[\"hub1\"] = ::HTTP::Cookie.new(\"mercureAuthorization\", \"hub1-token\", path: \"/hub1\")\n    request.attributes.set \"_mercure_authorization_cookies\", cookies, Hash(String, ::HTTP::Cookie)\n\n    event = new_response_event(request: request)\n\n    ABM::Listeners::SetCookie.new.on_response event\n\n    # The last cookie with the same name wins in the cookies collection,\n    # but both should have been added via <<\n    event.response.headers.cookies.size.should be >= 1\n  end\nend\n"
  },
  {
    "path": "src/bundles/mercure/spec/spec_helper.cr",
    "content": "require \"spec\"\n\nrequire \"athena-spec\"\n\nrequire \"../src/athena-mercure_bundle\"\n\nrequire \"athena-mercure/src/spec\"\n\nASPEC.run_all\n\ndef new_hub_registry(\n  url : String = \"https://example.com/.well-known/mercure\",\n  token_factory : AMC::TokenFactory::Interface? = AMC::TokenFactory::JWT.new(\"looooooooooooongenoughtestsecret\", jwt_lifetime: 4000),\n) : AMC::Hub::Registry\n  AMC::Hub::Registry.new(AMC::Spec::MockHub.new(url, AMC::TokenProvider::Static.new(\"JWT\"), token_factory: token_factory) { \"ID\" })\nend\n\ndef new_request(\n  *,\n  method : String = \"GET\",\n  path : String = \"/\",\n  headers : ::HTTP::Headers = ::HTTP::Headers.new,\n) : AHTTP::Request\n  AHTTP::Request.new(method, path, headers)\nend\n\ndef new_response_event(\n  request : AHTTP::Request = new_request,\n  response : AHTTP::Response = AHTTP::Response.new,\n) : AHK::Events::Response\n  AHK::Events::Response.new(request, response)\nend\n"
  },
  {
    "path": "src/bundles/mercure/src/athena-mercure_bundle.cr",
    "content": "require \"athena-dependency_injection\"\nrequire \"athena-http_kernel\"\nrequire \"athena-mercure\"\n\nrequire \"./authorization\"\nrequire \"./discovery\"\n\nrequire \"./listeners/*\"\n\n# Convenience alias to make referencing `Athena::MercureBundle` types easier.\nalias ABM = Athena::MercureBundle\n\n# The `Athena::MercureBundle` integrates the `Athena::Mercure` component into the Athena framework.\n@[ADI::Bundle(\"mercure\")]\nstruct Athena::MercureBundle < ADI::AbstractBundle\n  # :nodoc:\n  PASSES = [] of _\n\n  # Represents the possible properties used to configure and customize the Mercure integration.\n  # See the [Getting Started](/getting_started/configuration) docs for more information on how the bundle system works in Athena.\n  #\n  # A full example showing all properties is as follows:\n  #\n  # ```\n  # ADI.configure({\n  #   mercure: {\n  #     hubs: {\n  #       default: {\n  #         url:        \"https://internal-hub/.well-known/mercure\",\n  #         public_url: \"https://hub.example.com/.well-known/mercure\",\n  #         jwt:        {\n  #           # Provide *secret* to generate JWTs dynamically via a token factory...\n  #           secret:     \"my-jwt-secret\",\n  #           publish:    [\"*\"],\n  #           subscribe:  [\"https://example.com/books/{id}\"],\n  #           algorithm:  :hs256,\n  #           passphrase: \"\",\n  #\n  #           # ...or provide *value* to use a static JWT token directly.\n  #           # value: \"eyJhbGciOiJIUzI1NiJ9...\",\n  #         },\n  #       },\n  #     },\n  #     default_hub:             \"default\",\n  #     default_cookie_lifetime: 1.hour,\n  #   },\n  # })\n  # ```\n  module Schema\n    include ADI::Extension::Schema\n\n    # JWT configuration for authenticating with a Mercure hub.\n    # Provide either *secret* to generate tokens dynamically, or *value* to use a static token.\n    #\n    # ---\n    # >>secret: The secret key used to sign JWTs.\n    # Required when generating tokens dynamically via a `AMC::TokenFactory::JWT` (i.e. when *value* is not set).\n    # Should be set via ENV var.\n    # >>publish: Topic selectors that the generated JWT grants publish access to. Included in the JWT's `mercure.publish` claim.\n    # >>subscribe: Topic selectors that the generated JWT grants subscribe access to. Included in the JWT's `mercure.subscribe` claim.\n    # >>algorithm: The signing algorithm used to encode the JWT.\n    # >>passphrase: Passphrase for the secret key, if the algorithm requires one (e.g. RSA).\n    # >>value: A pre-built static JWT token string provided via `AMC::TokenProvider::Static`. When set, the token is used as-is and *secret*, *algorithm*, and *passphrase* are ignored.\n    # ---\n    object_schema JWT,\n      secret : String? = nil,\n      publish : Array(String) = [] of String,\n      subscribe : Array(String) = [] of String,\n      algorithm : ::JWT::Algorithm = :hs256,\n      passphrase : String = \"\",\n      value : String? = nil\n\n    # Named Mercure hub definitions. Each hub requires a *url* and *jwt* configuration.\n    #\n    # ---\n    # >>url: The internal URL used by the server to publish updates to this hub.\n    # Should be set via ENV var.\n    # >>public_url: The public URL exposed to clients via the `Link` header for hub discovery.\n    # Falls back to *url* if not set. Useful when the internal hub URL differs from the one clients should connect to.\n    # Should be set via ENV var.\n    # ---\n    map_of hubs,\n      url : String,\n      public_url : String? = nil,\n      jwt : JWT\n\n    # The name of the hub to use when none is specified. Defaults to the first defined hub if not explicitly set.\n    property default_hub : String? = nil\n\n    # Default lifetime for authorization cookies set via `ABM::Authorization`.\n    property default_cookie_lifetime : Time::Span = 1.hour\n  end\n\n  # :nodoc:\n  module Extension\n    macro included\n      macro finished\n        {% verbatim do %}\n          {%\n            cfg = CONFIG[\"mercure\"]\n            parameters = CONFIG[\"parameters\"]\n\n            default_hub_id = nil\n            default_hub_name = nil\n            hubs = {} of Nil => Nil\n\n            hub_aliases = [] of Nil\n\n            cfg[\"hubs\"].to_a.reject { |(name, _)| name.stringify == \"__nil\" }.each do |(name, hub)|\n              token_provider = nil\n              token_factory = nil\n\n              jwt = hub[\"jwt\"]\n\n              if value = jwt[\"value\"]\n                SERVICE_HASH[token_provider = \"mercure_hub_#{name}_jwt_provider\"] = {\n                  class:      Athena::Mercure::TokenProvider::Static,\n                  parameters: {\n                    token: {value: value},\n                  },\n                }\n              else\n                # TODO: Maybe support providing the factory/provider service ID?\n\n                # TODO: Service is already lazy so no need for dedicated lazy service?\n                SERVICE_HASH[token_factory = \"mercure_hub_#{name}_jwt_factory\"] = {\n                  class:      Athena::Mercure::TokenFactory::JWT,\n                  tags:       [\"mercure.jwt.factory\"],\n                  parameters: {\n                    jwt_secret: {value: jwt[\"secret\"]},\n                    algorithm:  {value: jwt[\"algorithm\"]},\n                    # jwt_lifetime: {value: nil},\n                    passphrase: {value: jwt[\"passphrase\"]},\n                  },\n                }\n\n                SERVICE_HASH[token_provider = \"mercure_hub_#{name}_jwt_provider\"] = {\n                  class:      Athena::Mercure::TokenProvider::Factory,\n                  tags:       [\"mercure.jwt.provider\"],\n                  parameters: {\n                    factory:   {value: token_factory.id},\n                    subscribe: {value: jwt[\"subscribe\"]},\n                    publish:   {value: jwt[\"publish\"]},\n                  },\n                }\n\n                ALIASES[Athena::Mercure::TokenFactory::Interface] = [\n                  {id: token_factory, public: false, name: name},\n                  {id: token_factory, public: false, name: \"#{name}_factory\"},\n                  {id: token_factory, public: false, name: \"#{name}_token_factory\"},\n                ]\n              end\n\n              if token_provider\n                ALIASES[Athena::Mercure::TokenProvider::Interface] = [\n                  {id: token_provider, public: false, name: name},\n                  {id: token_provider, public: false, name: \"#{name}_provider\"},\n                  {id: token_provider, public: false, name: \"#{name}_token_provider\"},\n                ]\n              end\n\n              hub_id = \"mercure_hub_#{name}\"\n              publisher_id = \"mercure_hub_#{name}_publisher\"\n              hubs[name.stringify] = hub_id.id\n\n              if cfg[\"default_hub\"] == name || default_hub_id == nil\n                default_hub_name = name\n                default_hub_id = hub_id\n              end\n\n              SERVICE_HASH[hub_id] = {\n                class:      Athena::Mercure::Hub,\n                tags:       [\"mercure.hub\"],\n                parameters: {\n                  url:            {value: hub[\"url\"]},\n                  token_provider: {value: token_provider.id},\n                  token_factory:  {value: token_factory.id},\n                  public_url:     {value: hub[\"public_url\"]},\n                  # http_client\n                },\n              }\n\n              hub_aliases << {id: hub_id, public: false, name: name}\n              hub_aliases << {id: hub_id, public: false, name: \"#{name}_hub\"}\n            end\n\n            # Unnamed alias resolves to the default hub\n            hub_aliases << {id: default_hub_id, public: false}\n\n            ALIASES[Athena::Mercure::Hub::Interface] = hub_aliases\n\n            SERVICE_HASH[hub_registry_id = \"mercure_hub_registry\"] = {\n              class:      Athena::Mercure::Hub::Registry,\n              parameters: {\n                default_hub: {value: default_hub_id.id},\n                hubs:        {value: \"#{hubs} of String => Athena::Mercure::Hub::Interface\".id},\n              },\n            }\n\n            SERVICE_HASH[\"mercure_authorization\"] = {\n              class:      ABM::Authorization,\n              parameters: {\n                hub_registry:    {value: hub_registry_id.id},\n                cookie_lifetime: {value: cfg[\"default_cookie_lifetime\"]},\n              },\n            }\n\n            SERVICE_HASH[\"mercure_discovery\"] = {\n              class:      ABM::Discovery,\n              parameters: {\n                hub_registry: {value: hub_registry_id.id},\n              },\n            }\n          %}\n        {% end %}\n      end\n    end\n  end\nend\n\nADI.register_bundle Athena::MercureBundle\n"
  },
  {
    "path": "src/bundles/mercure/src/authorization.cr",
    "content": "struct Athena::MercureBundle < ADI::AbstractBundle; end\n\n# Extension of [AMC::Authorization](/Mercure/Authorization) to add support for [AHTTP::Request](/HTTP/Request).\nclass Athena::MercureBundle::Authorization < Athena::Mercure::Authorization\n  def initialize(\n    hub_registry : AMC::Hub::Registry,\n    cookie_lifetime : Time::Span = 1.hour,\n    cookie_samesite : ::HTTP::Cookie::SameSite = :strict,\n  )\n    super\n  end\n\n  # Sets the `mercureAuthorization` cookie for the provided *hub_name*.\n  # The cookie is automatically applied to the `AHTTP::Response` via the `Listeners::SetCookie` listener.\n  #\n  # The JWT cookie value by default does not have access to publish or subscribe to any topic.\n  # Be sure to set the *subscribe* and *publish* arrays to the topics you want it to be able to interact with, or `[\"*\"]` to handle all topics.\n  # *additional_claims* may also be used to define additional claims to the JWT if needed.\n  def set_cookie(\n    request : AHTTP::Request,\n    subscribe : Array(String)? = [] of String,\n    publish : Array(String)? = [] of String,\n    additional_claims : Hash? = nil,\n    hub_name : String? = nil,\n  ) : Nil\n    self.update_cookies request, hub_name, self.create_cookie(request, subscribe, publish, additional_claims, hub_name)\n  end\n\n  # Clears the `mercureAuthorization` cookie for the given *hub_name*.\n  def clear_cookie(\n    request : AHTTP::Request,\n    hub_name : String? = nil,\n  ) : Nil\n    self.update_cookies request, hub_name, self.create_clear_cookie(request.request, hub_name)\n  end\n\n  # Returns a Mercure auth cookie given the provided *request* and optionally for the provided *hub_name*.\n  #\n  # The JWT cookie value by default does not have access to publish or subscribe to any topic.\n  # Be sure to set the *subscribe* and *publish* arrays to the topics you want it to be able to interact with, or `[\"*\"]` to handle all topics.\n  # *additional_claims* may also be used to define additional claims to the JWT if needed.\n  def create_cookie(\n    request : AHTTP::Request,\n    subscribe : Array(String)? = [] of String,\n    publish : Array(String)? = [] of String,\n    additional_claims : Hash? = nil,\n    hub_name : String? = nil,\n  ) : ::HTTP::Cookie\n    super request.request, subscribe, publish, additional_claims, hub_name\n  end\n\n  private def update_cookies(\n    request : AHTTP::Request,\n    hub_name : String?,\n    cookie : ::HTTP::Cookie,\n  ) : Nil\n    hub_name ||= \"\"\n\n    cookies = request.attributes.get?(\"_mercure_authorization_cookies\", Hash(String, ::HTTP::Cookie)) || Hash(String, ::HTTP::Cookie).new\n\n    if cookies.has_key? hub_name\n      raise AMC::Exception::Runtime.new \"The 'mercureAuthorization' cookie for the '#{hub_name.presence ? \"#{hub_name} hub\" : \"default hub\"}' has already been set. You cannot set it two times during the same request.\"\n    end\n\n    cookies[hub_name] = cookie\n\n    request.attributes.set \"_mercure_authorization_cookies\", cookies, Hash(String, ::HTTP::Cookie)\n  end\nend\n"
  },
  {
    "path": "src/bundles/mercure/src/discovery.cr",
    "content": "# Extension of [AMC::Discovery](/Mercure/Discovery/) that accepts [AHTTP::Request](/HTTP/Request/)\n# and stores the link in a request attribute for the [AddLinkHeader](/MercureBundle/Listeners/AddLinkHeader/) listener.\nclass Athena::MercureBundle::Discovery < AMC::Discovery\n  def initialize(\n    hub_registry : AMC::Hub::Registry,\n  )\n    super\n  end\n\n  # Adds the mercure relation `link` header to the provided *request*, optionally for the provided *hub_name*.\n  def add_link(request : AHTTP::Request, hub_name : String? = nil) : Nil\n    return if self.preflight_request? request.request\n\n    hub = @hub_registry.hub hub_name\n\n    # TODO: Create WebLink component?\n    links = request.attributes.get?(\"_links\", Array(String)) || Array(String).new\n    links << self.generate_link(hub.public_url)\n    request.attributes.set(\"_links\", links, Array(String))\n  end\nend\n"
  },
  {
    "path": "src/bundles/mercure/src/listeners/add_link_header.cr",
    "content": "# Adds Mercure hub `Link` headers that was stored in the request attributes via `ABM::Discovery`.\n@[ADI::Register]\nstruct Athena::MercureBundle::Listeners::AddLinkHeader\n  # :nodoc:\n  def initialize; end\n\n  @[AEDA::AsEventListener]\n  def on_response(event : AHK::Events::Response) : Nil\n    return unless links = event.request.attributes.get? \"_links\", Array(String)\n\n    # TODO: Create WebLink component?\n    links.each do |link|\n      event.response.headers.add \"link\", link\n    end\n  end\nend\n"
  },
  {
    "path": "src/bundles/mercure/src/listeners/set_cookie.cr",
    "content": "# Adds `mercureAuthorization` cookies that were stored in the request attributes via `ABM::Authorization`.\n@[ADI::Register]\nstruct Athena::MercureBundle::Listeners::SetCookie\n  # :nodoc:\n  def initialize; end\n\n  @[AEDA::AsEventListener]\n  def on_response(event : AHK::Events::Response) : Nil\n    return unless cookies = event.request.attributes.get? \"_mercure_authorization_cookies\", Hash(String, ::HTTP::Cookie)\n\n    event.request.attributes.remove \"_mercure_authorization_cookies\"\n\n    cookies.each_value do |cookie|\n      event.response.headers << cookie\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/clock/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/clock/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/clock/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.3.0] - 2026-04-19\n\n### Removed\n\n- Remove `ACLK::Monotonic` ([#667]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.3.0]: https://github.com/athena-framework/clock/releases/tag/v0.3.0\n[#667]: https://github.com/athena-framework/athena/pull/667\n\n## [0.2.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Remove `Athena::Clock::Interface#sleep(Number)` overload ([#449]) (George Dietrich)\n\n### Fixed\n\n- Fix type error when trying to use `ACLK::Aware#now` ([#498]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/clock/releases/tag/v0.2.0\n[#449]: https://github.com/athena-framework/athena/pull/449\n[#498]: https://github.com/athena-framework/athena/pull/498\n\n## [0.1.2] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Fixed\n\n- Fix that `Athena::Clock::Aware` was not required by default ([#365]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/clock/releases/tag/v0.1.2\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.1.1] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.1.1]: https://github.com/athena-framework/clock/releases/tag/v0.1.1\n\n## [0.1.0] - 2023-09-16\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/clock/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/clock/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/clock/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2023 George Dietrich\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": "src/components/clock/README.md",
    "content": "# Clock\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/clock.svg)](https://github.com/athena-framework/clock/releases)\n\nDecouples applications from the system clock.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Clock).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/clock/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.3.0\n\n### Remove `ACLK::Monotonic`\n\n`Time.monotonic` has been [deprecated](https://github.com/crystal-lang/rfcs/pull/15) in Crystal stdlib.\nThe new `Time.instant` API doesn't, for good reason, doesn't have any overlap with `Time`, thus making it somewhat incompatible with `ACLK::Interface`.\nUse cases that require measuring time should likely just use `Time.instant` directly.\n\n## Upgrade to 0.2.0\n\n### Dropped `ACLK::Interface#sleep(Number)` overload\n\n`::sleep(Number)` has been [deprecated](https://github.com/crystal-lang/crystal/pull/14962) in Crystal stdlib.\nThe clock component follows suite, with the same migration path.\nInstead of `.sleep 5` do `.sleep 5.seconds`.\n"
  },
  {
    "path": "src/components/clock/docs/README.md",
    "content": "The `Athena::Clock` component allows decoupling an application from the system clock.\nThis allows for more easily testing time sensitive code.\n\nThe component provides a [ACLK::Interface](/Clock/Interface/) with the following implementations for different use cases:\n\n* [ACLK::Native](/Clock/Native/) - Provides access to the system clock for most usages\n* [ACLK::Monotonic](/Clock/Monotonic/) - Provides access to a high resolution, monotonic clock for when needing to measure time precisely\n* [ACLK::Spec::MockClock](/Clock/Spec/MockClock/) - Provides the ability to freeze and change the current time for use in tests\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-clock:\n    github: athena-framework/clock\n    version: ~> 0.3.0\n```\n\n## Usage\n\nThe core `Athena::Clock` type can be used to return the current time via a global clock.\n\n```crystal\n# By default, `Athena::Clock` uses the native clock implementation,\n# but it can be changed to any other implementation\nAthena::Clock.clock = ACLK::Monotonic.new\n\n# Then, obtain a clock instance\nclock = ACLK.clock\n\n# Optionally, with in a specific location\nberlin_clock = clock.in_location Time::Location.load \"Europe/Berlin\"\n\n# From here, get the current time as a `Time` instance\nnow = clock.now # : ::Time\n\n# and sleep for any span of time\nclock.sleep 2.seconds\n```\n"
  },
  {
    "path": "src/components/clock/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Clock\nsite_url: https://athenaframework.org/Clock/\nrepo_url: https://github.com/athena-framework/clock\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-clock/src/athena-clock.cr\n            - ./lib/athena-clock/src/spec.cr\n          source_locations:\n            lib/athena-clock: https://github.com/athena-framework/clock/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/clock/shard.yml",
    "content": "name: athena-clock\n\nversion: 0.3.0\n\ncrystal: ~> 1.4\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/clock\n\ndocumentation: https://athenaframework.org/Clock\n\ndescription: |\n  Decouples applications from the system clock.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n"
  },
  {
    "path": "src/components/clock/spec/athena-clock_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct ClockTest < ASPEC::TestCase\n  include ACLK::Spec::ClockSensitive\n\n  def test_functions_as_a_clock : Nil\n    self.mock_time Time.utc 2023, 9, 16\n\n    clock = ACLK.new\n\n    clock.now.to_s(\"%F\").should eq \"2023-09-16\"\n  end\n\n  def test_accepts_an_existing_clock : Nil\n    clock = ACLK.new ACLK::Spec::MockClock.new Time.utc 2023, 9, 16, 23, 53, 0\n    clock.now.to_s(\"%F\").should eq \"2023-09-16\"\n  end\n\n  def test_in_location : Nil\n    clock = ACLK.new location: Time::Location.load(\"America/New_York\")\n    utc_clock = clock.in_location Time::Location::UTC\n\n    clock.should_not eq utc_clock\n    utc_clock.now.location.should eq Time::Location::UTC\n  end\n\n  def test_sleep : Nil\n    clock = ACLK.new ACLK::Spec::MockClock.new Time.utc 2023, 9, 16, 23, 53, 0, nanosecond: 999_000_000\n    location = clock.now.location\n\n    clock.sleep 2.002_001.seconds\n    clock.now.to_s(\"%F %H:%M:%S.%6N\").should eq \"2023-09-16 23:53:03.001001\"\n    clock.now.location.should eq location\n  end\n\n  def test_supports_mock_clock : Nil\n    ACLK.clock.should be_a ACLK::Native\n\n    clock = self.mock_time\n    ACLK.clock.should be_a ACLK::Spec::MockClock\n    ACLK.clock.should eq clock\n  end\n\n  def test_defaults_to_native_clock : Nil\n    ACLK.clock.should be_a ACLK::Native\n  end\n\n  def test_response_to_mocked_clocks : Nil\n    ACLK.clock.should be_a ACLK::Native\n\n    self.mock_time.should be_a ACLK::Spec::MockClock\n    self.mock_time(false).should be_a ACLK::Native\n  end\n\n  def test_ensure_mock_clock_freezes : Nil\n    self.mock_time Time.utc 2023, 9, 16\n\n    ACLK.clock.now.to_s(\"%F\").should eq \"2023-09-16\"\n    ACLK.clock.now.shift(days: 1).to_s(\"%F\").should eq \"2023-09-17\"\n\n    self.shift days: 1\n    ACLK.clock.now.to_s(\"%F\").should eq \"2023-09-17\"\n  end\nend\n"
  },
  {
    "path": "src/components/clock/spec/aware_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate class Example\n  include Athena::Clock::Aware\nend\n\nstruct AwareTest < ASPEC::TestCase\n  def test_happy_path : Nil\n    instance = Example.new\n    instance.now.should_not be_nil\n    instance.clock = ACLK::Spec::MockClock.new Time.utc 2023, 9, 16, 23, 53, 0\n    instance.now.should eq Time.utc(2023, 9, 16, 23, 53, 0)\n  end\nend\n"
  },
  {
    "path": "src/components/clock/spec/mock_clock_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct MockClockTest < ASPEC::TestCase\n  def test_allows_customizing_timezone : Nil\n    clock = ACLK::Spec::MockClock.new location: Time::Location.load \"Europe/Berlin\"\n    clock.now.location.name.should eq \"Europe/Berlin\"\n  end\n\n  def test_defaults_to_utc : Nil\n    ACLK::Spec::MockClock.new.now.location.utc?.should be_true\n  end\n\n  def test_allows_specifying_a_specific_time : Nil\n    ACLK::Spec::MockClock.new(now = Time.local).now.should eq now\n  end\n\n  def test_now : Nil\n    before = Time.utc.to_unix_ms\n    sleep 10.milliseconds\n    clock = ACLK::Spec::MockClock.new\n    sleep 10.milliseconds\n    now = clock.now\n    after = Time.utc.to_unix_ms\n\n    now.to_unix_ms.should be > before\n    now.to_unix_ms.should be < after\n    clock.now.should eq clock.now\n  end\n\n  def test_sleep : Nil\n    clock = ACLK::Spec::MockClock.new Time.utc 2023, 9, 16, 23, 53, 0, nanosecond: 999_000_000\n    location = clock.now.location\n\n    clock.sleep 2.002_001.seconds\n    clock.now.to_s(\"%F %H:%M:%S.%6N\").should eq \"2023-09-16 23:53:03.001001\"\n    clock.now.location.should eq location\n  end\n\n  def test_shift : Nil\n    clock = ACLK::Spec::MockClock.new Time.utc 2023, 9, 16, 23, 53, 0\n\n    clock.shift days: 2, seconds: 12, hours: -1\n\n    clock.now.to_s(\"%F %H:%M:%S\").should eq \"2023-09-18 22:53:12\"\n  end\n\n  def test_in_location : Nil\n    clock = ACLK::Spec::MockClock.new location: Time::Location.load(\"America/New_York\")\n    utc_clock = clock.in_location Time::Location::UTC\n\n    clock.should_not eq utc_clock\n    utc_clock.now.location.should eq Time::Location::UTC\n  end\nend\n"
  },
  {
    "path": "src/components/clock/spec/native_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct NativeClockTest < ASPEC::TestCase\n  def test_allows_customizing_timezone : Nil\n    clock = ACLK::Native.new Time::Location.load \"Europe/Berlin\"\n    clock.now.location.name.should eq \"Europe/Berlin\"\n  end\n\n  def test_defaults_to_local_tz : Nil\n    ACLK::Native.new.now.location.local?.should be_true\n  end\n\n  def test_now : Nil\n    clock = ACLK::Native.new\n    before = Time.local.to_unix_ms\n    sleep 10.milliseconds\n    now = clock.now\n    sleep 10.milliseconds\n    after = Time.local.to_unix_ms\n\n    now.to_unix_ms.should be > before\n    now.to_unix_ms.should be < after\n  end\n\n  def test_sleep : Nil\n    clock = ACLK::Native.new\n    location = clock.now.location\n\n    before = Time.local.to_unix_ms\n    clock.sleep 0.5.seconds\n    now = clock.now.to_unix_ms\n    sleep 10.milliseconds\n    after = Time.local.to_unix_ms\n\n    now.should be >= (before + 1.5)\n    now.should be < after\n    clock.now.location.should eq location\n  end\n\n  def test_in_location : Nil\n    clock = ACLK::Native.new Time::Location.load(\"America/New_York\")\n    utc_clock = clock.in_location Time::Location::UTC\n\n    clock.should_not eq utc_clock\n    utc_clock.now.location.should eq Time::Location::UTC\n  end\nend\n"
  },
  {
    "path": "src/components/clock/spec/spec_helper.cr",
    "content": "require \"spec\"\n\nrequire \"athena-spec\"\n\nrequire \"../src/athena-clock\"\nrequire \"../src/spec\"\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/clock/src/athena-clock.cr",
    "content": "require \"./aware\"\nrequire \"./interface\"\nrequire \"./native\"\n\n# Convenience alias to make referencing `Athena::Clock` types easier.\nalias ACLK = Athena::Clock\n\n# Decouples applications from the system clock.\nclass Athena::Clock\n  include Athena::Clock::Interface\n\n  VERSION = \"0.3.0\"\n\n  # Represents the global clock used by all `Athena::Clock` instances.\n  #\n  # NOTE: It is preferable injecting an `Athena::Clock::Interface` when possible versus using the global clock getter.\n  class_property clock : ACLK::Interface = ACLK::Native.new\n\n  @clock : ACLK::Interface?\n  @location : Time::Location?\n\n  def initialize(\n    @clock : ACLK::Interface? = nil,\n    @location : Time::Location? = nil,\n  )\n  end\n\n  # :inherit:\n  def in_location(location : Time::Location) : self\n    self.class.new @clock, location\n  end\n\n  # :inherit:\n  def now : Time\n    now = (@clock || self.class.clock).now\n\n    (location = @location) ? now.in(location) : now\n  end\n\n  # :inherit:\n  def sleep(span : Time::Span) : Nil\n    (@clock || self.class.clock).sleep span\n  end\nend\n"
  },
  {
    "path": "src/components/clock/src/aware.cr",
    "content": "class Athena::Clock; end\n\n# This module can be included to make a type time aware without having to alter its constructor.\n#\n# ```\n# class Example\n#   include Athena::Clock::Aware\n#\n#   def expired?\n#     self.now > some_time_instance\n#   end\n# end\n#\n# # Will use a `Athena::Clock` instance if a custom one is not set on the instance.\n# example = Example.new\n#\n# # Or if so desired, explicitly set custom implementation.\n# my_clock = MySpecialClock.new\n# custom_example = Example.new\n# custom_example.clock = my_clock\n# ```\nmodule Athena::Clock::Aware\n  # TODO: Wire this up with an `@[ADI::Required]` annotation.\n\n  setter clock : ACLK::Interface? = nil\n\n  def now : ::Time\n    (@clock ||= ACLK.new).now\n  end\nend\n"
  },
  {
    "path": "src/components/clock/src/interface.cr",
    "content": "# Represents a clock that returns a `Time` instance, possibly in a specific location.\nmodule Athena::Clock::Interface\n  # Returns a new clock instance set to the provided *location*.\n  abstract def in_location(location : Time::Location) : self\n\n  # Returns the current time as determined by the clock.\n  abstract def now : Time\n\n  # Sleeps for the provided *span* of time.\n  abstract def sleep(span : Time::Span) : Nil\nend\n"
  },
  {
    "path": "src/components/clock/src/native.cr",
    "content": "# The default clock for most use cases which returns the current system time.\n# For example:\n#\n# ```\n# class ExpirationChecker\n#   def initialize(@clock : Athena::Clock::Interface); end\n#\n#   def expired?(valid_until : Time) : Bool\n#     @clock.now > valid_until\n#   end\n# end\n# ```\nstruct Athena::Clock::Native\n  include Athena::Clock::Interface\n\n  @location : Time::Location\n\n  def initialize(\n    location : Time::Location? = nil,\n  )\n    @location = location || Time::Location.local\n  end\n\n  # :inherit:\n  def in_location(location : Time::Location) : self\n    self.class.new location: location\n  end\n\n  # :inherit:\n  def now : Time\n    Time.local @location\n  end\n\n  # :inherit:\n  def sleep(span : Time::Span) : Nil\n    ::sleep span\n  end\nend\n"
  },
  {
    "path": "src/components/clock/src/spec.cr",
    "content": "# A set of testing utilities/types to aid in testing `Athena::Clock` related types.\n#\n# ### Getting Started\n#\n# Require this module in your `spec_helper.cr` file:\n#\n# ```\n# require \"athena-clock/spec\"\n# ```\nmodule Athena::Clock::Spec\n  # The mock clock is instantiated with a time and does not move forward on its own.\n  # The time is fixed until `#sleep` or `#shift` is called.\n  # This provides full control over what time the code assumes it's running with,\n  # ultimately making testing time-sensitive types much easier.\n  #\n  # ```\n  # class ExpirationChecker\n  #   def initialize(@clock : Athena::Clock::Interface); end\n  #\n  #   def expired?(valid_until : Time) : Bool\n  #     @clock.now > valid_until\n  #   end\n  # end\n  #\n  # clock = ACLK::Spec::MockClock.new Time.utc 2023, 9, 16, 15, 20\n  # expiration_checker = ExpirationChecker.new clock\n  # valid_until = Time.utc 2023, 9, 16, 15, 25\n  #\n  # # valid_until is in the future, so not expired\n  # expiration_checker.expired?(valid_until).should be_false\n  #\n  # # Sleep for 10 minutes, so time is now 2023-09-16 15:30:00,\n  # # time is instantly changes as if 10 minutes really passed\n  # clock.sleep 10.minutes\n  #\n  # expiration_checker.expired?(valid_until).should be_true\n  #\n  # # Time can also be shifted, either into the future or past\n  # clock.shift minutes: -20\n  #\n  # # valid_until is in the future again, so not expired\n  # expiration_checker.expired?(valid_until).should be_false\n  # ```\n  class MockClock\n    include Athena::Clock::Interface\n\n    @now : Time\n    @location : Time::Location\n\n    def initialize(\n      now : Time = Time.local,\n      location : Time::Location? = nil,\n    )\n      @location = location || Time::Location::UTC\n      @now = now.in @location\n    end\n\n    # :inherit:\n    def in_location(location : Time::Location) : self\n      self.class.new now: Time.local(location)\n    end\n\n    # :inherit:\n    def now : Time\n      @now\n    end\n\n    # Shifts the mocked time instance by the provided amount of time.\n    # Positive values shift into the future, while negative values shift into the past.\n    #\n    # This method is essentially equivalent to calling `#sleep` with the same amount of time, but this method provides a better API in some cases.\n    def shift(*, years : Int = 0, months : Int = 0, weeks : Int = 0, days : Int = 0, hours : Int = 0, minutes : Int = 0, seconds : Int = 0) : Nil\n      @now = @now.shift(years: years, months: months, weeks: weeks, days: days, hours: hours, minutes: minutes, seconds: seconds)\n    end\n\n    # :inherit:\n    def sleep(span : Time::Span) : Nil\n      @now += span\n    end\n  end\n\n  # An `Athena::Spec::TestCase` mix-in that allows freezing time and restoring the global clock after each test.\n  #\n  # ```\n  # struct MonthSensitiveTest < ASPEC::TestCase\n  #   include ACLK::Spec::ClockSensitive\n  #\n  #   def test_winter_month : Nil\n  #     clock = self.mock_time Time.utc 2023, 12, 10\n  #\n  #     month_sensitive = MonthSensitive.new\n  #     month_sensitive.clock = clock\n  #\n  #     month_sensitive.winter_month?.should be_true\n  #   end\n  #\n  #   def test_non_winter_month : Nil\n  #     clock = self.mock_time Time.utc 2023, 7, 10\n  #\n  #     month_sensitive = MonthSensitive.new\n  #     month_sensitive.clock = clock\n  #\n  #     month_sensitive.winter_month?.should be_false\n  #   end\n  # end\n  # ```\n  module ClockSensitive\n    @@original_clock : ACLK::Interface? = nil\n\n    # Returns a new clock instanced with the global clock value shifted by the provided amount of time.\n    # Positive values shift into the future, while negative values shift into the past.\n    def shift(*, years : Int = 0, months : Int = 0, weeks : Int = 0, days : Int = 0, hours : Int = 0, minutes : Int = 0, seconds : Int = 0) : ACLK::Interface\n      self.mock_time ACLK.clock.now.shift(years: years, months: months, weeks: weeks, days: days, hours: hours, minutes: minutes, seconds: seconds)\n    end\n\n    # Returns clock instance based on the provided *now* value.\n    #\n    # If a `Time` instance is passed, that value is used.\n    # If `true`, freezes the global clock to the current time.\n    # If `false`, restores the previous global clock.\n    def mock_time(now : Time | Bool = true) : ACLK::Interface\n      ACLK.clock = case now\n                   in false then self.save_clock_before_test false\n                   in true  then ACLK::Spec::MockClock.new\n                   in Time  then ACLK::Spec::MockClock.new now\n                   end\n\n      Athena::Clock.clock\n    end\n\n    protected def save_clock_before_test(save : Bool = true) : ACLK::Interface\n      save ? (@@original_clock = ACLK.clock) : @@original_clock.not_nil!\n    end\n\n    protected def restore_clock_after_test : Nil\n      ACLK.clock = self.save_clock_before_test false\n    end\n\n    # :nodoc:\n    def initialize\n      super\n\n      self.save_clock_before_test\n    end\n\n    # :inherit:\n    protected def tear_down : Nil\n      super\n\n      self.restore_clock_after_test\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/console/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/console/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.4.3] - 2026-04-19\n\n### Added\n\n- Add opt-in support for deriving the command name from `PROGRAM_NAME` when a CLI binary is invoked via a symlink ([#645]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.3]: https://github.com/athena-framework/console/releases/tag/v0.4.3\n[#645]: https://github.com/athena-framework/athena/pull/645\n\n## [0.4.2] - 2025-09-04\n\n### Added\n\n- Add ability to customize the finished state of an `ACON::Helper::ProgressIndicator` ([#535]) (George Dietrich) <!-- blacksmoke16 -->\n- Add `markdown` `ACON::Helper::Table` style ([#536]) (George Dietrich) <!-- blacksmoke16 -->\n- Add support for nested style tags ([#568]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix `ACON::Helper::ProgressBar` messing up output in console section with EOL ([#537]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.2]: https://github.com/athena-framework/console/releases/tag/v0.4.2\n[#535]: https://github.com/athena-framework/athena/pull/535\n[#536]: https://github.com/athena-framework/athena/pull/536\n[#568]: https://github.com/athena-framework/athena/pull/568\n[#537]: https://github.com/athena-framework/athena/pull/537\n\n## [0.4.1] - 2025-02-08\n\n### Fixed\n\n- Fix incorrectly aligned block ([#519]) (Zohir Tamda)\n\n[0.4.1]: https://github.com/athena-framework/console/releases/tag/v0.4.1\n[#519]: https://github.com/athena-framework/athena/pull/519\n\n## [0.4.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n\n### Added\n\n- **Breaking:** Add `ACON::Output::Verbosity::SILENT` verbosity level ([#489]) (George Dietrich)\n- **Breaking:** Rename `ACON::Completion::Input#must_suggest_values_for?` to `#must_suggest_option_values_for?` ([#498]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.13.0` ([#498]) (George Dietrich)\n- Add `#assert_command_is_not_successful` spec expectation method ([#498]) (George Dietrich)\n- Add support for [`FORCE_COLOR`](https://force-color.org/) and improve color support logic ([#488]) (George Dietrich)\n\n### Fixed\n\n- Fix unexpected completion value when given an array of options ([#498]) (George Dietrich)\n- Fix error when trying to set `ACON::Helper::Table::Style#padding_char` ([#498]) (George Dietrich)\n\n[0.4.0]: https://github.com/athena-framework/console/releases/tag/v0.4.0\n[#428]: https://github.com/athena-framework/athena/pull/428\n[#488]: https://github.com/athena-framework/athena/pull/488\n[#489]: https://github.com/athena-framework/athena/pull/489\n[#498]: https://github.com/athena-framework/athena/pull/498\n\n## [0.3.6] - 2024-07-31\n\n### Changed\n\n- **Breaking:** `ACON::Application#getter` and constructor argument must now be a `String` instead of `SemanticVersion` ([#419]) (George Dietrich)\n- Changed the default `ACON::Application` version to `UNKNOWN` from `0.1.0` ([#419]) (George Dietrich)\n- List commands in a namespace when using it as the command name ([#427]) (George Dietrich)\n- Use single quotes in text descriptor to quote values in the output ([#427]) (George Dietrich)\n\n[0.3.6]: https://github.com/athena-framework/console/releases/tag/v0.3.6\n[#419]: https://github.com/athena-framework/athena/pull/419\n[#427]: https://github.com/athena-framework/athena/pull/427\n\n## [0.3.5] - 2024-04-09\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.11.0` ([#270]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- Support for Windows OS ([#270]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect column/width `ACON::Terminal` values on Windows ([#361]) (George Dietrich)\n\n[0.3.5]: https://github.com/athena-framework/console/releases/tag/v0.3.5\n[#270]: https://github.com/athena-framework/athena/pull/270\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#361]: https://github.com/athena-framework/athena/pull/361\n\n## [0.3.4] - 2023-10-10\n\n### Added\n\n- Add support for tab completion to the `bash` shell when binary is in the `bin/` directory and referenced with `./` ([#323]) (George Dietrich)\n\n[0.3.4]: https://github.com/athena-framework/console/releases/tag/v0.3.4\n[#323]: https://github.com/athena-framework/athena/pull/323\n\n## [0.3.3] - 2023-10-09\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.8.0` ([#282]) (George Dietrich)\n\n### Added\n\n- **Breaking:** Add `ACON::Helper::ProgressBar` to enable rendering progress bars ([#304]) (George Dietrich)\n- Add native shell tab completion support for `bash`, `zsh`, and `fish` for both built-in and custom commands ([#294], [#296], [#297], [#299]) (George Dietrich)\n- Add `ACON::Helper::ProgressIndicator` to enable rendering spinners ([#314]) (George Dietrich)\n- Add support for defining a max height for an `ACON::Output::Section` ([#303]) (George Dietrich)\n- Add `ACON::Helper.format_time` to format a duration as a human readable string ([#304]) (George Dietrich)\n- Add `#assert_command_is_successful` helper method to `ACON::Spec::CommandTester` and `ACON::Spec::ApplicationTester` ([#294]) (George Dietrich)\n\n### Fixed\n\n- Ensure long lines with URLs are not cut when wrapped ([#314]) (George Dietrich)\n- Do not emit erroneous newline from `ACON::Style::Athena` when it's the first thing being written ([#314]) (George Dietrich)\n- Fix misalignment when word wrapping a hyperlink ([#305]) (George Dietrich)\n- Do not emit erroneous extra newlines from an `ACON::Output::Section` ([#303]) (George Dietrich)\n- Fix misalignment within a vertical table with multi-line cell ([#300]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/console/releases/tag/v0.3.3\n[#282]: https://github.com/athena-framework/athena/pull/282\n[#294]: https://github.com/athena-framework/athena/pull/294\n[#296]: https://github.com/athena-framework/athena/pull/296\n[#297]: https://github.com/athena-framework/athena/pull/297\n[#299]: https://github.com/athena-framework/athena/pull/299\n[#300]: https://github.com/athena-framework/athena/pull/300\n[#303]: https://github.com/athena-framework/athena/pull/303\n[#304]: https://github.com/athena-framework/athena/pull/304\n[#305]: https://github.com/athena-framework/athena/pull/305\n[#314]: https://github.com/athena-framework/athena/pull/314\n\n## [0.3.2] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n### Fixed\n\n- Fix formatting issue in Crystal `1.8-dev` ([#258]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/console/releases/tag/v0.3.2\n[#261]: https://github.com/athena-framework/athena/pull/261\n[#258]: https://github.com/athena-framework/athena/pull/258\n\n## [0.3.1] - 2023-02-04\n\n### Added\n\n- Add better integration between `Athena::Console` and `Athena::DependencyInjection` ([#259]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/console/releases/tag/v0.3.1\n[#259]: https://github.com/athena-framework/athena/pull/259\n\n## [0.3.0] - 2023-01-07\n\n### Changed\n\n- **Breaking:** deprecate command default name/description class variables in favor of the new `ACONA::AsCommand` annotation ([#214]) (George Dietrich)\n- **Breaking:** refactor `ACON::Command#application=` to no longer have a `nil` default value ([#217]) (George Dietrich)\n- **Breaking:** refactor `ACON::Command#process_title=` no longer accept `nil` ([#217]) (George Dietrich)\n- **Breaking:** rename `ACON::Command#process_title=` to `ACON::Command#process_title` ([#217]) (George Dietrich)\n\n### Added\n\n- **Breaking:** add `#table` method to `ACON::Style::Interface` ([#220]) (George Dietrich)\n- Add `ACONA::AsCommand` annotation to configure a command's name, description, aliases, and if it should be hidden ([#214]) (George Dietrich)\n- Add support for generating tables ([#220]) (George Dietrich)\n\n### Fixed\n\n- Fix issue with using `ACON::Formatter::Output#format_and_wrap` with `nil` input and an edge case when wrapping a string with a space at the limit ([#220]) (George Dietrich)\n- Fix `ACON::Formatter::NullStyle#*_option` method using incorrect `ACON::Formatter::Mode` type restriction ([#220]) (George Dietrich)\n- Fix some flakiness when testing commands with input ([#224]) (George Dietrich)\n- Fix compiler error when trying to use `ACON::Style::Athena#error_style` ([#240]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/console/releases/tag/v0.3.0\n[#214]: https://github.com/athena-framework/athena/pull/214\n[#217]: https://github.com/athena-framework/athena/pull/217\n[#220]: https://github.com/athena-framework/athena/pull/220\n[#224]: https://github.com/athena-framework/athena/pull/224\n[#240]: https://github.com/athena-framework/athena/pull/240\n\n## [0.2.1] - 2022-09-05\n\n### Changed\n\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n### Added\n\n- Add an `ACON::Input::Interface` based on a command line string ([#186], [#187]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/console/releases/tag/v0.2.1\n[#186]: https://github.com/athena-framework/athena/pull/186\n[#187]: https://github.com/athena-framework/athena/pull/187\n[#188]: https://github.com/athena-framework/athena/pull/188\n\n## [0.2.0] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Changed\n\n- **Breaking:** remove `ACON::Formatter::Mode` in favor of `Colorize::Mode`. Breaking only if not using symbol autocasting. ([#170]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Added\n\n- Add `VERSION` constant to `Athena::Console` namespace ([#166]) (George Dietrich)\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Fixed\n\n- Disallow multi char option shortcuts made up of diff chars ([#164]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/console/releases/tag/v0.2.0\n[#164]: https://github.com/athena-framework/athena/pull/164\n[#166]: https://github.com/athena-framework/athena/pull/166\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#170]: https://github.com/athena-framework/athena/pull/170\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.1] - 2021-12-01\n\n### Fixed\n\n- **Breaking:** fix typo in parameter name of `ACON::Command#option` method ([#3]) (George Dietrich)\n- Fix recursive struct error ([#4]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/console/releases/tag/v0.1.1\n[#3]: https://github.com/athena-framework/console/pull/3\n[#4]: https://github.com/athena-framework/console/pull/4\n\n## [0.1.0] - 2021-10-30\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/console/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/console/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/console/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 George Dietrich\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": "src/components/console/README.md",
    "content": "# Console\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/console.svg)](https://github.com/athena-framework/console/releases)\n\nAllows for the creation of CLI based commands.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Console).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/console/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.4.0\n\n### New `ACON::Output::Verbosity::SILENT` verbosity level\n\nExisting commands that define a `--silent` option will have to be renamed.\n\n### Normalization of Exception types\n\nThe namespace exception types live in has changed from `ACON::Exceptions` to `ACON::Exception`.\nAny usages of `console` exception types will need to be updated.\n\nSome additional types have also been removed/renamed:\n\n* `ACON::Exceptions::ConsoleException` has been removed in favor of using `ACON::Exception` directly\n* `ACON::Exceptions::RuntimeError` has been renamed `ACON::Exception::Runtime`\n* `ACON::Exceptions::ValidationError` has been removed with past usages now raising an `ACON::Exception::Runtime` error\n\nIf using a `rescue` statement with a parent exception type, either from the `console` component or Crystal stdlib, double check it to ensure it'll still rescue what you are expecting it will.\n\n## Upgrade to 0.3.6\n\n### `ACON::Application` version is now represented as a `String`\n\nIf passing a [SemanticVersion](https://crystal-lang.org/api/SemanticVersion.html) as the *version* of an `ACON::Application`, call `#to_s` on it or ideally pass a semver `String` directly.\nIf using the `#version` getter off the `ACON::Application`, your code will need to adapt to it now being a `String`.\nEither by manually constructing a `SemanticVersion` or ideally just supporting the returned `String`.\n\n## Upgrade to 0.3.3\n\n### New `ACON::Style::Interface` methods\n\nIf implementing a custom style, you will now need to implement the following methods:\n\n- `abstract def progress_start(max : Int32? = nil) : Nil`\n- `abstract def progress_advance(by step : Int32 = 1) : Nil`\n- `abstract def progress_finish : Nil`\n\nThese should use an internal `ACON::Helper::ProgressBar` customized to fit your style that delegates to the related methods.\n"
  },
  {
    "path": "src/components/console/docs/README.md",
    "content": "The `Athena::Console` component allows creating CLI based commands.\nThis integration can be a way to define alternate entry points into your business logic,\nsuch as for use with scheduled jobs (Cron, Airflow, etc), or one-off internal/administrative things (running migrations, creating users, etc).\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-console:\n    github: athena-framework/console\n    version: ~> 0.4.0\n```\n\n## Usage\n\nIn its most basic form, a [ACON::Command](/Console/Command/) consists of an `#execute` method that provides access to [input](/Console/Input/Interface/) and [output](/Console/Output/Interface/) of the command and returns a [ACON::Command::Status](/Console/Command/Status/) member.\n\n```crystal\n@[ACONA::AsCommand(\"app:create-user\", description: \"Manually create a user with the provided username\")]\nclass CreateUserCommand < ACON::Command\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : Status\n    # Implement all the business logic here.\n\n    # Indicates the command executed successfully.\n    Status::SUCCESS\n  end\nend\n```\n\nHowever, in most cases the command will need to be configured to better fit its use case.\nCommands may also implement a [#configure](/Console/Command/#Athena::Console::Command--configuring-the-command) method to accomplish this.\nThis method is where the [ACON::Input::Argument](/Console/Input/Argument/)s and [ACON::Input::Option](/Console/Input/Option/)s may be defined, but also additional help output, aliases, etc.\n\n```crystal\nprotected def configure : Nil\n  self\n    .argument(\"username\", :required, \"The username of the user\")\n    .aliases(\"new-user\")\nend\n```\n\n### Application\n\nThe core of the console component is the [ACON::Application](/Console/Application/) type which is where all the registered [ACON::Command](/Console/Command/)s are stored\nas well as what controls what built-in command(s), global input options (flags), and [ACON::Helper](/Console/Helper/)s are available.\nIn most cases it provides a good starting point, but may be extended/customized if needed.\n\n```crystal\n# Create an ACON::Application, passing it the name of your CLI.\n# Optionally accepts a second argument representing the version of the application.\napplication = ACON::Application.new \"My CLI\"\n\n# Register commands using the `#add` method\napplication.add CreateUserCommand.new\n\n# Or register a block as a command directly\napplication.register \"foo\" do |input, output, cmd|\n  # Do stuff\n\n  ACON::Command::Status::SUCCESS\nend\n\n# Run the application.\n# By default this uses STDIN and STDOUT for its input and output.\napplication.run\n```\n\n### Entrypoint\n\nThe console component best works in conjunction with a dedicated Crystal file that'll be used as the entry point.\nIdeally this file is compiled into a dedicated binary for use in production, but is invoked directly while developing.\nOtherwise, any changes made to the files it requires would not be represented.\nThe most basic example would be:\n\n```\n#!/usr/bin/env crystal\n\n# Require the component and anything extra needed based on your business logic.\nrequire \"athena-console\"\n\napplication = ACON::Application.new \"My CLI\"\n\n# Add any commands defined externally,\n# or configure/customize the application as needed.\n\napplication.run\n```\n\nThe [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) allows executing the file as a command without needing the `crystal` prefix.\nFor example `./console list` would list all commands.\n\n### Console Completion\n\nAthena's completion script can be installed to provide auto tab completion out of the box for command and option names, and values in some cases.\nThe script currently supports the shells: `bash` (also requires the `bash-completion` package), `fish`, and `zsh`.\nRun `./console completion --help` for installation instructions based on your shell.\n\nNOTE: The completion script only needs to be installed _once_, but is specific to the binary used to generate it.\nE.g. `./console completion` would be scoped to the `console` binary, while `./myapp completion` would be scoped to `myapp`.\n\nOnce installed, restart your terminal, and you should be good to go!\n\nWARNING: The completion script may only be used with real built binaries, not temporary ones such as `crystal run src/console.cr -- completion`.\nThis is to ensure the performance of the script is sufficient, and to avoid any issues with the naming of the temporary binary.\n\n## Learn More\n\n* Asking [ACON::Question](/Console/Question/)s\n* Reusable output [styles](/Console/Formatter/OutputStyleInterface/)\n* High level reusable formatting [styles](/Console/Style/Interface/)\n* [Testing abstractions](/Console/Spec/)\n* [Tab Completion](/Console/Input/Interface/#Athena::Console::Input::Interface--argumentoption-value-completion)\n* Rendering [ACON::Helper::Table](/Console/Helper/Table/)s, [ACON::Helper::ProgressBar](/Console/Helper/ProgressBar/)s, or [ACON::Helper::ProgressIndicator](/Console/Helper/ProgressIndicator/)s\n* The various [Verbosity Levels](/Console/Output/Verbosity/)\n"
  },
  {
    "path": "src/components/console/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Console\nsite_url: https://athenaframework.org/Console/\nrepo_url: https://github.com/athena-framework/console\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-clock/src/athena-clock.cr\n            - ./lib/athena-console/src/athena-console.cr\n            - ./lib/athena-console/src/spec.cr\n          source_locations:\n            lib/athena-console: https://github.com/athena-framework/console/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/console/shard.yml",
    "content": "name: athena-console\n\nversion: 0.4.3\n\ncrystal: ~> 1.13\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/console\n\ndocumentation: https://athenaframework.org/Console\n\ndescription: |\n  Allows the creation of CLI based commands.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\ndependencies:\n  athena-clock:\n    github: athena-framework/clock\n    version: ~> 0.3.0\n"
  },
  {
    "path": "src/components/console/spec/application_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate class ProgramNameApplication < ACON::Application\n  def initialize(name : String, @test_program_name : String, version : String = \"UNKNOWN\")\n    super(name, version)\n  end\n\n  protected def program_name : String\n    @test_program_name\n  end\nend\n\nstruct ApplicationTest < ASPEC::TestCase\n  def tear_down : Nil\n    ENV.delete \"COLUMNS\"\n    ENV.delete \"SHELL_VERBOSITY\"\n  end\n\n  protected def assert_file_equals_string(filepath : String, string : String, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    normalized_path = File.join __DIR__, \"fixtures\", filepath\n    string.should match(Regex.new(File.read(normalized_path).gsub EOL, \"\\n\")), file: file, line: line\n  end\n\n  protected def ensure_static_command_help(application : ACON::Application) : Nil\n    application.each_command do |command|\n      command.help = command.help.gsub(\"%command.full_name%\", \"console %command.name%\")\n    end\n  end\n\n  def test_defaults : Nil\n    application = ACON::Application.new \"foo\", \"1.0.0\"\n    application.auto_exit?.should be_true\n    application.catch_exceptions?.should be_true\n  end\n\n  def test_long_version : Nil\n    ACON::Application.new(\"foo\", \"1.2.3\").long_version.should eq \"foo <info>1.2.3</info>\"\n  end\n\n  def test_long_version_non_semver : Nil\n    ACON::Application.new(\"foo\", \"2024.1.2\").long_version.should eq \"foo <info>2024.1.2</info>\"\n  end\n\n  def test_help : Nil\n    ACON::Application.new(\"foo\", \"1.2.3\").help.should eq \"foo <info>1.2.3</info>\"\n  end\n\n  def test_commands : Nil\n    app = ACON::Application.new \"foo\"\n    commands = app.commands\n\n    commands.keys.should eq [\"help\", \"list\", \"completion\", \"_complete\"]\n    commands[\"help\"].should be_a ACON::Commands::Help\n    commands[\"list\"].should be_a ACON::Commands::List\n\n    app.add FooCommand.new\n    commands = app.commands \"foo\"\n    commands.size.should eq 1\n  end\n\n  def test_commands_with_loader : Nil\n    app = ACON::Application.new \"foo\"\n    commands = app.commands\n\n    commands[\"help\"].should be_a ACON::Commands::Help\n    commands[\"list\"].should be_a ACON::Commands::List\n\n    app.add FooCommand.new\n    commands = app.commands \"foo\"\n    commands.size.should eq 1\n\n    app.command_loader = ACON::Loader::Factory.new({\n      \"foo:bar1\" => -> { Foo1Command.new.as ACON::Command },\n    })\n    commands = app.commands \"foo\"\n    commands.size.should eq 2\n    commands[\"foo:bar\"].should be_a FooCommand\n    commands[\"foo:bar1\"].should be_a Foo1Command\n  end\n\n  def test_add : Nil\n    app = ACON::Application.new \"foo\"\n    app.add foo = FooCommand.new\n    commands = app.commands\n\n    commands[\"foo:bar\"].should be foo\n\n    # TODO: Add a splat/enumerable overload of #add ?\n  end\n\n  def test_has_get : Nil\n    app = ACON::Application.new \"foo\"\n    app.has?(\"list\").should be_true\n    app.has?(\"afoobar\").should be_false\n\n    app.add foo = FooCommand.new\n    app.has?(\"afoobar\").should be_true\n    app.get(\"afoobar\").should be foo\n    app.get(\"foo:bar\").should be foo\n\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n\n    pointerof(app.@wants_help).value = true\n\n    app.get(\"foo:bar\").should be_a ACON::Commands::Help\n  end\n\n  def test_has_get_with_loader : Nil\n    app = ACON::Application.new \"foo\"\n    app.has?(\"list\").should be_true\n    app.has?(\"afoobar\").should be_false\n\n    app.add foo = FooCommand.new\n    app.has?(\"afoobar\").should be_true\n    app.get(\"foo:bar\").should be foo\n    app.get(\"afoobar\").should be foo\n\n    app.command_loader = ACON::Loader::Factory.new({\n      \"foo:bar1\" => -> { Foo1Command.new.as ACON::Command },\n    })\n\n    app.has?(\"afoobar\").should be_true\n    app.get(\"foo:bar\").should be foo\n    app.get(\"afoobar\").should be foo\n    app.has?(\"foo:bar1\").should be_true\n    (foo1 = app.get(\"foo:bar1\")).should be_a Foo1Command\n    app.has?(\"afoobar1\").should be_true\n    app.get(\"afoobar1\").should be foo1\n  end\n\n  def test_silent_help : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run(\"-h\": true, \"-q\": true, decorated: false)\n    tester.display.should be_empty\n  end\n\n  def test_get_missing_command : Nil\n    app = ACON::Application.new \"foo\"\n\n    expect_raises ACON::Exception::CommandNotFound, \"The command 'foofoo' does not exist.\" do\n      app.get \"foofoo\"\n    end\n  end\n\n  def test_namespaces : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add Foo1Command.new\n    app.namespaces.should eq [\"foo\"]\n  end\n\n  def test_find_namespace : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.find_namespace(\"foo\").should eq \"foo\"\n    app.find_namespace(\"f\").should eq \"foo\"\n    app.add Foo1Command.new\n    app.find_namespace(\"foo\").should eq \"foo\"\n  end\n\n  def test_find_namespace_subnamespaces : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooSubnamespaced1Command.new\n    app.add FooSubnamespaced2Command.new\n    app.find_namespace(\"foo\").should eq \"foo\"\n  end\n\n  def test_find_namespace_ambiguous : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add BarBucCommand.new\n    app.add Foo2Command.new\n\n    expect_raises ACON::Exception::NamespaceNotFound, \"The namespace 'f' is ambiguous.\" do\n      app.find_namespace \"f\"\n    end\n  end\n\n  def test_find_namespace_invalid : Nil\n    app = ACON::Application.new \"foo\"\n\n    expect_raises ACON::Exception::NamespaceNotFound, \"There are no commands defined in the 'bar' namespace.\" do\n      app.find_namespace \"bar\"\n    end\n  end\n\n  def test_find_namespace_does_not_fail_on_deep_similar_namespaces : Nil\n    app = ACON::Application.new \"foo\"\n\n    app.register \"foo:sublong:bar\" { ACON::Command::Status::SUCCESS }\n    app.register \"bar:sub:foo\" { ACON::Command::Status::SUCCESS }\n\n    app.find_namespace(\"f:sub\").should eq \"foo:sublong\"\n  end\n\n  def test_find : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n\n    app.find(\"foo:bar\").should be_a FooCommand\n    app.find(\"h\").should be_a ACON::Commands::Help\n    app.find(\"f:bar\").should be_a FooCommand\n    app.find(\"f:b\").should be_a FooCommand\n    app.find(\"a\").should be_a FooCommand\n  end\n\n  def test_find_non_ambiguous : Nil\n    app = ACON::Application.new \"foo\"\n    app.add TestAmbiguousCommandRegistering.new\n    app.add TestAmbiguousCommandRegistering2.new\n\n    app.find(\"test\").name.should eq \"test-ambiguous\"\n  end\n\n  def test_find_unique_name_but_namespace_name : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add Foo1Command.new\n    app.add Foo2Command.new\n\n    expect_raises ACON::Exception::CommandNotFound, \"Command 'foo1' is not defined.\" do\n      app.find \"foo1\"\n    end\n  end\n\n  def test_find_case_sensitive_first : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooSameCaseUppercaseCommand.new\n    app.add FooSameCaseLowercaseCommand.new\n\n    app.find(\"f:B\").should be_a FooSameCaseUppercaseCommand\n    app.find(\"f:BAR\").should be_a FooSameCaseUppercaseCommand\n    app.find(\"f:b\").should be_a FooSameCaseLowercaseCommand\n    app.find(\"f:bar\").should be_a FooSameCaseLowercaseCommand\n  end\n\n  def test_find_case_insensitive_fallback : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooSameCaseLowercaseCommand.new\n\n    app.find(\"f:b\").should be_a FooSameCaseLowercaseCommand\n    app.find(\"f:B\").should be_a FooSameCaseLowercaseCommand\n    app.find(\"fOO:bar\").should be_a FooSameCaseLowercaseCommand\n  end\n\n  def test_find_case_insensitive_ambiguous : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooSameCaseUppercaseCommand.new\n    app.add FooSameCaseLowercaseCommand.new\n\n    expect_raises ACON::Exception::CommandNotFound, \"Command 'FOO:bar' is ambiguous.\" do\n      app.find \"FOO:bar\"\n    end\n  end\n\n  def test_find_command_loader : Nil\n    app = ACON::Application.new \"foo\"\n\n    app.command_loader = ACON::Loader::Factory.new({\n      \"foo:bar\" => -> { FooCommand.new.as ACON::Command },\n    })\n\n    app.find(\"foo:bar\").should be_a FooCommand\n    app.find(\"h\").should be_a ACON::Commands::Help\n    app.find(\"f:bar\").should be_a FooCommand\n    app.find(\"f:b\").should be_a FooCommand\n    app.find(\"a\").should be_a FooCommand\n  end\n\n  @[DataProvider(\"ambiguous_abbreviations_provider\")]\n  def test_find_ambiguous_abbreviations(abbreviation, expected_message) : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add Foo1Command.new\n    app.add Foo2Command.new\n\n    expect_raises ACON::Exception::CommandNotFound, expected_message do\n      app.find abbreviation\n    end\n  end\n\n  def ambiguous_abbreviations_provider : Tuple\n    {\n      {\"f\", \"Command 'f' is not defined.\"},\n      {\"a\", \"Command 'a' is ambiguous.\"},\n      {\"foo:b\", \"Command 'foo:b' is ambiguous.\"},\n    }\n  end\n\n  def test_find_ambiguous_abbreviations_finds_command_if_alternatives_are_hidden : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add FooHiddenCommand.new\n\n    app.find(\"foo:\").should be_a FooCommand\n  end\n\n  def test_find_command_equal_namespace\n    app = ACON::Application.new \"foo\"\n    app.add Foo3Command.new\n    app.add Foo4Command.new\n\n    app.find(\"foo3:bar\").should be_a Foo3Command\n    app.find(\"foo3:bar:toh\").should be_a Foo4Command\n  end\n\n  def test_find_ambiguous_namespace_but_unique_name\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add FooBarCommand.new\n\n    app.find(\"f:f\").should be_a FooBarCommand\n  end\n\n  def test_find_missing_namespace\n    app = ACON::Application.new \"foo\"\n    app.add Foo4Command.new\n\n    app.find(\"f::t\").should be_a Foo4Command\n  end\n\n  @[DataProvider(\"invalid_command_names_single_provider\")]\n  def test_find_alternative_exception_message_single(name) : Nil\n    app = ACON::Application.new \"foo\"\n    app.add Foo3Command.new\n\n    expect_raises ACON::Exception::CommandNotFound, \"Did you mean this?\" do\n      app.find name\n    end\n  end\n\n  def invalid_command_names_single_provider : Tuple\n    {\n      {\"foo3:barr\"},\n      {\"fooo3:bar\"},\n    }\n  end\n\n  def test_doesnt_run_alternative_namespace_name : Nil\n    app = ACON::Application.new \"foo\"\n    app.add Foo1Command.new\n    app.auto_exit = false\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run command: \"foos:bar1\", decorated: false\n    self.assert_file_equals_string \"text/application_alternative_namespace.txt\", tester.display true\n  end\n\n  def test_run_alternate_command_name : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooWithoutAliasCommand.new\n    app.auto_exit = false\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.inputs = [\"y\"]\n    tester.run command: \"foos\", decorated: false\n    output = tester.display.strip\n    output.should contain \"Command 'foos' is not defined\"\n    output.should contain \"Do you want to run 'foo' instead? (yes/no) [no]:\"\n    output.should contain \"execute called\"\n  end\n\n  def test_dont_run_alternate_command_name : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooWithoutAliasCommand.new\n    app.auto_exit = false\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.inputs = [\"n\"]\n    tester.run(command: \"foos\", decorated: false).should eq ACON::Command::Status::FAILURE\n    output = tester.display.strip\n    output.should contain \"Command 'foos' is not defined\"\n    output.should contain \"Do you want to run 'foo' instead? (yes/no) [no]:\"\n  end\n\n  def test_run_namespace : Nil\n    ENV[\"COLUMNS\"] = \"120\"\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.add FooCommand.new\n    app.add Foo1Command.new\n    app.add Foo2Command.new\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run(command: \"foo\", decorated: false) # .should eq ACON::Command::Status::FAILURE\n\n    output = tester.display true\n    output.should contain \"Available commands for the 'foo' namespace:\"\n    output.should contain \"The foo:bar command\"\n    output.should contain \"The foo:bar1 command\"\n  end\n\n  def test_run_with_find_error : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n    app.command_loader = MockCommandLoader.new(\n      command_or_exception: FooCommand.new,\n      has: false,\n      names: ::Exception.new(\"Oh noes\")\n    )\n\n    expect_raises ::Exception, \"Oh noes\" do\n      ACON::Spec::ApplicationTester.new(app).run command: \"blah\"\n    end\n  end\n\n  def test_find_alternative_exception_message_multiple : Nil\n    ENV[\"COLUMNS\"] = \"120\"\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add Foo1Command.new\n    app.add Foo2Command.new\n\n    # Command + plural\n    ex = expect_raises ACON::Exception::CommandNotFound do\n      app.find \"foo:BAR\"\n    end\n\n    message = ex.message.should_not be_nil\n    message.should contain \"Did you mean one of these?\"\n    message.should contain \"foo1:bar\"\n    message.should contain \"foo:bar\"\n\n    # Namespace + plural\n    ex = expect_raises ACON::Exception::CommandNotFound do\n      app.find \"foo2:bar\"\n    end\n\n    message = ex.message.should_not be_nil\n    message.should contain \"Did you mean one of these?\"\n    message.should contain \"foo1\"\n\n    app.add Foo3Command.new\n    app.add Foo4Command.new\n\n    # Subnamespace + plural\n    ex = expect_raises ACON::Exception::CommandNotFound do\n      app.find \"foo3:\"\n    end\n\n    message = ex.message.should_not be_nil\n    message.should contain \"foo3:bar\"\n    message.should contain \"foo3:bar:toh\"\n  end\n\n  def test_find_alternative_commands : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add Foo1Command.new\n    app.add Foo2Command.new\n\n    ex = expect_raises ACON::Exception::CommandNotFound do\n      app.find \"Unknown command\"\n    end\n\n    ex.alternatives.should be_empty\n    ex.message.should eq \"Command 'Unknown command' is not defined.\"\n\n    # Test if \"bar1\" command throw a \"CommandNotFoundException\" and does not contain\n    # \"foo:bar\" as alternative because \"bar1\" is too far from \"foo:bar\"\n    ex = expect_raises ACON::Exception::CommandNotFound do\n      app.find \"bar1\"\n    end\n\n    ex.alternatives.should eq [\"afoobar1\", \"foo:bar1\"]\n\n    message = ex.message.should_not be_nil\n    message.should contain \"Command 'bar1' is not defined\"\n    message.should contain \"afoobar1\"\n    message.should contain \"foo:bar1\"\n    message.should_not match /foo:bar(?!1)/\n  end\n\n  def test_find_alternative_commands_with_alias : Nil\n    foo_command = FooCommand.new\n    foo_command.aliases = [\"foo2\"]\n\n    app = ACON::Application.new \"foo\"\n    app.command_loader = ACON::Loader::Factory.new({\n      \"foo3\" => -> { foo_command.as ACON::Command },\n    })\n    app.add foo_command\n\n    app.find(\"foo\").should be foo_command\n  end\n\n  def test_find_alternate_namespace : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add Foo1Command.new\n    app.add Foo2Command.new\n    app.add Foo3Command.new\n\n    ex = expect_raises ACON::Exception::CommandNotFound, \"There are no commands defined in the 'Unknown-namespace' namespace.\" do\n      app.find \"Unknown-namespace:Unknown-command\"\n    end\n    ex.alternatives.should be_empty\n\n    ex = expect_raises ACON::Exception::CommandNotFound do\n      app.find \"foo2:command\"\n    end\n    ex.alternatives.should eq [\"foo\", \"foo1\", \"foo3\"]\n\n    message = ex.message.should_not be_nil\n    message.should contain \"There are no commands defined in the 'foo2' namespace.\"\n    message.should contain \"foo\"\n    message.should contain \"foo1\"\n    message.should contain \"foo3\"\n  end\n\n  def test_find_alternates_output : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add Foo1Command.new\n    app.add Foo2Command.new\n    app.add Foo3Command.new\n    app.add FooHiddenCommand.new\n\n    expect_raises ACON::Exception::CommandNotFound, \"There are no commands defined in the 'Unknown-namespace' namespace.\" do\n      app.find \"Unknown-namespace:Unknown-command\"\n    end.alternatives.should be_empty\n\n    expect_raises ACON::Exception::CommandNotFound, /Command 'foo' is not defined\\..*Did you mean one of these\\?.*/m do\n      app.find \"foo\"\n    end.alternatives.should eq [\"afoobar\", \"afoobar1\", \"afoobar2\", \"foo1:bar\", \"foo3:bar\", \"foo:bar\", \"foo:bar1\"]\n  end\n\n  def test_find_double_colon_doesnt_find_command : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add Foo4Command.new\n\n    expect_raises ACON::Exception::CommandNotFound, \"Command 'foo::bar' is not defined.\" do\n      app.find \"foo::bar\"\n    end\n  end\n\n  def test_find_hidden_command_exact_name : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooHiddenCommand.new\n\n    app.find(\"foo:hidden\").should be_a FooHiddenCommand\n    app.find(\"afoohidden\").should be_a FooHiddenCommand\n  end\n\n  def test_find_ambiguous_commands_if_all_alternatives_are_hidden : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n    app.add FooHiddenCommand.new\n\n    app.find(\"foo:\").should be_a FooCommand\n  end\n\n  def test_set_catch_exceptions : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    ENV[\"COLUMNS\"] = \"120\"\n    tester = ACON::Spec::ApplicationTester.new app\n\n    app.catch_exceptions = true\n    tester.run command: \"foo\", decorated: false\n    self.assert_file_equals_string \"text/application_renderexception1.txt\", tester.display true\n\n    tester.run command: \"foo\", decorated: false, capture_stderr_separately: true\n    self.assert_file_equals_string \"text/application_renderexception1.txt\", tester.error_output true\n    tester.display.should be_empty\n\n    app.catch_exceptions = false\n\n    expect_raises Exception, \"Command 'foo' is not defined.\" do\n      tester.run command: \"foo\", decorated: false\n    end\n  end\n\n  def test_render_exception : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    ENV[\"COLUMNS\"] = \"120\"\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"foo\", decorated: false, verbosity: :quiet, capture_stderr_separately: true\n    self.assert_file_equals_string \"text/application_renderexception1.txt\", tester.error_output true\n\n    tester.run command: \"foo\", decorated: false, verbosity: :verbose, capture_stderr_separately: true\n    tester.error_output.should contain \"Exception trace\"\n\n    tester.run command: \"foo\", decorated: false, verbosity: :silent, capture_stderr_separately: true\n    tester.error_output(true).should be_empty\n\n    tester.run command: \"list\", \"--foo\": true, decorated: false, capture_stderr_separately: true\n    self.assert_file_equals_string \"text/application_renderexception2.txt\", tester.error_output true\n\n    app.add Foo3Command.new\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"foo3:bar\", decorated: false, capture_stderr_separately: true\n    self.assert_file_equals_string \"text/application_renderexception3.txt\", tester.error_output true\n\n    tester.run({\"command\" => \"foo3:bar\"}, decorated: false, verbosity: :verbose)\n    tester.display(true).should match /\\[Exception\\]\\s*First exception/\n    tester.display(true).should match /\\[Exception\\]\\s*Second exception/\n    tester.display(true).should match /\\[Exception\\]\\s*Third exception/\n\n    tester.run command: \"foo3:bar\", decorated: true\n    self.assert_file_equals_string \"text/application_renderexception3_decorated.txt\", tester.display true\n\n    tester.run command: \"foo3:bar\", decorated: true, capture_stderr_separately: true\n    self.assert_file_equals_string \"text/application_renderexception3_decorated.txt\", tester.error_output true\n\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    ENV[\"COLUMNS\"] = \"32\"\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"foo\", decorated: false, capture_stderr_separately: true\n    self.assert_file_equals_string \"text/application_renderexception4.txt\", tester.error_output true\n\n    ENV[\"COLUMNS\"] = \"120\"\n  end\n\n  def ptest_render_exception_double_width_characters : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    ENV[\"COLUMNS\"] = \"120\"\n    tester = ACON::Spec::ApplicationTester.new app\n\n    app.register \"foo\" do\n      raise \"エラーメッセージ\"\n    end\n\n    tester.run command: \"foo\", decorated: false, capture_stderr_separately: true\n    tester.error_output.should eq RENDER_EXCEPTION_DOUBLE_WIDTH\n  end\n\n  def test_render_exception_escapes_lines : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    ENV[\"COLUMNS\"] = \"22\"\n    app.register \"foo\" do\n      raise \"dont break here <info>!</info>\"\n    end\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"foo\", decorated: false\n    self.assert_file_equals_string \"text/application_renderexception_escapeslines.txt\", tester.display true\n\n    ENV[\"COLUMNS\"] = \"120\"\n  end\n\n  def test_render_exception_line_breaks : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    ENV[\"COLUMNS\"] = \"120\"\n    app.register \"foo\" do\n      raise \"\\n\\nline 1 with extra spaces        \\nline 2\\n\\nline 4\\n\"\n    end\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"foo\", decorated: false\n    self.assert_file_equals_string \"text/application_renderexception_linebreaks.txt\", tester.display true\n  end\n\n  def test_render_exception_escapes_lines_of_synopsis : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n\n    app.register \"foo\" do\n      raise \"some exception\"\n    end.argument \"info\"\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run command: \"foo\", decorated: false\n    self.assert_file_equals_string \"text/application_renderexception_synopsis_escapeslines.txt\", tester.display true\n  end\n\n  def test_run_passes_io_thru : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n    app.add command = Foo1Command.new\n\n    input = ACON::Input::Hash.new({\"command\" => \"foo:bar1\"})\n    output = ACON::Output::IO.new IO::Memory.new\n\n    app.run input, output\n\n    command.input.should eq input\n    command.output.should eq output\n  end\n\n  def test_run_default_command : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    self.ensure_static_command_help app\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run decorated: false\n    self.assert_file_equals_string \"text/application_run1.txt\", tester.display true\n  end\n\n  def test_run_help_command : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    self.ensure_static_command_help app\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run \"--help\": true, decorated: false\n    self.assert_file_equals_string \"text/application_run2.txt\", tester.display true\n\n    tester.run \"-h\": true, decorated: false\n    self.assert_file_equals_string \"text/application_run2.txt\", tester.display true\n  end\n\n  def test_run_help_list_command : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    self.ensure_static_command_help app\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"list\", \"--help\": true, decorated: false\n    self.assert_file_equals_string \"text/application_run3.txt\", tester.display true\n\n    tester.run command: \"list\", \"-h\": true, decorated: false\n    self.assert_file_equals_string \"text/application_run3.txt\", tester.display true\n  end\n\n  def test_run_ansi : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run \"--ansi\": true\n    tester.output.decorated?.should be_true\n\n    tester.run \"--no-ansi\": true\n    tester.output.decorated?.should be_false\n  end\n\n  def test_run_version : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run \"--version\": true, decorated: false\n    self.assert_file_equals_string \"text/application_run4.txt\", tester.display true\n\n    tester.run \"-V\": true, decorated: false\n    self.assert_file_equals_string \"text/application_run4.txt\", tester.display true\n  end\n\n  def test_run_quest : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"list\", \"--quiet\": true, decorated: false\n    tester.display.should be_empty\n    tester.input.interactive?.should be_false\n\n    tester.run command: \"list\", \"-q\": true, decorated: false\n    tester.display.should be_empty\n    tester.input.interactive?.should be_false\n  end\n\n  def test_run_verbosity : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    self.ensure_static_command_help app\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"list\", \"--verbose\": true, decorated: false\n    tester.output.verbosity.should eq ACON::Output::Verbosity::VERBOSE\n\n    tester.run command: \"list\", \"--verbose\": 1, decorated: false\n    tester.output.verbosity.should eq ACON::Output::Verbosity::VERBOSE\n\n    tester.run command: \"list\", \"--verbose\": 2, decorated: false\n    tester.output.verbosity.should eq ACON::Output::Verbosity::VERY_VERBOSE\n\n    tester.run command: \"list\", \"--verbose\": 3, decorated: false\n    tester.output.verbosity.should eq ACON::Output::Verbosity::DEBUG\n\n    tester.run command: \"list\", \"--verbose\": 4, decorated: false\n    tester.output.verbosity.should eq ACON::Output::Verbosity::VERBOSE\n\n    tester.run command: \"list\", \"-v\": true, decorated: false\n    tester.output.verbosity.should eq ACON::Output::Verbosity::VERBOSE\n\n    tester.run command: \"list\", \"-vv\": true, decorated: false\n    tester.output.verbosity.should eq ACON::Output::Verbosity::VERY_VERBOSE\n\n    tester.run command: \"list\", \"-vvv\": true, decorated: false\n    tester.output.verbosity.should eq ACON::Output::Verbosity::DEBUG\n  end\n\n  def test_run_help_help_command : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    self.ensure_static_command_help app\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"help\", \"--help\": true, decorated: false\n    self.assert_file_equals_string \"text/application_run5.txt\", tester.display true\n\n    tester.run command: \"help\", \"-h\": true, decorated: false\n    self.assert_file_equals_string \"text/application_run5.txt\", tester.display true\n  end\n\n  def test_run_no_interaction : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    app.add FooCommand.new\n\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run command: \"foo:bar\", \"--no-interaction\": true, decorated: false\n    tester.display.should eq \"execute called#{EOL}\"\n\n    tester.run command: \"foo:bar\", \"-n\": true, decorated: false\n    tester.display.should eq \"execute called#{EOL}\"\n  end\n\n  def test_run_global_option_and_no_command : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n    app.definition << ACON::Input::Option.new \"foo\", \"f\", :optional\n\n    input = ACON::Input::ARGV.new [\"--foo\", \"bar\"]\n\n    app.run(input, ACON::Output::Null.new).should eq ACON::Command::Status::SUCCESS\n  end\n\n  def test_run_verbose_value_doesnt_break_arguments : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n    app.add FooCommand.new\n\n    output = ACON::Output::IO.new IO::Memory.new\n    input = ACON::Input::ARGV.new [\"-v\", \"foo:bar\"]\n\n    app.run(input, output).should eq ACON::Command::Status::SUCCESS\n\n    input = ACON::Input::ARGV.new [\"--verbose\", \"foo:bar\"]\n\n    app.run(input, output).should eq ACON::Command::Status::SUCCESS\n  end\n\n  def test_run_returns_status_with_custom_code_on_exception : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.register \"foo\" do\n      raise ACON::Exception::Logic.new \"\", code: 5\n    end\n\n    input = ACON::Input::Hash.new({\"command\" => \"foo\"})\n\n    app.run(input, ACON::Output::Null.new).value.should eq 5\n  end\n\n  def test_run_returns_failure_status_on_exception : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.register \"foo\" do\n      raise \"\"\n    end\n\n    input = ACON::Input::Hash.new({\"command\" => \"foo\"})\n\n    app.run(input, ACON::Output::Null.new).value.should eq 1\n  end\n\n  def test_add_option_duplicate_shortcut : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n    app.definition << ACON::Input::Option.new \"--env\", \"-e\", :required, \"Environment\"\n\n    app.register \"foo\" do\n      ACON::Command::Status::SUCCESS\n    end\n      .aliases(\"f\")\n      .definition(\n        ACON::Input::Option.new(\"survey\", \"e\", :required, \"Option with shortcut\")\n      )\n\n    input = ACON::Input::Hash.new({\"command\" => \"foo\"})\n\n    expect_raises ACON::Exception::Logic, \"An option with shortcut 'e' already exists.\" do\n      app.run input, ACON::Output::Null.new\n    end\n  end\n\n  @[DataProvider(\"already_set_definition_element_provider\")]\n  def test_adding_already_set_definition_element(element) : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    app.register \"foo\" do\n      ACON::Command::Status::SUCCESS\n    end\n      .definition(element)\n\n    input = ACON::Input::Hash.new({\"command\" => \"foo\"})\n\n    expect_raises ACON::Exception::Logic do\n      app.run input, ACON::Output::Null.new\n    end\n  end\n\n  def already_set_definition_element_provider : Tuple\n    {\n      {ACON::Input::Argument.new(\"command\", :required)},\n      {ACON::Input::Option.new(\"quiet\", value_mode: :none)},\n      {ACON::Input::Option.new(\"query\", \"q\", :none)},\n    }\n  end\n\n  def test_helper_set_contains_default_helpers : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    helper_set = app.helper_set\n\n    helper_set.has?(ACON::Helper::Question).should be_true\n    helper_set.has?(ACON::Helper::Formatter).should be_true\n  end\n\n  def test_adding_single_helper_overwrites_default : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    app.helper_set = ACON::Helper::HelperSet.new(ACON::Helper::Formatter.new)\n\n    helper_set = app.helper_set\n    helper_set.has?(ACON::Helper::Question).should be_false\n    helper_set.has?(ACON::Helper::Formatter).should be_true\n  end\n\n  def test_default_input_definition_returns_default_values : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    definition = app.definition\n\n    definition.has_argument?(\"command\").should be_true\n\n    definition.has_option?(\"help\").should be_true\n    definition.has_option?(\"quiet\").should be_true\n    definition.has_option?(\"verbose\").should be_true\n    definition.has_option?(\"version\").should be_true\n    definition.has_option?(\"ansi\").should be_true\n    definition.has_option?(\"no-interaction\").should be_true\n    definition.has_negation?(\"no-ansi\").should be_true\n    definition.has_option?(\"no-ansi\").should be_false\n  end\n\n  # TODO: Test custom application type's helper set.\n\n  def test_setting_custom_input_definition_overrides_default_values : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.catch_exceptions = false\n\n    app.definition = ACON::Input::Definition.new(\n      ACON::Input::Option.new \"--custom\", \"-c\", :none, \"Set the custom input definition\"\n    )\n\n    definition = app.definition\n\n    definition.has_argument?(\"command\").should be_false\n\n    definition.has_option?(\"help\").should be_false\n    definition.has_option?(\"quiet\").should be_false\n    definition.has_option?(\"verbose\").should be_false\n    definition.has_option?(\"version\").should be_false\n    definition.has_option?(\"ansi\").should be_false\n    definition.has_option?(\"no-interaction\").should be_false\n    definition.has_negation?(\"no-ansi\").should be_false\n\n    definition.has_option?(\"custom\").should be_true\n  end\n\n  # TODO: Add dispatcher related specs\n\n  def test_run_custom_default_command : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.add command = FooCommand.new\n    app.default_command command.name\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run interactive: false\n    tester.display.should eq \"execute called#{EOL}\"\n\n    # TODO: Test custom application default.\n  end\n\n  def test_run_custom_default_command_with_option : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.add command = FooOptCommand.new\n    app.default_command command.name\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run \"--fooopt\": \"opt\", interactive: false\n    tester.display.should eq \"execute called#{EOL}opt#{EOL}\"\n  end\n\n  def test_run_custom_single_default_command : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.add command = FooOptCommand.new\n    app.default_command command.name, true\n\n    tester = ACON::Spec::ApplicationTester.new app\n\n    tester.run\n    tester.display.should contain \"execute called\"\n\n    tester.run \"--help\": true\n    tester.display.should contain \"The foo:bar command\"\n  end\n\n  def test_find_alternative_does_not_load_same_namespace_commands_on_exact_match : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n\n    loaded = Hash(String, Bool).new\n\n    app.command_loader = ACON::Loader::Factory.new({\n      \"foo:bar\" => -> do\n        loaded[\"foo:bar\"] = true\n\n        ACON::Commands::Generic.new(\"foo:bar\") { ACON::Command::Status::SUCCESS }.as ACON::Command\n      end,\n      \"foo\" => -> do\n        loaded[\"foo\"] = true\n\n        ACON::Commands::Generic.new(\"foo\") { ACON::Command::Status::SUCCESS }.as ACON::Command\n      end,\n    })\n\n    app.run ACON::Input::Hash.new({\"command\" => \"foo\"}), ACON::Output::Null.new\n\n    loaded.should eq({\"foo\" => true})\n  end\n\n  def test_command_name_mismatch_with_command_loader_raises : Nil\n    app = ACON::Application.new \"foo\"\n\n    app.command_loader = ACON::Loader::Factory.new({\n      \"foo\" => -> { ACON::Commands::Generic.new(\"bar\") { ACON::Command::Status::SUCCESS }.as ACON::Command },\n    })\n\n    expect_raises ACON::Exception::CommandNotFound, \"The 'foo' command cannot be found because it is registered under multiple names. Make sure you don't set a different name via constructor or 'name='.\" do\n      app.get \"foo\"\n    end\n  end\n\n  def test_use_program_name_as_command_runs_matching_command : Nil\n    app = ProgramNameApplication.new \"foo\", test_program_name: \"mycommand\"\n    app.auto_exit = false\n    app.use_program_name_as_command = true\n\n    app.register(\"mycommand\") { |_, output| output.puts \"mycommand executed\"; ACON::Command::Status::SUCCESS }\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run\n\n    tester.display.should contain \"mycommand executed\"\n  end\n\n  def test_use_program_name_as_command_falls_back_when_no_match : Nil\n    app = ProgramNameApplication.new \"foo\", test_program_name: \"nonexistent\"\n    app.auto_exit = false\n    app.use_program_name_as_command = true\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run\n\n    # Should fall back to default command (list)\n    tester.display.should contain \"Available commands:\"\n  end\n\n  def test_use_program_name_as_command_takes_precedence_over_arguments : Nil\n    app = ProgramNameApplication.new \"foo\", test_program_name: \"mycommand\"\n    app.auto_exit = false\n    app.use_program_name_as_command = true\n\n    app.register(\"mycommand\") do |input, output|\n      output.puts \"mycommand executed\"\n      output.puts \"first arg: #{input.first_argument}\"\n      ACON::Command::Status::SUCCESS\n    end.argument(\"arg\", :optional)\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run input: {\"arg\" => \"somevalue\"}\n\n    # Program name takes precedence - \"somevalue\" becomes an argument, not a command\n    tester.display.should contain \"mycommand executed\"\n    tester.display.should contain \"first arg: somevalue\"\n  end\n\n  def test_use_program_name_as_command_disabled_ignores_program_name : Nil\n    app = ProgramNameApplication.new \"foo\", test_program_name: \"mycommand\"\n    app.auto_exit = false\n    app.use_program_name_as_command = false\n\n    app.register(\"mycommand\") { |_, output| output.puts \"mycommand executed\"; ACON::Command::Status::SUCCESS }\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run\n\n    # Should show list since feature is disabled\n    tester.display.should contain \"Available commands:\"\n    tester.display.should_not contain \"mycommand executed\"\n  end\n\n  def test_use_program_name_as_command_single_command_takes_precedence : Nil\n    app = ProgramNameApplication.new \"foo\", test_program_name: \"mycommand\"\n    app.auto_exit = false\n    app.use_program_name_as_command = true\n\n    app.register(\"mycommand\") { |_, output| output.puts \"mycommand executed\"; ACON::Command::Status::SUCCESS }\n    app.register(\"singlecmd\") { |_, output| output.puts \"singlecmd executed\"; ACON::Command::Status::SUCCESS }\n    app.default_command \"singlecmd\", true\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run\n\n    # single_command mode should take precedence\n    tester.display.should contain \"singlecmd executed\"\n    tester.display.should_not contain \"mycommand executed\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/application_tester_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct ApplicationTesterTest < ASPEC::TestCase\n  @app : ACON::Application\n  @tester : ACON::Spec::ApplicationTester\n\n  def initialize\n    @app = ACON::Application.new \"foo\"\n    @app.auto_exit = false\n\n    @app.register \"foo\" do |_, output|\n      output.puts \"foo\"\n\n      ACON::Command::Status::SUCCESS\n    end.argument \"foo\"\n\n    @tester = ACON::Spec::ApplicationTester.new @app\n    @tester.run command: \"foo\", foo: \"bar\", interactive: false, decorated: false, verbosity: :verbose\n  end\n\n  def test_run : Nil\n    @tester.input.interactive?.should be_false\n    @tester.output.decorated?.should be_false\n    @tester.output.verbosity.verbose?.should be_true\n  end\n\n  def test_input : Nil\n    @tester.input.argument(\"foo\").should eq \"bar\"\n  end\n\n  def test_output : Nil\n    @tester.output.to_s.should eq \"foo#{EOL}\"\n  end\n\n  def test_display : Nil\n    @tester.display.to_s.should eq \"foo#{EOL}\"\n  end\n\n  def test_status : Nil\n    @tester.status.should eq ACON::Command::Status::SUCCESS\n  end\n\n  def test_inputs : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.register \"foo\" do |input, output|\n      helper = ACON::Helper::Question.new\n\n      helper.ask input, output, ACON::Question(String?).new \"Q1\", nil\n      helper.ask input, output, ACON::Question(String?).new \"Q2\", nil\n      helper.ask input, output, ACON::Question(String?).new \"Q3\", nil\n\n      ACON::Command::Status::SUCCESS\n    end\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.inputs = [\"A1\", \"A2\", \"A3\"]\n    tester.run command: \"foo\"\n\n    tester.status.should eq ACON::Command::Status::SUCCESS\n    tester.display.should eq \"Q1Q2Q3\"\n  end\n\n  def test_error_output : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.register \"foo\" do |_, output|\n      output.as(ACON::Output::ConsoleOutput).error_output.print \"foo\"\n\n      ACON::Command::Status::SUCCESS\n    end.argument \"foo\"\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.run command: \"foo\", foo: \"bar\", capture_stderr_separately: true\n\n    tester.error_output.should eq \"foo\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/command_spec.cr",
    "content": "require \"./spec_helper\"\n\nabstract class ACON::Command\n  def merge_application_definition(merge_args : Bool = true) : Nil\n    previous_def\n  end\nend\n\ndescribe ACON::Command do\n  describe \".new\" do\n    describe \"when configured via annotation\" do\n      it \"sets name and description\" do\n        command = AnnotationConfiguredCommand.new\n        command.name.should eq \"annotation:configured\"\n        command.description.should eq \"Command configured via annotation\"\n        command.hidden?.should be_false\n        command.aliases.should eq [\"ac\"]\n      end\n\n      it \"sets the command as hidden if its name is an empty string\" do\n        command = AnnotationConfiguredHiddenCommand.new\n        command.name.should eq \"annotation:configured\"\n        command.hidden?.should be_true\n        command.aliases.should be_empty\n      end\n\n      it \"sets the command as hidden if that field is true\" do\n        command = AnnotationConfiguredHiddenFieldCommand.new\n        command.name.should eq \"annotation:configured\"\n        command.hidden?.should be_true\n        command.aliases.should be_empty\n      end\n\n      it \"sets aliases\" do\n        command = AnnotationConfiguredAliasesCommand.new\n        command.name.should eq \"annotation:configured\"\n        command.hidden?.should be_false\n        command.aliases.should eq [\"ac\"]\n      end\n    end\n\n    it \"prioritizes constructor args\" do\n      command = AnnotationConfiguredCommand.new \"cv\"\n      command.name.should eq \"cv\"\n      command.description.should eq \"Command configured via annotation\"\n    end\n\n    it \"raises on invalid name\" do\n      expect_raises ACON::Exception::InvalidArgument, \"Command name '' is invalid.\" do\n        AnnotationConfiguredCommand.new \"\"\n      end\n\n      expect_raises ACON::Exception::InvalidArgument, \"Command name '  ' is invalid.\" do\n        AnnotationConfiguredCommand.new \"  \"\n      end\n\n      expect_raises ACON::Exception::InvalidArgument, \"Command name 'foo:' is invalid.\" do\n        AnnotationConfiguredCommand.new \"foo:\"\n      end\n    end\n  end\n\n  describe \"#application=\" do\n    it \"sets the helper_set and application\" do\n      app = ACON::Application.new \"foo\"\n      command = TestCommand.new\n      command.application = app\n\n      command.application.should be app\n      command.helper_set.should be app.helper_set\n    end\n\n    it \"clears out the command's helper_set when clearing out the application\" do\n      command = TestCommand.new\n      command.application = nil\n      command.helper_set.should be_nil\n    end\n  end\n\n  describe \"get/set definition\" do\n    command = TestCommand.new\n    command.definition definition = ACON::Input::Definition.new\n    command.definition.should be definition\n\n    command.definition ACON::Input::Argument.new(\"foo\"), ACON::Input::Option.new(\"bar\")\n    command.definition.has_argument?(\"foo\").should be_true\n    command.definition.has_option?(\"bar\").should be_true\n  end\n\n  describe \"#argument\" do\n    it \"basic form\" do\n      command = TestCommand.new\n      command.argument \"foo\"\n      command.definition.has_argument?(\"foo\").should be_true\n    end\n    describe \"suggested values\" do\n      it \"array\" do\n        command = TestCommand.new\n        command.argument \"foo\", suggested_values: {\"a\", \"b\", \"c\"}\n        command.definition.has_argument?(\"foo\").should be_true\n        command.definition.argument(\"foo\").has_completion?.should be_true\n      end\n\n      it \"block\" do\n        command = TestCommand.new\n        command.argument \"foo\" do\n          [\"a\", \"b\"]\n        end\n        command.definition.has_argument?(\"foo\").should be_true\n        command.definition.argument(\"foo\").has_completion?.should be_true\n      end\n    end\n  end\n\n  describe \"#option\" do\n    it \"basic form\" do\n      command = TestCommand.new\n      command.option \"bar\"\n      command.definition.has_option?(\"bar\").should be_true\n    end\n\n    describe \"suggested values\" do\n      it \"array\" do\n        command = TestCommand.new\n        command.option \"foo\", value_mode: :required, suggested_values: {\"a\", \"b\", \"c\"}\n        command.definition.has_option?(\"foo\").should be_true\n        command.definition.option(\"foo\").has_completion?.should be_true\n      end\n\n      it \"block\" do\n        command = TestCommand.new\n        command.option \"foo\", value_mode: :required do\n          [\"a\", \"b\"]\n        end\n        command.definition.has_option?(\"foo\").should be_true\n        command.definition.option(\"foo\").has_completion?.should be_true\n      end\n    end\n  end\n\n  describe \"#processed_help\" do\n    it \"replaces placeholders correctly\" do\n      command = TestCommand.new\n      command.help = \"The %command.name% command does... Example: %command.full_name%.\"\n      command.processed_help.should start_with \"The namespace:name command does\"\n      command.processed_help.should_not contain \"%command.full_name%\"\n    end\n\n    it \"falls back on the description\" do\n      command = TestCommand.new\n      command.help = \"\"\n      command.processed_help.should eq \"description\"\n    end\n  end\n\n  describe \"#synopsis\" do\n    it \"long\" do\n      TestCommand.new.option(\"foo\").argument(\"bar\").argument(\"info\").synopsis.should eq \"namespace:name [--foo] [--] [<bar> [<info>]]\"\n    end\n\n    it \"short\" do\n      TestCommand.new.option(\"foo\").argument(\"bar\").synopsis(true).should eq \"namespace:name [options] [--] [<bar>]\"\n    end\n  end\n\n  describe \"#usages\" do\n    it \"that starts with the command's name\" do\n      TestCommand.new.usage(\"namespace:name foo\").usages.should contain \"namespace:name foo\"\n    end\n\n    it \"that doesn't include the command's name\" do\n      TestCommand.new.usage(\"bar\").usages.should contain \"namespace:name bar\"\n    end\n  end\n\n  # TODO: Does `#merge_application_definition` need explicit tests?\n\n  describe \"#run\" do\n    it \"interactive\" do\n      tester = ACON::Spec::CommandTester.new TestCommand.new\n      tester.execute interactive: true\n      tester.display.should eq \"interact called#{EOL}execute called#{EOL}\"\n    end\n\n    it \"non-interactive\" do\n      tester = ACON::Spec::CommandTester.new TestCommand.new\n      tester.execute interactive: false\n      tester.display.should eq \"execute called#{EOL}\"\n    end\n\n    it \"invalid option\" do\n      tester = ACON::Spec::CommandTester.new TestCommand.new\n\n      expect_raises ACON::Exception::InvalidOption, \"The '--bar' option does not exist.\" do\n        tester.execute \"--bar\": true\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/command_tester_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct CommandTesterTest < ASPEC::TestCase\n  @command : ACON::Command\n  @tester : ACON::Spec::CommandTester\n\n  def initialize\n    @command = ACON::Commands::Generic.new \"foo\" do |_, output|\n      output.puts \"foo\"\n\n      ACON::Command::Status::SUCCESS\n    end\n    @command.argument \"command\"\n    @command.argument \"foo\"\n\n    @tester = ACON::Spec::CommandTester.new @command\n    @tester.execute foo: \"bar\", interactive: false, decorated: false, verbosity: :verbose\n  end\n\n  def test_execute : Nil\n    @tester.input.interactive?.should be_false\n    @tester.output.decorated?.should be_false\n    @tester.output.verbosity.verbose?.should be_true\n  end\n\n  def test_input : Nil\n    @tester.input.argument(\"foo\").should eq \"bar\"\n  end\n\n  def test_output : Nil\n    @tester.output.to_s.should eq \"foo#{EOL}\"\n  end\n\n  def test_display : Nil\n    @tester.display.to_s.should eq \"foo#{EOL}\"\n  end\n\n  def test_display_before_calling_execute : Nil\n    tester = ACON::Spec::CommandTester.new ACON::Commands::Generic.new \"foo\" { ACON::Command::Status::SUCCESS }\n\n    expect_raises ACON::Exception::Logic, \"Output not initialized. Did you execute the command before requesting the display?\" do\n      tester.display\n    end\n  end\n\n  def test_status_code : Nil\n    @tester.status.should eq ACON::Command::Status::SUCCESS\n  end\n\n  def test_command_from_application : Nil\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n\n    app.register \"foo\" { |_, output| output.puts \"foo\"; ACON::Command::Status::SUCCESS }\n\n    tester = ACON::Spec::CommandTester.new app.find \"foo\"\n\n    tester.execute.should eq ACON::Command::Status::SUCCESS\n  end\n\n  def test_command_with_inputs : Nil\n    questions = {\n      \"What is your name?\",\n      \"How are you?\",\n      \"Where do you come from?\",\n    }\n\n    command = ACON::Commands::Generic.new \"foo\" do |input, output, c|\n      helper = c.helper ACON::Helper::Question\n\n      helper.ask input, output, ACON::Question(String?).new questions[0], nil\n      helper.ask input, output, ACON::Question(String?).new questions[1], nil\n      helper.ask input, output, ACON::Question(String?).new questions[2], nil\n\n      ACON::Command::Status::SUCCESS\n    end\n    command.helper_set = ACON::Helper::HelperSet.new ACON::Helper::Question.new\n\n    tester = ACON::Spec::CommandTester.new command\n    tester.inputs = [\"Bobby\", \"Fine\", \"Germany\"]\n    tester.execute\n\n    tester.status.should eq ACON::Command::Status::SUCCESS\n    tester.display.should eq questions.join\n  end\n\n  def test_command_with_inputs_with_defaults : Nil\n    questions = {\n      \"What is your name?\",\n      \"How are you?\",\n      \"Where do you come from?\",\n    }\n\n    command = ACON::Commands::Generic.new \"foo\" do |input, output, c|\n      helper = c.helper ACON::Helper::Question\n\n      helper.ask input, output, ACON::Question(String).new questions[0], \"Bobby\"\n      helper.ask input, output, ACON::Question(String).new questions[1], \"Fine\"\n      helper.ask input, output, ACON::Question(String).new questions[2], \"Estonia\"\n\n      ACON::Command::Status::SUCCESS\n    end\n    command.helper_set = ACON::Helper::HelperSet.new ACON::Helper::Question.new\n\n    tester = ACON::Spec::CommandTester.new command\n    tester.inputs = [\"\", \"\", \"\"]\n    tester.execute\n\n    tester.status.should eq ACON::Command::Status::SUCCESS\n    tester.display.should eq questions.join\n  end\n\n  def test_command_with_inputs_wrong_input_amount : Nil\n    questions = {\n      \"What is your name?\",\n      \"How are you?\",\n      \"Where do you come from?\",\n    }\n\n    command = ACON::Commands::Generic.new \"foo\" do |input, output, c|\n      helper = c.helper ACON::Helper::Question\n\n      helper.ask input, output, ACON::Question::Choice.new \"choice\", {\"a\", \"b\"}\n      helper.ask input, output, ACON::Question(String?).new questions[0], nil\n      helper.ask input, output, ACON::Question(String?).new questions[1], nil\n      helper.ask input, output, ACON::Question(String?).new questions[2], nil\n\n      ACON::Command::Status::SUCCESS\n    end\n    command.helper_set = ACON::Helper::HelperSet.new ACON::Helper::Question.new\n\n    tester = ACON::Spec::CommandTester.new command\n    tester.inputs = [\"a\", \"Bobby\", \"Fine\"]\n\n    expect_raises ACON::Exception::MissingInput, \"Aborted.\" do\n      tester.execute\n    end\n  end\n\n  def ptest_command_with_questions_but_no_input : Nil\n    questions = {\n      \"What is your name?\",\n      \"How are you?\",\n      \"Where do you come from?\",\n    }\n\n    command = ACON::Commands::Generic.new \"foo\" do |input, output, c|\n      helper = c.helper ACON::Helper::Question\n\n      helper.ask input, output, ACON::Question::Choice.new \"choice\", {\"a\", \"b\"}\n      helper.ask input, output, ACON::Question(String?).new questions[0], nil\n      helper.ask input, output, ACON::Question(String?).new questions[1], nil\n      helper.ask input, output, ACON::Question(String?).new questions[2], nil\n\n      ACON::Command::Status::SUCCESS\n    end\n    command.helper_set = ACON::Helper::HelperSet.new ACON::Helper::Question.new\n\n    tester = ACON::Spec::CommandTester.new command\n\n    expect_raises ACON::Exception::MissingInput, \"Aborted.\" do\n      tester.execute\n    end\n  end\n\n  def test_athena_style_command_with_inputs : Nil\n    questions = {\n      \"What is your name?\",\n      \"How are you?\",\n      \"Where do you come from?\",\n    }\n\n    command = ACON::Commands::Generic.new \"foo\" do |input, output|\n      style = ACON::Style::Athena.new input, output\n\n      style.ask ACON::Question(String?).new questions[0], nil\n      style.ask ACON::Question(String?).new questions[1], nil\n      style.ask ACON::Question(String?).new questions[2], nil\n\n      ACON::Command::Status::SUCCESS\n    end\n\n    tester = ACON::Spec::CommandTester.new command\n    tester.inputs = [\"Bobby\", \"Fine\", \"France\"]\n    tester.execute.should eq ACON::Command::Status::SUCCESS\n  end\n\n  def test_error_output : Nil\n    command = ACON::Commands::Generic.new \"foo\" do |_, output|\n      output.as(ACON::Output::ConsoleOutput).error_output.print \"foo\"\n\n      ACON::Command::Status::SUCCESS\n    end\n    command.argument \"command\"\n    command.argument \"foo\"\n\n    tester = ACON::Spec::CommandTester.new command\n    tester.execute foo: \"bar\", capture_stderr_separately: true\n\n    tester.error_output.should eq \"foo\"\n  end\n\n  def test_assert_command_is_not_successful : Nil\n    command = ACON::Commands::Generic.new \"foo\" do |_, _|\n      ACON::Command::Status::FAILURE\n    end\n\n    tester = ACON::Spec::CommandTester.new command\n    tester.execute\n    tester.assert_command_is_not_successful\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/commands/complete_spec.cr",
    "content": "require \"../spec_helper\"\n\n@[ACONA::AsCommand(\"hello|ahoy\", description: \"Hello test command\")]\nprivate class HelloCommand < ACON::Command\n  protected def configure : Nil\n    self\n      .argument(\"name\", :required)\n  end\n\n  def complete(input : ACON::Completion::Input, suggestions : ACON::Completion::Suggestions) : Nil\n    if input.must_suggest_argument_values_for? \"name\"\n      suggestions.suggest_values \"Athena\", \"Crystal\", \"Ruby\"\n    end\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n\nstruct CompleteCommandTest < ASPEC::TestCase\n  @command : ACON::Commands::Complete\n  @application : ACON::Application\n  @tester : ACON::Spec::CommandTester\n\n  def initialize\n    @command = ACON::Commands::Complete.new\n\n    @application = ACON::Application.new \"TEST\"\n    @application.add HelloCommand.new\n\n    @command.application = @application\n\n    @tester = ACON::Spec::CommandTester.new @command\n  end\n\n  def test_required_shell_option : Nil\n    expect_raises ACON::Exception::Runtime, \"The '--shell' option must be set.\" do\n      self.execute\n    end\n  end\n\n  def test_unsupported_shell_option : Nil\n    expect_raises ACON::Exception::Runtime, \"Shell completion is not supported for your shell: 'unsupported' (supported: 'bash', 'fish', 'zsh').\" do\n      self.execute({\"--shell\" => \"unsupported\"})\n    end\n  end\n\n  def test_completes_command_name_with_loader : Nil\n    @application.command_loader = ACON::Loader::Factory.new({\n      \"foo:bar1\" => -> { Foo1Command.new.as ACON::Command },\n    })\n\n    self.execute({\"--current\" => \"0\", \"--input\" => [] of String})\n    @tester.display.should eq \"#{[\"help\", \"list\", \"completion\", \"hello\", \"ahoy\", \"foo:bar1\", \"afoobar1\"].join(\"\\n\")}#{EOL}\"\n  end\n\n  def test_additional_shell_support : Nil\n    @command = ACON::Commands::Complete.new({\"supported\" => ACON::Completion::Output::Bash} of String => ACON::Completion::Output::Interface.class)\n    @command.application = @application\n    @tester = ACON::Spec::CommandTester.new @command\n\n    self.execute({\"--shell\" => \"supported\", \"--current\" => \"0\", \"--input\" => [] of String})\n\n    # Default shell should still be supported\n    self.execute({\"--shell\" => \"bash\", \"--current\" => \"0\", \"--input\" => [] of String})\n  end\n\n  @[DataProvider(\"input_and_current_option_provider\")]\n  def test_input_and_current_option_validation(input : Hash(String, _), exception_message : String?) : Nil\n    if exception_message\n      expect_raises ::Exception, exception_message do\n        self.execute input.merge!({\"--shell\" => \"bash\"})\n      end\n\n      return\n    end\n\n    self.execute input.merge!({\"--shell\" => \"bash\"})\n\n    @tester.assert_command_is_successful\n  end\n\n  def input_and_current_option_provider : Tuple\n    {\n      {Hash(String, String).new, \"The '--current' option must be set and it must be an integer\"},\n      { {\"--current\" => \"a\"}, \"The '--current' option must be set and it must be an integer\" },\n      { {\"--current\" => \"0\", \"--input\" => [] of String}, nil },\n      { {\"--current\" => \"2\", \"--input\" => [] of String}, \"Current index is invalid, it must be the number of input tokens.\" },\n      { {\"--current\" => \"0\", \"--input\" => [] of String}, nil },\n      { {\"--current\" => \"1\", \"--input\" => [\"foo:bar\"] of String}, nil },\n      { {\"--current\" => \"2\", \"--input\" => [\"foo:bar\", \"bar\"] of String}, nil },\n    }\n  end\n\n  @[DataProvider(\"provide_complete_command_name_inputs\")]\n  def test_completion_command_name(input : Array(String), suggestions : Array(String)) : Nil\n    self.execute({\"--current\" => \"0\", \"--input\" => input})\n    @tester.display.should eq \"#{suggestions.join(\"\\n\")}#{EOL}\"\n  end\n\n  def provide_complete_command_name_inputs : Hash\n    {\n      \"empty\"                  => {[] of String, [\"help\", \"list\", \"completion\", \"hello\", \"ahoy\"]},\n      \"partial\"                => {[\"he\"], [\"help\", \"list\", \"completion\", \"hello\", \"ahoy\"]},\n      \"complete shortcut name\" => {[\"hell\"], [\"hello\", \"ahoy\"]},\n      \"complete alias\"         => {[\"ah\"], [\"hello\", \"ahoy\"]},\n    }\n  end\n\n  @[DataProvider(\"provide_input_definition_inputs\")]\n  def test_completion_command_input_definitions(input : Array(String), suggestions : Array(String)) : Nil\n    self.execute({\"--current\" => \"1\", \"--input\" => input})\n    @tester.display.should eq \"#{suggestions.join(\"\\n\")}#{EOL}\"\n  end\n\n  def provide_input_definition_inputs : Hash\n    {\n      \"definition\"         => {[\"hello\", \"-\"], [\"--help\", \"--silent\", \"--quiet\", \"--verbose\", \"--version\", \"--ansi\", \"--no-ansi\", \"--no-interaction\"]},\n      \"custom\"             => {[\"hello\"], [\"Athena\", \"Crystal\", \"Ruby\"]},\n      \"aliased definition\" => {[\"ahoy\", \"-\"], [\"--help\", \"--silent\", \"--quiet\", \"--verbose\", \"--version\", \"--ansi\", \"--no-ansi\", \"--no-interaction\"]},\n      \"aliased custom\"     => {[\"ahoy\"], [\"Athena\", \"Crystal\", \"Ruby\"]},\n    }\n  end\n\n  private def execute(input : Hash(String, _) = {} of String => String) : Nil\n    # Run in verbose mode to assert exceptions\n    @tester.execute(\n      (!input.empty? ? {\"--shell\" => \"bash\", \"--api-version\" => ACON::Commands::Complete::API_VERSION.to_s}.merge(input) : input),\n      verbosity: ACON::Output::Verbosity::DEBUG\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/commands/dump_completion_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct DumpCompletionCommandTest < ASPEC::TestCase\n  @[DataProvider(\"complete_provider\")]\n  def test_complete(input : Array(String), expected_suggestions : Array(String)) : Nil\n    tester = ACON::Spec::CommandCompletionTester.new ACON::Commands::DumpCompletion.new\n    suggestions = tester.complete input\n\n    suggestions.should eq expected_suggestions\n  end\n\n  def complete_provider : Hash\n    {\n      \"shell\" => {[] of String, [\"bash\", \"fish\", \"zsh\"]},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/commands/help_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct HelpCommandTest < ASPEC::TestCase\n  def test_execute_alias : Nil\n    command = ACON::Commands::Help.new\n    command.application = ACON::Application.new \"foo\"\n\n    tester = ACON::Spec::CommandTester.new command\n    tester.execute command_name: \"li\", decorated: false\n\n    tester.display.should contain \"list [options] [--] [<namespace>]\"\n    tester.display.should contain \"format=FORMAT\"\n    tester.display.should contain \"raw\"\n  end\n\n  def test_execute : Nil\n    command = ACON::Commands::Help.new\n    command.application = ACON::Application.new \"foo\"\n\n    tester = ACON::Spec::CommandTester.new command\n    tester.execute command_name: \"li\", decorated: false\n\n    tester.display.should contain \"list [options] [--] [<namespace>]\"\n    tester.display.should contain \"format=FORMAT\"\n    tester.display.should contain \"raw\"\n  end\n\n  def test_execute_application_command : Nil\n    app = ACON::Application.new \"foo\"\n    tester = ACON::Spec::CommandTester.new app.get \"help\"\n    tester.execute command_name: \"list\"\n\n    tester.display.should contain \"list [options] [--] [<namespace>]\"\n    tester.display.should contain \"format=FORMAT\"\n    tester.display.should contain \"raw\"\n  end\n\n  @[DataProvider(\"complete_provider\")]\n  def test_complete(input : Array(String), expected_suggestions : Array(String)) : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n\n    tester = ACON::Spec::CommandCompletionTester.new app.get \"help\"\n    suggestions = tester.complete input\n\n    suggestions.should eq expected_suggestions\n  end\n\n  def complete_provider : Hash\n    {\n      \"long option\"  => {[\"--format\"], [\"txt\"]},\n      \"nothing\"      => {[] of String, [\"completion\", \"help\", \"list\", \"foo:bar\"]},\n      \"command name\" => {[\"f\"], [\"completion\", \"help\", \"list\", \"foo:bar\"]},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/commands/lazy_spec.cr",
    "content": "require \"../spec_helper\"\n\n@[ACONA::AsCommand(\"blahhhh\")]\nprivate class MockCommand < ACON::Command\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n\ndescribe ACON::Commands::Lazy do\n  it \"applies metadata to the instantiated command\" do\n    lazy_command = ACON::Commands::Lazy.new \"cmd_name\", [\"foo\", \"bar\"], \"description\", true, -> { MockCommand.new.as ACON::Command }\n    command = lazy_command.command\n\n    command.should be_a MockCommand\n    command.name.should eq \"cmd_name\"\n    command.aliases.should eq [\"foo\", \"bar\"]\n    command.description.should eq \"description\"\n    command.hidden?.should be_true\n  end\n\n  it \"forwards methods to the wrapped command instance\" do\n    mock_command = MockCommand.new\n\n    lazy_command = ACON::Commands::Lazy.new \"cmd_name\", [\"foo\", \"bar\"], \"description\", true, -> { mock_command.as ACON::Command }\n    command = lazy_command.command\n\n    command.helper_set = ACON::Helper::HelperSet.new ACON::Helper::Question.new\n    command.process_title \"title\"\n    command.usage \"usages\"\n    command.argument \"name\"\n    command.option \"active\"\n\n    command.definition.should eq mock_command.definition\n    command.help.should eq mock_command.help\n    command.processed_help.should eq mock_command.processed_help\n    command.synopsis.should eq mock_command.synopsis\n    command.usages.should eq mock_command.usages\n    command.helper(ACON::Helper::Question).should eq mock_command.helper(ACON::Helper::Question)\n  end\n\n  it \"is runnable\" do\n    command = MockCommand.new\n    command.application = ACON::Application.new \"foo\"\n\n    tester = ACON::Spec::CommandTester.new command\n    tester.execute.should eq ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/commands/list_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def normalize(input : String) : String\n  input.gsub EOL, \"\\n\"\nend\n\nstruct ListCommandTest < ASPEC::TestCase\n  def test_execute_lists_commands : Nil\n    app = ACON::Application.new \"foo\"\n    tester = ACON::Spec::CommandTester.new app.get(\"list\")\n    tester.execute command: \"list\", decorated: false\n\n    tester.display.should match /help\\s{2,}Display help for a command/\n  end\n\n  def test_with_raw_option : Nil\n    app = ACON::Application.new \"foo\"\n    tester = ACON::Spec::CommandTester.new app.get(\"list\")\n    tester.execute command: \"list\", \"--raw\": true\n\n    tester.display.should eq \"completion   Dump the shell completion script\\nhelp         Display help for a command\\nlist         List available commands\\n\"\n  end\n\n  def test_with_namespace_argument : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n\n    tester = ACON::Spec::CommandTester.new app.get(\"list\")\n    tester.execute command: \"list\", namespace: \"foo\", \"--raw\": true\n\n    tester.display.should eq \"foo:bar   The foo:bar command\\n\"\n  end\n\n  def test_lists_command_in_expected_order : Nil\n    app = ACON::Application.new \"foo\"\n    app.add Foo6Command.new\n\n    tester = ACON::Spec::CommandTester.new app.get(\"list\")\n    tester.execute command: \"list\", decorated: false\n\n    tester.display(true).should eq normalize <<-OUTPUT\n      foo UNKNOWN\n\n      Usage:\n        command [options] [arguments]\n\n      Options:\n        -h, --help            Display help for the given command. When no command is given display help for the list command\n            --silent          Do not output any message\n        -q, --quiet           Only errors are displayed. All other output is suppressed\n        -V, --version         Display this application version\n            --ansi|--no-ansi  Force (or disable --no-ansi) ANSI output\n        -n, --no-interaction  Do not ask any interactive question\n        -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\n\n      Available commands:\n        completion  Dump the shell completion script\n        help        Display help for a command\n        list        List available commands\n       0foo\n        0foo:bar    0foo:bar command\\n\n      OUTPUT\n  end\n\n  def test_lists_commands_in_expected_order_in_raw_mode : Nil\n    app = ACON::Application.new \"foo\"\n    app.add Foo6Command.new\n\n    tester = ACON::Spec::CommandTester.new app.get(\"list\")\n    tester.execute command: \"list\", \"--raw\": true\n\n    tester.display.should eq \"completion   Dump the shell completion script\\nhelp         Display help for a command\\nlist         List available commands\\n0foo:bar     0foo:bar command\\n\"\n  end\n\n  @[DataProvider(\"complete_provider\")]\n  def test_complete(input : Array(String), expected_suggestions : Array(String)) : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n\n    tester = ACON::Spec::CommandCompletionTester.new app.get \"list\"\n    suggestions = tester.complete input\n\n    suggestions.should eq expected_suggestions\n  end\n\n  def test_complete_var_arg : Nil\n    app = ACON::Application.new \"foo\"\n    app.add FooCommand.new\n\n    ACON::Spec::CommandCompletionTester\n      .new(app.get \"list\")\n      .complete(\"--format\")\n      .should eq [\"txt\"]\n  end\n\n  def complete_provider : Hash\n    {\n      \"--format option\"   => {[\"--format\"], [\"txt\"]},\n      \"empty namespace\"   => {[] of String, [\"_global\", \"foo\"]},\n      \"partial namespace\" => {[\"f\"], [\"_global\", \"foo\"]},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/compiler_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe Athena::Console do\n  describe \"compiler errors\", tags: \"compiled\" do\n    describe \"when a command configured via annotation doesn't have a name\" do\n      it \"non hidden no aliases\" do\n        ASPEC::Methods.assert_compile_time_error \"Console command 'NoNameCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.\", <<-CR\n          require \"./spec_helper.cr\"\n\n          @[ACONA::AsCommand]\n          class NoNameCommand < ACON::Command\n            protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n              ACON::Command::Status::SUCCESS\n            end\n          end\n\n          NoNameCommand.default_name\n        CR\n      end\n\n      it \"hidden\" do\n        ASPEC::Methods.assert_compile_time_error \"Console command 'NoNameCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.\", <<-CR\n          require \"./spec_helper.cr\"\n\n          @[ACONA::AsCommand(hidden: true)]\n          class NoNameCommand < ACON::Command\n            protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n              ACON::Command::Status::SUCCESS\n            end\n          end\n\n          NoNameCommand.default_name\n        CR\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/completion/input_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias Input = ACON::Completion::Input\n\nstruct CompletionInputTest < ASPEC::TestCase\n  @[DataProvider(\"bind_data_provider\")]\n  def test_bind(input : Input, expected_type : Input::Type, expected_name : String?, expected_value : String) : Nil\n    definition = ACON::Input::Definition.new(\n      ACON::Input::Option.new(\"with-required-value\", \"r\", :required),\n      ACON::Input::Option.new(\"with-optional-value\", \"o\", :optional),\n      ACON::Input::Option.new(\"without-value\", \"n\", :none),\n      ACON::Input::Argument.new(\"required-arg\", :required),\n      ACON::Input::Argument.new(\"optional-arg\", :optional),\n    )\n\n    input.bind definition\n\n    input.completion_type.should eq expected_type\n    input.completion_name.should eq expected_name\n    input.completion_value.should eq expected_value\n    input.must_suggest_option_values_for?(\"with-required-value\").should be_true if expected_value.starts_with? \"athe\"\n  end\n\n  def bind_data_provider : Hash\n    {\n      # Option names\n      \"optname minimal input\" => {Input.from_tokens([\"-\"], 0), Input::Type::OPTION_NAME, nil, \"-\"},\n      \"optname partial\"       => {Input.from_tokens([\"--with\"], 0), Input::Type::OPTION_NAME, nil, \"--with\"},\n\n      # Option values\n      \"optvalue short\"                => {Input.from_tokens([\"-r\"], 0), Input::Type::OPTION_VALUE, \"with-required-value\", \"\"},\n      \"optvalue short partial\"        => {Input.from_tokens([\"-rathe\"], 0), Input::Type::OPTION_VALUE, \"with-required-value\", \"athe\"},\n      \"optvalue short space\"          => {Input.from_tokens([\"-r\"], 1), Input::Type::OPTION_VALUE, \"with-required-value\", \"\"},\n      \"optvalue short space partial\"  => {Input.from_tokens([\"-r\", \"athe\"], 1), Input::Type::OPTION_VALUE, \"with-required-value\", \"athe\"},\n      \"optvalue short before arg\"     => {Input.from_tokens([\"-r\", \"athena\"], 0), Input::Type::OPTION_VALUE, \"with-required-value\", \"\"},\n      \"optvalue short optional\"       => {Input.from_tokens([\"-o\"], 0), Input::Type::OPTION_VALUE, \"with-optional-value\", \"\"},\n      \"optvalue short space optional\" => {Input.from_tokens([\"-o\"], 1), Input::Type::OPTION_VALUE, \"with-optional-value\", \"\"},\n\n      \"optvalue long\"                => {Input.from_tokens([\"--with-required-value=\"], 0), Input::Type::OPTION_VALUE, \"with-required-value\", \"\"},\n      \"optvalue long partial\"        => {Input.from_tokens([\"--with-required-value=ath\"], 0), Input::Type::OPTION_VALUE, \"with-required-value\", \"ath\"},\n      \"optvalue long space\"          => {Input.from_tokens([\"--with-required-value\"], 1), Input::Type::OPTION_VALUE, \"with-required-value\", \"\"},\n      \"optvalue long space partial\"  => {Input.from_tokens([\"--with-required-value\", \"ath\"], 1), Input::Type::OPTION_VALUE, \"with-required-value\", \"ath\"},\n      \"optvalue long optional\"       => {Input.from_tokens([\"--with-optional-value=\"], 0), Input::Type::OPTION_VALUE, \"with-optional-value\", \"\"},\n      \"optvalue long space optional\" => {Input.from_tokens([\"--with-optional-value\"], 1), Input::Type::OPTION_VALUE, \"with-optional-value\", \"\"},\n\n      # Arguments\n      \"arg minimal input\"         => {Input.from_tokens([] of String, 0), Input::Type::ARGUMENT_VALUE, \"required-arg\", \"\"},\n      \"arg optional\"              => {Input.from_tokens([\"athena\"], 1), Input::Type::ARGUMENT_VALUE, \"optional-arg\", \"\"},\n      \"arg partial\"               => {Input.from_tokens([\"ath\"], 0), Input::Type::ARGUMENT_VALUE, \"required-arg\", \"ath\"},\n      \"arg optional partial\"      => {Input.from_tokens([\"athena\", \"cry\"], 1), Input::Type::ARGUMENT_VALUE, \"optional-arg\", \"cry\"},\n      \"arg after option\"          => {Input.from_tokens([\"--without-value\"], 1), Input::Type::ARGUMENT_VALUE, \"required-arg\", \"\"},\n      \"arg after optional option\" => {Input.from_tokens([\"--with-optional-value\", \"--\"], 2), Input::Type::ARGUMENT_VALUE, \"required-arg\", \"\"},\n\n      # End of definition\n      \"end\" => {Input.from_tokens([\"athena\", \"crystal\"], 2), Input::Type::NONE, nil, \"\"},\n    }\n  end\n\n  @[DataProvider(\"last_array_argument_provider\")]\n  def test_bind_with_last_array_argument(input : Input, expected_value : String?) : Nil\n    definition = ACON::Input::Definition.new(\n      ACON::Input::Argument.new(\"list-arg\", ACON::Input::Argument::Mode[:required, :is_array]),\n    )\n\n    input.bind definition\n\n    input.completion_type.should eq Input::Type::ARGUMENT_VALUE\n    input.completion_name.should eq \"list-arg\"\n    input.completion_value.should eq expected_value\n  end\n\n  def last_array_argument_provider : Tuple\n    {\n      {Input.from_tokens([] of String, 0), \"\"},\n      {Input.from_tokens([\"athena\", \"crystal\"], 2), \"\"},\n      {Input.from_tokens([\"athena\", \"cry\"], 1), \"cry\"},\n    }\n  end\n\n  def test_bind_argument_with_default : Nil\n    definition = ACON::Input::Definition.new(\n      ACON::Input::Argument.new(\"arg-with-default\", :optional, default: \"default\"),\n    )\n\n    input = Input.from_tokens [] of String, 0\n    input.bind definition\n\n    input.completion_type.should eq Input::Type::ARGUMENT_VALUE\n    input.completion_name.should eq \"arg-with-default\"\n    input.completion_value.should eq \"\"\n    input.must_suggest_argument_values_for?(\"arg-with-default\").should be_true\n  end\n\n  @[DataProvider(\"from_string_provider\")]\n  def test_from_string(input_string : String, expected_tokens : Array(String)) : Nil\n    input = Input.from_string input_string, 1\n\n    input.@tokens.should eq expected_tokens\n  end\n\n  def from_string_provider : Tuple\n    {\n      {\"do:thing\", [\"do:thing\"]},\n      {\"--env prod\", [\"--env\", \"prod\"]},\n      {\"--env=prod\", [\"--env=prod\"]},\n      {\"-eprod\", [\"-eprod\"]},\n      { %(do:thing \"multi word string\"), [\"do:thing\", %(\"multi word string\")] },\n      {\"do:thing 'multi word string'\", [\"do:thing\", \"'multi word string'\"]},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/completion/output/bash_spec.cr",
    "content": "require \"./completion_output_test_case\"\n\nstruct BashTest < CompletionOutputTestCase\n  def completion_output : ACON::Completion::Output::Interface\n    ACON::Completion::Output::Bash.new\n  end\n\n  def expected_options_output : String\n    \"--option1\\n--negatable\\n--no-negatable#{EOL}\"\n  end\n\n  def expected_values_output : String\n    \"Green\\nRed\\nYellow#{EOL}\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/completion/output/completion_output_test_case.cr",
    "content": "require \"../../spec_helper\"\n\nabstract struct CompletionOutputTestCase < ASPEC::TestCase\n  abstract def completion_output : ACON::Completion::Output::Interface\n  abstract def expected_options_output : String\n  abstract def expected_values_output : String\n\n  def test_options : Nil\n    options = [\n      ACON::Input::Option.new(\"option1\", \"o\", :none, \"First Option\"),\n      ACON::Input::Option.new(\"negatable\", nil, :negatable, \"Can be negative\"),\n    ]\n\n    suggestions = ACON::Completion::Suggestions.new\n    suggestions.suggest_options options\n\n    buffer = IO::Memory.new\n\n    self.completion_output.write suggestions, ACON::Output::IO.new buffer\n\n    buffer.to_s.should eq self.expected_options_output\n  end\n\n  def test_values : Nil\n    suggestions = ACON::Completion::Suggestions.new\n    suggestions.suggest_value \"Green\", \"Beans are green\"\n    suggestions.suggest_value \"Red\", \"Roses are red\"\n    suggestions.suggest_value \"Yellow\", \"Canaries are yellow\"\n\n    buffer = IO::Memory.new\n\n    self.completion_output.write suggestions, ACON::Output::IO.new buffer\n\n    buffer.to_s.should eq self.expected_values_output\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/completion/output/fish_spec.cr",
    "content": "require \"./completion_output_test_case\"\n\nstruct FishTest < CompletionOutputTestCase\n  def completion_output : ACON::Completion::Output::Interface\n    ACON::Completion::Output::Fish.new\n  end\n\n  def expected_options_output : String\n    \"--option1\\n--negatable\\n--no-negatable\"\n  end\n\n  def expected_values_output : String\n    \"Green\\nRed\\nYellow\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/completion/output/zsh_spec.cr",
    "content": "require \"./completion_output_test_case\"\n\nstruct ZshTest < CompletionOutputTestCase\n  def completion_output : ACON::Completion::Output::Interface\n    ACON::Completion::Output::Zsh.new\n  end\n\n  def expected_options_output : String\n    \"--option1\\tFirst Option\\n--negatable\\tCan be negative\\n--no-negatable\\tCan be negative\\n\"\n  end\n\n  def expected_values_output : String\n    \"Green\\tBeans are green\\nRed\\tRoses are red\\nYellow\\tCanaries are yellow\\n\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/cursor_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct CursorTest < ASPEC::TestCase\n  @cursor : ACON::Cursor\n  @output : ACON::Output::IO\n\n  def initialize\n    @output = ACON::Output::IO.new IO::Memory.new\n    @cursor = ACON::Cursor.new @output\n  end\n\n  def test_move_up_one_line : Nil\n    @cursor.move_up\n    @output.to_s.should eq \"\\x1b[1A\"\n  end\n\n  def test_move_up_multiple_lines : Nil\n    @cursor.move_up 12\n    @output.to_s.should eq \"\\x1b[12A\"\n  end\n\n  def test_move_down_one_line : Nil\n    @cursor.move_down\n    @output.to_s.should eq \"\\x1b[1B\"\n  end\n\n  def test_move_down_multiple_lines : Nil\n    @cursor.move_down 12\n    @output.to_s.should eq \"\\x1b[12B\"\n  end\n\n  def test_move_right_one_line : Nil\n    @cursor.move_right\n    @output.to_s.should eq \"\\x1b[1C\"\n  end\n\n  def test_move_right_multiple_lines : Nil\n    @cursor.move_right 12\n    @output.to_s.should eq \"\\x1b[12C\"\n  end\n\n  def test_move_left_one_line : Nil\n    @cursor.move_left\n    @output.to_s.should eq \"\\x1b[1D\"\n  end\n\n  def test_move_left_multiple_lines : Nil\n    @cursor.move_left 12\n    @output.to_s.should eq \"\\x1b[12D\"\n  end\n\n  def test_move_to_column : Nil\n    @cursor.move_to_column 5\n    @output.to_s.should eq \"\\x1b[5G\"\n  end\n\n  def test_move_to_position : Nil\n    @cursor.move_to_position 18, 16\n    @output.to_s.should eq \"\\x1b[17;18H\"\n  end\n\n  def test_clear_line : Nil\n    @cursor.clear_line\n    @output.to_s.should eq \"\\x1b[2K\"\n  end\n\n  def test_clear_line_after : Nil\n    @cursor.clear_line_after\n    @output.to_s.should eq \"\\x1b[K\"\n  end\n\n  def test_clear_screen : Nil\n    @cursor.clear_screen\n    @output.to_s.should eq \"\\x1b[2J\"\n  end\n\n  def test_save_position : Nil\n    @cursor.save_position\n    @output.to_s.should eq \"\\x1b7\"\n  end\n\n  def test_restore_position : Nil\n    @cursor.restore_position\n    @output.to_s.should eq \"\\x1b8\"\n  end\n\n  def test_hide : Nil\n    @cursor.hide\n    @output.to_s.should eq \"\\x1b[?25l\"\n  end\n\n  def test_show : Nil\n    @cursor.show\n    @output.to_s.should eq \"\\x1b[?25h\\x1b[?0c\"\n  end\n\n  def test_clear_output : Nil\n    @cursor.clear_output\n    @output.to_s.should eq \"\\x1b[0J\"\n  end\n\n  def test_current_position : Nil\n    @cursor = ACON::Cursor.new @output, IO::Memory.new\n\n    @cursor.move_to_position 10, 10\n    position = @cursor.current_position\n\n    @output.to_s.should eq \"\\x1b[11;10H\"\n\n    position.should eq({1, 1})\n  end\n\n  def test_current_position_tty : Nil\n    pending! \"Cursor input must be a TTY\" unless STDIN.tty?\n\n    @cursor = ACON::Cursor.new @output\n\n    @cursor.move_to_position 10, 10\n    position = @cursor.current_position\n\n    @output.to_s.should eq \"\\x1b[11;10H\"\n\n    position.should_not eq({1, 1})\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/descriptor/abstract_descriptor_test_case.cr",
    "content": "require \"../spec_helper\"\nrequire \"./object_provider\"\n\nabstract struct AbstractDescriptorTestCase < ASPEC::TestCase\n  @[DataProvider(\"input_argument_test_data\")]\n  def test_describe_input_argument(object : ACON::Input::Argument, expected : String) : Nil\n    self.assert_description expected, object\n  end\n\n  @[DataProvider(\"input_option_test_data\")]\n  def test_describe_input_option(object : ACON::Input::Option, expected : String) : Nil\n    self.assert_description expected, object\n  end\n\n  @[DataProvider(\"input_definition_test_data\")]\n  def test_describe_input_definition(object : ACON::Input::Definition, expected : String) : Nil\n    self.assert_description expected, object\n  end\n\n  @[DataProvider(\"command_test_data\")]\n  def test_describe_command(object : ACON::Command, expected : String) : Nil\n    self.assert_description expected, object\n  end\n\n  @[DataProvider(\"application_test_data\")]\n  def test_describe_application(object : ACON::Application, expected : String) : Nil\n    self.assert_description expected, object\n  end\n\n  def input_argument_test_data : Array\n    self.description_test_data ObjectProvider.input_arguments\n  end\n\n  def input_option_test_data : Array\n    self.description_test_data ObjectProvider.input_options\n  end\n\n  def input_definition_test_data : Array\n    self.description_test_data ObjectProvider.input_definitions\n  end\n\n  def command_test_data : Array\n    self.description_test_data ObjectProvider.commands\n  end\n\n  def application_test_data : Array\n    self.description_test_data ObjectProvider.applications\n  end\n\n  protected abstract def descriptor : ACON::Descriptor::Interface\n  protected abstract def format : String\n\n  protected def description_test_data(data : Hash(String, _)) : Array\n    data.map do |k, v|\n      normalized_path = File.join __DIR__, \"..\", \"fixtures\", \"text\"\n      {v, File.read \"#{normalized_path}/#{k}.#{self.format}\"}\n    end\n  end\n\n  protected def assert_description(expected : String, object, context : ACON::Descriptor::Context = ACON::Descriptor::Context.new) : Nil\n    output = ACON::Output::IO.new IO::Memory.new\n    context = context.clone\n    context.raw_output = true\n    self.descriptor.describe output, object, context\n    self.normalize_output(output.to_s).should eq self.normalize_output(expected)\n  end\n\n  private def normalize_output(output : String) : String\n    output.gsub(EOL, \"\\n\").strip\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/descriptor/application_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class TestApplication < ACON::Application\n  protected def default_commands : Array(ACON::Command)\n    [] of ACON::Command\n  end\nend\n\nstruct ApplicationDescriptorTest < ASPEC::TestCase\n  @[DataProvider(\"namespace_provider\")]\n  def test_namespaces(expected : Array(String), names : Array(String)) : Nil\n    app = TestApplication.new \"foo\"\n\n    names.each do |name|\n      app.register name do\n        ACON::Command::Status::SUCCESS\n      end\n    end\n\n    ACON::Descriptor::Application.new(app).namespaces.keys.should eq expected\n  end\n\n  def namespace_provider : Tuple\n    {\n      {[\"_global\"], [\"foobar\"]},\n      {[\"a\", \"b\"], [\"b:foo\", \"a:foo\", \"b:bar\"]},\n      {[\"_global\", \"22\", \"33\", \"b\", \"z\"], [\"z:foo\", \"1\", \"33:foo\", \"b:foo\", \"22:foo:bar\"]},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/descriptor/object_provider.cr",
    "content": "module ObjectProvider\n  def self.input_arguments : Hash(String, ACON::Input::Argument)\n    {\n      \"input_argument_1\"          => ACON::Input::Argument.new(\"argument_name\", :required),\n      \"input_argument_2\"          => ACON::Input::Argument.new(\"argument_name\", :is_array, \"argument description\"),\n      \"input_argument_3\"          => ACON::Input::Argument.new(\"argument_name\", :optional, \"argument description\", \"default_value\"),\n      \"input_argument_4\"          => ACON::Input::Argument.new(\"argument_name\", :required, \"multiline\\nargument description\"),\n      \"input_argument_with_style\" => ACON::Input::Argument.new(\"argument_name\", :optional, \"argument description\", \"<comment>style</>\"),\n    }\n  end\n\n  def self.input_options : Hash(String, ACON::Input::Option)\n    {\n      \"input_option_1\"                => ACON::Input::Option.new(\"option_name\", \"o\", :none),\n      \"input_option_2\"                => ACON::Input::Option.new(\"option_name\", \"o\", :optional, \"option description\", \"default_value\"),\n      \"input_option_3\"                => ACON::Input::Option.new(\"option_name\", \"o\", :required, \"option description\"),\n      \"input_option_4\"                => ACON::Input::Option.new(\"option_name\", \"o\", ACON::Input::Option::Value[:optional, :is_array], \"option description\", Array(String).new),\n      \"input_option_5\"                => ACON::Input::Option.new(\"option_name\", \"o\", :required, \"multiline\\noption description\"),\n      \"input_option_6\"                => ACON::Input::Option.new(\"option_name\", {\"o\", \"O\"}, :required, \"option with multiple shortcuts\"),\n      \"input_option_with_style\"       => ACON::Input::Option.new(\"option_name\", \"o\", :required, \"option description\", \"<comment>style</>\"),\n      \"input_option_with_style_array\" => ACON::Input::Option.new(\"option_name\", \"o\", ACON::Input::Option::Value[:required, :is_array], \"option description\", [\"<comment>Hello</comment>\", \"<info>world</info>\"]),\n    }\n  end\n\n  def self.input_definitions : Hash(String, ACON::Input::Definition)\n    {\n      \"input_definition_1\" => ACON::Input::Definition.new,\n      \"input_definition_2\" => ACON::Input::Definition.new(ACON::Input::Argument.new(\"argument_name\", :required)),\n      \"input_definition_3\" => ACON::Input::Definition.new(ACON::Input::Option.new(\"option_name\", \"o\", :none)),\n      \"input_definition_4\" => ACON::Input::Definition.new(\n        ACON::Input::Argument.new(\"argument_name\", :required),\n        ACON::Input::Option.new(\"option_name\", \"o\", :none),\n      ),\n    }\n  end\n\n  def self.commands : Hash(String, ACON::Command)\n    {\n      \"command_1\" => DescriptorCommand1.new,\n      \"command_2\" => DescriptorCommand2.new,\n    }\n  end\n\n  def self.applications : Hash(String, ACON::Application)\n    {\n      \"application_1\" => DescriptorApplication1.new(\"foo\"),\n      \"application_2\" => DescriptorApplication2.new,\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/descriptor/text_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./abstract_descriptor_test_case\"\n\nstruct TextDescriptorTest < AbstractDescriptorTestCase\n  # TODO: Include test data for double width chars\n  # For both Application and Command contexts\n\n  def test_describe_application_filtered_namespace : Nil\n    self.assert_description(\n      File.read(\"#{__DIR__}/../fixtures/text/application_filtered_namespace.txt\"),\n      DescriptorApplication2.new,\n      ACON::Descriptor::Context.new(namespace: \"command4\"),\n    )\n  end\n\n  protected def descriptor : ACON::Descriptor::Interface\n    ACON::Descriptor::Text.new\n  end\n\n  protected def format : String\n    \"txt\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/applications/descriptor1.cr",
    "content": "class DescriptorApplication1 < ACON::Application\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/applications/descriptor2.cr",
    "content": "class DescriptorApplication2 < ACON::Application\n  def initialize\n    super \"My Athena application\", \"1.0.0\"\n\n    self.add DescriptorCommand1.new\n    self.add DescriptorCommand2.new\n    self.add DescriptorCommand3.new\n    self.add DescriptorCommand4.new\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/annotation_configured.cr",
    "content": "@[ACONA::AsCommand(\"annotation:configured\", description: \"Command configured via annotation\", aliases: [\"ac\"])]\nclass AnnotationConfiguredCommand < ACON::Command\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/annotation_configured_aliases.cr",
    "content": "@[ACONA::AsCommand(\"annotation:configured|ac\")]\nclass AnnotationConfiguredAliasesCommand < ACON::Command\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/annotation_configured_hidden.cr",
    "content": "@[ACONA::AsCommand(\"|annotation:configured\")]\nclass AnnotationConfiguredHiddenCommand < ACON::Command\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/annotation_configured_hidden_field.cr",
    "content": "@[ACONA::AsCommand(\"annotation:configured\", hidden: true)]\nclass AnnotationConfiguredHiddenFieldCommand < ACON::Command\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/bar_buc.cr",
    "content": "class BarBucCommand < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"bar:buc\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/descriptor1.cr",
    "content": "class DescriptorCommand1 < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"descriptor:command1\")\n      .aliases(\"alias1\", \"alias2\")\n      .description(\"command 1 description\")\n      .help(\"command 1 help\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/descriptor2.cr",
    "content": "class DescriptorCommand2 < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"descriptor:command2\")\n      .description(\"command 2 description\")\n      .help(\"command 2 help\")\n      .usage(\"-o|--option_name <argument_name>\")\n      .usage(\"<argument_name>\")\n      .argument(\"argument_name\", :required)\n      .option(\"option_name\", \"o\", :none)\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/descriptor3.cr",
    "content": "class DescriptorCommand3 < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"descriptor:command3\")\n      .description(\"command 3 description\")\n      .help(\"command 3 help\")\n      .hidden\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/descriptor4.cr",
    "content": "class DescriptorCommand4 < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"descriptor:command4\")\n      .aliases(\"descriptor:alias_command4\", \"command4:descriptor\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo.cr",
    "content": "class FooCommand < IOCommand\n  protected def configure : Nil\n    self\n      .name(\"foo:bar\")\n      .description(\"The foo:bar command\")\n      .aliases(\"afoobar\")\n  end\n\n  protected def interact(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil\n    output.puts \"interact called\"\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    output.puts \"execute called\"\n\n    super\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo1.cr",
    "content": "class Foo1Command < IOCommand\n  protected def configure : Nil\n    self\n      .name(\"foo:bar1\")\n      .description(\"The foo:bar1 command\")\n      .aliases(\"afoobar1\")\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo2.cr",
    "content": "class Foo2Command < IOCommand\n  protected def configure : Nil\n    self\n      .name(\"foo1:bar\")\n      .description(\"The foo1:bar command\")\n      .aliases(\"afoobar2\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo3.cr",
    "content": "class Foo3Command < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"foo3:bar\")\n      .description(\"The foo3:bar command\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    begin\n      begin\n        raise Exception.new \"First exception <p>this is html</p>\"\n      rescue ex\n        raise Exception.new \"Second exception <comment>comment</comment>\", ex\n      end\n    rescue ex\n      raise Exception.new \"Third exception <fg=blue;bg=red>comment</>\", ex\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo4.cr",
    "content": "class Foo4Command < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"foo3:bar:toh\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo6.cr",
    "content": "class Foo6Command < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"0foo:bar\")\n      .description(\"0foo:bar command\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo_bar.cr",
    "content": "class FooBarCommand < IOCommand\n  protected def configure : Nil\n    self\n      .name(\"foobar:foo\")\n      .description(\"The foobar:foo command\")\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo_hidden.cr",
    "content": "class FooHiddenCommand < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"foo:hidden\")\n      .aliases(\"afoohidden\")\n      .hidden(true)\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo_opt.cr",
    "content": "class FooOptCommand < IOCommand\n  protected def configure : Nil\n    self\n      .name(\"foo:bar\")\n      .description(\"The foo:bar command\")\n      .aliases(\"afoobar\")\n      .option(\"fooopt\", \"f\", :optional, \"fooopt description\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    super\n\n    self.output.puts \"execute called\"\n    self.output.puts input.option(\"fooopt\")\n\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo_same_case_lowercase.cr",
    "content": "class FooSameCaseLowercaseCommand < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"foo:bar\")\n      .description(\"foo:bar command\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo_same_case_uppercase.cr",
    "content": "class FooSameCaseUppercaseCommand < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"foo:BAR\")\n      .description(\"foo:BAR command\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo_subnamespaced1.cr",
    "content": "class FooSubnamespaced1Command < IOCommand\n  protected def configure : Nil\n    self\n      .name(\"foo:bar:baz\")\n      .description(\"The foo:bar:baz command\")\n      .aliases(\"foobarbaz\")\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo_subnamespaced2.cr",
    "content": "class FooSubnamespaced2Command < IOCommand\n  protected def configure : Nil\n    self\n      .name(\"foo:bar:go\")\n      .description(\"The foo:bar:go command\")\n      .aliases(\"foobargo\")\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/foo_without_alias.cr",
    "content": "class FooWithoutAliasCommand < IOCommand\n  protected def configure : Nil\n    self\n      .name(\"foo\")\n      .description(\"The foo command\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    output.puts \"execute called\"\n\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/io.cr",
    "content": "abstract class IOCommand < ACON::Command\n  getter! input : ACON::Input::Interface\n  getter! output : ACON::Output::Interface\n\n  protected def execute(@input : ACON::Input::Interface, @output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/test.cr",
    "content": "class TestCommand < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"namespace:name\")\n      .description(\"description\")\n      .aliases(\"name\")\n      .help(\"help\")\n  end\n\n  protected def interact(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil\n    output.puts \"interact called\"\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    output.puts \"execute called\"\n\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/test_ambiguous_command_registering1.cr",
    "content": "class TestAmbiguousCommandRegistering < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"test-ambiguous\")\n      .description(\"The test-ambiguous command\")\n      .aliases(\"test\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    output.puts \"test-ambiguous\"\n\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/commands/test_ambiguous_command_registering2.cr",
    "content": "class TestAmbiguousCommandRegistering2 < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"test-ambiguous2\")\n      .description(\"The test-ambiguous2 command\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    output.puts \"test-ambiguous2\"\n\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/borderless.txt",
    "content": " =============== ========================== ================== \n  ISBN            Title                      Author            \n =============== ========================== ================== \n  99921-58-10-7   Divine Comedy              Dante Alighieri   \n  9971-5-0210-0   A Tale of Two Cities       Charles Dickens   \n  960-425-059-0   The Lord of the Rings      J. R. R. Tolkien  \n  80-902734-1-6   And Then There Were None   Agatha Christie   \n =============== ========================== ================== \n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/borderless_vertical.txt",
    "content": " ============================== \n    ISBN: 99921-58-10-7         \n   Title: Divine Comedy         \n  Author: Dante Alighieri       \n   Price: 9.95                  \n ============================== \n    ISBN: 9971-5-0210-0         \n   Title: A Tale of Two Cities  \n  Author: Charles Dickens       \n   Price: 139.25                \n ============================== \n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/box.txt",
    "content": "┌───────────────┬──────────────────────────┬──────────────────┐\n│ ISBN          │ Title                    │ Author           │\n├───────────────┼──────────────────────────┼──────────────────┤\n│ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  │\n│ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  │\n│ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien │\n│ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  │\n└───────────────┴──────────────────────────┴──────────────────┘\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/compact.txt",
    "content": "ISBN          Title                    Author           \n99921-58-10-7 Divine Comedy            Dante Alighieri  \n9971-5-0210-0 A Tale of Two Cities     Charles Dickens  \n960-425-059-0 The Lord of the Rings    J. R. R. Tolkien \n80-902734-1-6 And Then There Were None Agatha Christie  \n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/compact_vertical.txt",
    "content": "  ISBN: 99921-58-10-7        \n Title: Divine Comedy        \nAuthor: Dante Alighieri      \n Price: 9.95                 \n\n  ISBN: 9971-5-0210-0        \n Title: A Tale of Two Cities \nAuthor: Charles Dickens      \n Price: 139.25               \n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default.txt",
    "content": "+---------------+--------------------------+------------------+\n| ISBN          | Title                    | Author           |\n+---------------+--------------------------+------------------+\n| 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n| 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens  |\n| 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n| 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n+---------------+--------------------------+------------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_colspan.txt",
    "content": "+-------------------------------+-------------------------------+-----------------------------+\n| ISBN                          | Title                         | Author                      |\n+-------------------------------+-------------------------------+-----------------------------+\n| 99921-58-10-7                 | Divine Comedy                 | Dante Alighieri             |\n+-------------------------------+-------------------------------+-----------------------------+\n| Divine Comedy(Dante Alighieri)                                                              |\n+-------------------------------+-------------------------------+-----------------------------+\n| Arduino: A Quick-Start Guide                                  | Mark Schmidt                |\n+-------------------------------+-------------------------------+-----------------------------+\n| 9971-5-0210-0                 | A Tale of                                                   |\n|                               | Two Cities                                                  |\n+-------------------------------+-------------------------------+-----------------------------+\n| Cupiditate dicta atque porro, tempora exercitationem modi animi nulla nemo vel nihil!       |\n+-------------------------------+-------------------------------+-----------------------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_formatting_tags.txt",
    "content": "+---------------+----------------------+-----------------+\n| ISBN          | Title                | Author          |\n+---------------+----------------------+-----------------+\n| 99921-58-10-7 | Divine Comedy        | Dante Alighieri |\n| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |\n+---------------+----------------------+-----------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_non_formatting_tags.txt",
    "content": "+----------------------------------+----------------------+-----------------+\n| ISBN                             | Title                | Author          |\n+----------------------------------+----------------------+-----------------+\n| <strong>99921-58-10-700</strong> | <f>Divine Com</f>    | Dante Alighieri |\n| 9971-5-0210-0                    | A Tale of Two Cities | Charles Dickens |\n+----------------------------------+----------------------+-----------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_rowspan.txt",
    "content": "+---------------+---------------+-----------------+\n| ISBN          | Title         | Author          |\n+---------------+---------------+-----------------+\n| 9971-5-0210-0 | Divine Comedy | Dante Alighieri |\n|               |               |                 |\n|               | The Lord of   | J. R.           |\n|               | the Rings     | R. Tolkien      |\n+---------------+---------------+-----------------+\n| 80-902734-1-6 | And Then      | Agatha Christie |\n| 80-902734-1-7 | There         | Test            |\n|               | Were None     |                 |\n+---------------+---------------+-----------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan.txt",
    "content": "+------------------+---------+-----------------+\n| ISBN             | Title   | Author          |\n+------------------+---------+-----------------+\n| 9971-5-0210-0              | Dante Alighieri |\n|                            | Charles Dickens |\n+------------------+---------+-----------------+\n| Dante Alighieri  | 9971-5-0210-0             |\n| J. R. R. Tolkien |                           |\n| J. R. R          |                           |\n+------------------+---------+-----------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_and_alignment.txt",
    "content": "+---------------+---------------+-------------------------------------------+\n|          ISBN | Title         |                  Author                   |\n+---------------+---------------+-------------------------------------------+\n|      978      | De Monarchia  |             Dante Alighieri               |\n| 99921-58-10-7 | Divine Comedy | spans multiple rows rows Dante Alighieri  |\n|               |               |         spans multiple rows rows          |\n+---------------+---------------+-------------------------------------------+\n|             test              |                                      tttt |\n+---------------+---------------+-------------------------------------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_and_custom_format.txt",
    "content": "+----------------+---------------+---------------------+\n|\u001b[30;46m ISBN           \u001b[39;49m|\u001b[32m Title         \u001b[39m|\u001b[32m Author              \u001b[39m|\n+----------------+---------------+---------------------+\n| 978-0521567817 | De Monarchia  |\u001b[32m Dante Alighieri     \u001b[39m|\n| 978-0804169127 | Divine Comedy |\u001b[32m spans multiple rows \u001b[39m|\n|\u001b[97;41m test                           \u001b[39;49m| tttt                |\n+----------------+---------------+---------------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_and_fgbg.txt",
    "content": "+---------------+---------------+-------------------------------------------+\n| \u001b[31m978\u001b[39m           | De Monarchia  |\u001b[31;42m             Dante Alighieri               \u001b[39;49m|\n| \u001b[32m99921-58-10-7\u001b[39m | Divine Comedy |\u001b[31;42m spans multiple rows rows Dante Alighieri  \u001b[39;49m|\n|               |               |\u001b[31;42m         spans multiple rows rows          \u001b[39;49m|\n+---------------+---------------+-------------------------------------------+\n|             \u001b[97;41mtest\u001b[39;49m              |\u001b[31;42m                                      tttt \u001b[39;49m|\n+---------------+---------------+-------------------------------------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_and_line_breaks.txt",
    "content": "+-----------------+-------+-----------------+\n| ISBN            | Title | Author          |\n+-----------------+-------+-----------------+\n| 9971                    | Dante Alighieri |\n| -5-                     | Charles Dickens |\n| 021                     |                 |\n| 0-0                     |                 |\n+-----------------+-------+-----------------+\n| Dante Alighieri | 9971                    |\n| Charles Dickens | -5-                     |\n|                 | 021                     |\n|                 | 0-0                     |\n+-----------------+-------+-----------------+\n| 9971                    | Dante           |\n| -5-                     | Alighieri       |\n| 021                     |                 |\n| 0-0                     |                 |\n+-----------------+-------+-----------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_no_separators.txt",
    "content": "+-----------------+-------+-----------------+\n| ISBN            | Title | Author          |\n+-----------------+-------+-----------------+\n| 9971                    | Dante Alighieri |\n| -5-                     | Charles Dickens |\n| 021                     |                 |\n| 0-0                     |                 |\n| Dante Alighieri | 9971                    |\n| Charles Dickens | -5-                     |\n|                 | 021                     |\n|                 | 0-0                     |\n+-----------------+-------+-----------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_separator_in_rowspan.txt",
    "content": "+---------------+-----------------+\n| ISBN          | Author          |\n+---------------+-----------------+\n| 9971-5-0210-0 | Dante Alighieri |\n|               |-----------------|\n|               | Charles Dickens |\n+---------------+-----------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_colspan_and_table_cell_with_comment_style.txt",
    "content": "+-----------------+------------------+---------+\n|\u001b[32m \u001b[39m\u001b[33mLong Title\u001b[39m\u001b[32m                                   \u001b[39m|\n+-----------------+------------------+---------+\n| 9971-5-0210-0                                |\n+-----------------+------------------+---------+\n| Dante Alighieri | J. R. R. Tolkien | J. R. R |\n+-----------------+------------------+---------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_formatted_row_with_line_breaks.txt",
    "content": "+-------+------------+\n\u001b[97;41m| \u001b[39;49m\u001b[97;41mDont break\u001b[39;49m\u001b[97;41m         |\u001b[39;49m\n\u001b[97;41m| here\u001b[39;49m               |\n+-------+------------+\n| foo   | \u001b[97;41mDont break\u001b[39;49m |\n| bar   | \u001b[97;41mhere\u001b[39;49m       |\n+-------+------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_headerless.txt",
    "content": "+---------------+--------------------------+------------------+\n| 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n| 9971-5-0210-0 |                          |                  |\n| 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n| 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n+---------------+--------------------------+------------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_line_break_after_colspan_cell.txt",
    "content": "+-----+-----+-----+\n| Foo | Bar | Baz |\n+-----+-----+-----+\n| foo       | baz |\n| bar       | qux |\n+-----+-----+-----+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_line_breaks_after_colspan_cell.txt",
    "content": "+-----+-----+------+\n| Foo | Bar | Baz  |\n+-----+-----+------+\n| foo       | baz  |\n| bar       | qux  |\n|           | quux |\n+-----+-----+------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_missing_cell_values.txt",
    "content": "+---------------+--------------------------+------------------+\n| ISBN          | Title                    |                  |\n+---------------+--------------------------+------------------+\n| 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n| 9971-5-0210-0 |                          |                  |\n| 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n| 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n+---------------+--------------------------+------------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_multiline_cells.txt",
    "content": "+---------------+----------------------------+-----------------+\n| ISBN          | Title                      | Author          |\n+---------------+----------------------------+-----------------+\n| 99921-58-10-7 | Divine                     | Dante Alighieri |\n|               | Comedy                     |                 |\n| 9971-5-0210-2 | Harry Potter               | Rowling         |\n|               | and the Chamber of Secrets | Joanne K.       |\n| 9971-5-0210-2 | Harry Potter               | Rowling         |\n|               | and the Chamber of Secrets | Joanne K.       |\n| 960-425-059-0 | The Lord of the Rings      | J. R. R.        |\n|               |                            | Tolkien         |\n+---------------+----------------------------+-----------------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_multiple_header_lines.txt",
    "content": "+------+-------+--------+\n| Main title            |\n+------+-------+--------+\n| ISBN | Title | Author |\n+------+-------+--------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_no_rows.txt",
    "content": "+------+-------+\n| ISBN | Title |\n+------+-------+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/default_row_with_multiple_cells.txt",
    "content": "+---+--+--+---+--+---+--+---+--+\n| 1       | 2    | 3    | 4    |\n+---+--+--+---+--+---+--+---+--+\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/double_box_separator.txt",
    "content": "╔═══════════════╤══════════════════════════╤══════════════════╗\n║ ISBN          │ Title                    │ Author           ║\n╠═══════════════╪══════════════════════════╪══════════════════╣\n║ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  ║\n║ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  ║\n╟───────────────┼──────────────────────────┼──────────────────╢\n║ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien ║\n║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  ║\n╚═══════════════╧══════════════════════════╧══════════════════╝\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/markdown.txt",
    "content": "| ISBN          | Title                    | Author           |\n|---------------|--------------------------|------------------|\n| 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n| 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens  |\n| 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n| 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n"
  },
  {
    "path": "src/components/console/spec/fixtures/helper/table/suggested_vertical.txt",
    "content": " ------------------------------ \n    ISBN: 99921-58-10-7         \n   Title: Divine Comedy         \n  Author: Dante Alighieri       \n   Price: 9.95                  \n ------------------------------ \n    ISBN: 9971-5-0210-0         \n   Title: A Tale of Two Cities  \n  Author: Charles Dickens       \n   Price: 139.25                \n ------------------------------ \n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/backslashes.txt",
    "content": "\nTitle ending with \\\\\n===================\n\nSection ending with \\\\\n---------------------\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/block.txt",
    "content": "\n ! \\[CAUTION\\] Lorem ipsum dolor sit amet                                                                                 \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/block_line_endings.txt",
    "content": "Lorem ipsum dolor sit amet\n \\* Lorem ipsum dolor sit amet\n \\* consectetur adipiscing elit\n\nLorem ipsum dolor sit amet\n \\* Lorem ipsum dolor sit amet\n \\* consectetur adipiscing elit\n\nLorem ipsum dolor sit amet\n Lorem ipsum dolor sit amet\n consectetur adipiscing elit\n\nLorem ipsum dolor sit amet\n\n \\/\\/ Lorem ipsum dolor sit amet                                                                                          \n \\/\\/                                                                                                                     \n \\/\\/ consectetur adipiscing elit                                                                                         \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/block_no_prefix_type.txt",
    "content": "\n \\[TEST\\] Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore  \n        magna aliqua\\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo  \n        consequat\\. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla         \n        pariatur\\. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est \n        laborum                                                                                                         \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/block_padding.txt",
    "content": "\n\\e\\[30;42m                                                                                                                        \\e\\[39;49m\n\\e\\[30;42m \\[OK\\] Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore    \\e\\[39;49m\n\\e\\[30;42m      magna aliqua\\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo    \\e\\[39;49m\n\\e\\[30;42m      consequat\\. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur\\. \\e\\[39;49m\n\\e\\[30;42m      Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum     \\e\\[39;49m\n\\e\\[30;42m                                                                                                                        \\e\\[39;49m\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/block_prefix_no_type.txt",
    "content": "\n\\$ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna  \n\\$ aliqua\\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat\\.   \n\\$ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur\\. Excepteur sint \n\\$ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum                        \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/blocks.txt",
    "content": "\n \\[WARNING\\] Warning                                                                                                      \n\n ! \\[CAUTION\\] Caution                                                                                                    \n\n \\[ERROR\\] Error                                                                                                          \n\n \\[OK\\] Success                                                                                                           \n\n ! \\[NOTE\\] Note                                                                                                          \n\n \\[INFO\\] Info                                                                                                            \n\nX \\[CUSTOM\\] Custom block                                                                                                 \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/closing_tag.txt",
    "content": "\\e\\[30;46mdo you want \\e\\[39;49m\\e\\[33msomething\\e\\[39m\\e\\[30;46m\\?\\e\\[39;49m\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/definition_list.txt",
    "content": " ---------- --------- \n  foo        bar      \n ---------- --------- \n  this is a title     \n ---------- --------- \n  foo2       bar2     \n ---------- --------- \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/emojis.txt",
    "content": "\n \\[OK\\] Lorem ipsum dolor sit amet                                                                                        \n\n \\[OK\\] Lorem ipsum dolor sit amet with one emoji 🎉                                    \n\n \\[OK\\] Lorem ipsum dolor sit amet with so many of them 👩‍🌾👩‍🌾👩‍🌾👩‍🌾👩‍🌾                                              \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/empty_buffer.txt",
    "content": " Hello\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/horizontal_table.txt",
    "content": " --- --- --- --- \n  a   1   4   7  \n  b   2   5   8  \n  c   3       9  \n  d              \n --- --- --- --- \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/long_line_block.txt",
    "content": "\nX \\[CUSTOM\\] Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et      \nX          dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea\nX          commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat    \nX          nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit    \nX          anim id est laborum                                                                                          \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/long_line_block_wrapping.txt",
    "content": "\n § \\[CUSTOM\\] Lopadotemachoselachogaleokranioleipsanodrimhypotrimmatosilphioparaomelitokatakechymenokichlepikossyphophatto\n §          peristeralektryonoptekephalliokigklopeleiolagoiosiraiobaphetraganopterygon                                  \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/long_line_comment.txt",
    "content": "\n // Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna\n // aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. \n // Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur    \n // sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum                 \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/long_line_comment_decorated.txt",
    "content": "\n \\/\\/ Árvíztűrőtükörfúrógép 🎼 Lorem ipsum dolor sit \\e\\[33m💕 amet, consectetur adipisicing elit, sed do eiusmod tempor incididu  \\e\\[39m\n \\/\\/ \\e\\[33mlabore et dolore magna aliqua\\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex\\e\\[39m\n \\/\\/ \\e\\[33mea commodo consequat\\. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla  \\e\\[39m\n \\/\\/ \\e\\[33mpariatur\\.\\e\\[39m Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est     \n \\/\\/ laborum                                                                                                             \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/multi_line_block.txt",
    "content": "\nX \\[CUSTOM\\] Custom block                                                                                                 \nX                                                                                                                       \nX          Second custom block line                                                                                     \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/nested_tag_prefix.txt",
    "content": "\n ║ \\[★\\] Árvíztűrőtükörfúrógép Lorem ipsum dolor sit \\e\\[33mamet, consectetur adipisicing elit, sed do eiusmod tempor incididunt \\e\\[39m\n ║ \\e\\[33m    ut labore et dolore magna aliqua\\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut     \\e\\[39m\n ║ \\e\\[33m    aliquip ex ea commodo consequat\\. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu \\e\\[39m\n ║ \\e\\[33m    fugiat nulla pariatur\\.\\e\\[39m Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit \n ║     anim id est laborum                                                                                              \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/non_interactive_question.txt",
    "content": "\nTitle\n=====\n\n Duis aute irure dolor in reprehenderit in voluptate velit esse\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/table.txt",
    "content": " ----- ------- \n \\e\\[32m Foo \\e\\[39m \\e\\[32m Bar   \\e\\[39m \n ----- ------- \n  Biz   Baz    \n  12    false  \n ----- ------- \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/table_horizontal.txt",
    "content": " ----- ----- ------- \n \\e\\[32m Foo \\e\\[39m  Biz   12     \n \\e\\[32m Bar \\e\\[39m  Baz   false  \n ----- ----- ------- \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/table_vertical.txt",
    "content": " ------------ \n  \u001b\\[33mFoo\u001b\\[39m: Biz    \n  \u001b\\[33mBar\u001b\\[39m: Baz    \n ------------ \n  \u001b\\[33mFoo\u001b\\[39m: 12     \n  \u001b\\[33mBar\u001b\\[39m: false  \n ------------ \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/text_block_blank_line.txt",
    "content": " \\* Lorem ipsum dolor sit amet\n \\* consectetur adipiscing elit\n\n \\[OK\\] Lorem ipsum dolor sit amet                                                                                        \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/title_block.txt",
    "content": "\nTitle\n=====\n\n \\[WARNING\\] Lorem ipsum dolor sit amet                                                                                   \n\nTitle\n=====\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/titles.txt",
    "content": "\nFirst title\n===========\n\nSecond title\n============\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/style/titles_text.txt",
    "content": "Lorem ipsum dolor sit amet\n\nFirst title\n===========\n\nLorem ipsum dolor sit amet\n\nSecond title\n============\n\nLorem ipsum dolor sit amet\n\nThird title\n===========\n\nLorem ipsum dolor sit amet\n\nFourth title\n============\n\nLorem ipsum dolor sit amet\n\n\nFifth title\n===========\n\nLorem ipsum dolor sit amet\n\n\nSixth title\n===========\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_1.txt",
    "content": "foo <info>UNKNOWN</info>\n\n<comment>Usage:</comment>\n  command [options] [arguments]\n\n<comment>Options:</comment>\n  <info>-h, --help</info>            Display help for the given command. When no command is given display help for the <info>list</info> command\n  <info>    --silent</info>          Do not output any message\n  <info>-q, --quiet</info>           Only errors are displayed. All other output is suppressed\n  <info>-V, --version</info>         Display this application version\n  <info>    --ansi|--no-ansi</info>  Force (or disable --no-ansi) ANSI output\n  <info>-n, --no-interaction</info>  Do not ask any interactive question\n  <info>-v|vv|vvv, --verbose</info>  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\n\n<comment>Available commands:</comment>\n  <info>completion</info>  Dump the shell completion script\n  <info>help</info>        Display help for a command\n  <info>list</info>        List available commands\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_2.txt",
    "content": "My Athena application <info>1.0.0</info>\n\n<comment>Usage:</comment>\n  command [options] [arguments]\n\n<comment>Options:</comment>\n  <info>-h, --help</info>            Display help for the given command. When no command is given display help for the <info>list</info> command\n  <info>    --silent</info>          Do not output any message\n  <info>-q, --quiet</info>           Only errors are displayed. All other output is suppressed\n  <info>-V, --version</info>         Display this application version\n  <info>    --ansi|--no-ansi</info>  Force (or disable --no-ansi) ANSI output\n  <info>-n, --no-interaction</info>  Do not ask any interactive question\n  <info>-v|vv|vvv, --verbose</info>  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\n\n<comment>Available commands:</comment>\n  <info>completion</info>           Dump the shell completion script\n  <info>help</info>                 Display help for a command\n  <info>list</info>                 List available commands\n <comment>descriptor</comment>\n  <info>descriptor:command1</info>  [alias1|alias2] command 1 description\n  <info>descriptor:command2</info>  command 2 description\n  <info>descriptor:command4</info>  [descriptor:alias_command4|command4:descriptor]\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_alternative_namespace.txt",
    "content": "\n                                                          \n  There are no commands defined in the 'foos' namespace\\.  \n                                                          \n  Did you mean this\\?                                      \n      foo                                                 \n                                                          \n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_filtered_namespace.txt",
    "content": "My Athena application <info>1.0.0</info>\n\n<comment>Usage:</comment>\n  command [options] [arguments]\n\n<comment>Options:</comment>\n  <info>-h, --help</info>            Display help for the given command. When no command is given display help for the <info>list</info> command\n  <info>    --silent</info>          Do not output any message\n  <info>-q, --quiet</info>           Only errors are displayed. All other output is suppressed\n  <info>-V, --version</info>         Display this application version\n  <info>    --ansi|--no-ansi</info>  Force (or disable --no-ansi) ANSI output\n  <info>-n, --no-interaction</info>  Do not ask any interactive question\n  <info>-v|vv|vvv, --verbose</info>  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\n\n<comment>Available commands for the 'command4' namespace:</comment>\n  <info>command4:descriptor</info>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_renderexception1.txt",
    "content": "\n                                 \n  Command 'foo' is not defined.  \n                                 \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_renderexception2.txt",
    "content": "\n                                      \n  The '--foo' option does not exist\\.  \n                                      \n\nlist \\[--raw\\] \\[--format FORMAT\\] \\[--short\\] \\[--\\] \\[<namespace>\\]\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_renderexception3.txt",
    "content": "\nIn \\w+.cr line \\d+:\n                                              \n  Third exception <fg=blue;bg=red>comment</>  \n                                              \n\nIn foo3.cr line \\d+:\n                                               \n  Second exception <comment>comment</comment>  \n                                               \n\nIn foo3.cr line \\d+:\n                                       \n  First exception <p>this is html</p>  \n                                       \n\nfoo3:bar\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_renderexception3_decorated.txt",
    "content": "\n\\e\\[33mIn \\w+\\.cr line \\d+:\\e\\[39m\n\\e\\[97;41m                                              \\e\\[39;49m\n\\e\\[97;41m  Third exception <fg=blue;bg=red>comment</>  \\e\\[39;49m\n\\e\\[97;41m                                              \\e\\[39;49m\n\n\\e\\[33mIn foo3\\.cr line \\d+:\\e\\[39m\n\\e\\[97;41m                                               \\e\\[39;49m\n\\e\\[97;41m  Second exception <comment>comment</comment>  \\e\\[39;49m\n\\e\\[97;41m                                               \\e\\[39;49m\n\n\\e\\[33mIn foo3\\.cr line \\d+:\\e\\[39m\n\\e\\[97;41m                                       \\e\\[39;49m\n\\e\\[97;41m  First exception <p>this is html</p>  \\e\\[39;49m\n\\e\\[97;41m                                       \\e\\[39;49m\n\n\\e\\[32mfoo3:bar\\e\\[39m\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_renderexception4.txt",
    "content": "\n                               \n  Command 'foo' is not define  \n  d.                           \n                               \n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_renderexception_doublewidth1.txt",
    "content": "\nAt spec/application_spec.cr:\\d+:\\d+ in '->'\n                    \n  エラーメッセージ    \n                    \n\nfoo\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_renderexception_escapeslines.txt",
    "content": "\nIn \\w+\\.cr line \\d+:\n                     \n  dont break here <  \n  info>!<\\/info>      \n                     \n\nfoo\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_renderexception_linebreaks.txt",
    "content": "\nIn \\w+\\.cr line \\d+:\n                                    \n  line 1 with extra spaces          \n  line 2                            \n                                    \n  line 4                            \n                                    \n\nfoo\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_renderexception_synopsis_escapeslines.txt",
    "content": "\nIn \\w+\\.cr line \\d+:\n                  \n  some exception  \n                  \n\nfoo \\[<info>\\]\n\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_run1.txt",
    "content": "foo UNKNOWN\n\nUsage:\n  command \\[options\\] \\[arguments\\]\n\nOptions:\n  -h, --help            Display help for the given command\\. When no command is given display help for the list command\n      --silent          Do not output any message\n  -q, --quiet           Only errors are displayed. All other output is suppressed\n  -V, --version         Display this application version\n      --ansi\\|--no-ansi  Force \\(or disable --no-ansi\\) ANSI output\n  -n, --no-interaction  Do not ask any interactive question\n  -v\\|vv\\|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\n\nAvailable commands:\n  completion  Dump the shell completion script\n  help        Display help for a command\n  list        List available commands\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_run2.txt",
    "content": "Description:\n  List available commands\n\nUsage:\n  list \\[options\\] \\[--\\] \\[<namespace>\\]\n\nArguments:\n  namespace             Only list commands in this namespace\n\nOptions:\n      --raw             To output raw command list\n      --format=FORMAT   The output format \\(txt\\) \\[default: \"txt\"\\]\n      --short           To skip describing command's arguments\n  -h, --help            Display help for the given command. When no command is given display help for the list command\n      --silent          Do not output any message\n  -q, --quiet           Only errors are displayed. All other output is suppressed\n  -V, --version         Display this application version\n      --ansi\\|--no-ansi  Force \\(or disable --no-ansi\\) ANSI output\n  -n, --no-interaction  Do not ask any interactive question\n  -v\\|vv\\|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\n\nHelp:\n  The list command lists all commands:\n  \n    console list\n  \n  You can also display the commands for a specific namespace:\n  \n    console list test\n  \n  It's also possible to get raw list of commands \\(useful for embedding command runner\\):\n  \n    console list --raw\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_run3.txt",
    "content": "Description:\n  List available commands\n\nUsage:\n  list \\[options\\] \\[--\\] \\[<namespace>\\]\n\nArguments:\n  namespace             Only list commands in this namespace\n\nOptions:\n      --raw             To output raw command list\n      --format=FORMAT   The output format \\(txt\\) \\[default: \"txt\"\\]\n      --short           To skip describing command's arguments\n  -h, --help            Display help for the given command\\. When no command is given display help for the list command\n      --silent          Do not output any message\n  -q, --quiet           Only errors are displayed. All other output is suppressed\n  -V, --version         Display this application version\n      --ansi\\|--no-ansi  Force \\(or disable --no-ansi\\) ANSI output\n  -n, --no-interaction  Do not ask any interactive question\n  -v\\|vv\\|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\n\nHelp:\n  The list command lists all commands:\n  \n    console list\n  \n  You can also display the commands for a specific namespace:\n  \n    console list test\n  \n  It's also possible to get raw list of commands \\(useful for embedding command runner\\):\n  \n    console list --raw\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_run4.txt",
    "content": "foo UNKNOWN\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/application_run5.txt",
    "content": "Description:\n  Display help for a command\n\nUsage:\n  help \\[options\\] \\[--\\] \\[<command_name>\\]\n\nArguments:\n  command_name          The command name \\[default: \"help\"\\]\n\nOptions:\n      --format=FORMAT   The output format \\(txt\\) \\[default: \"txt\"\\]\n      --raw             To output raw command help\n  -h, --help            Display help for the given command\\. When no command is given display help for the list command\n      --silent          Do not output any message\n  -q, --quiet           Only errors are displayed. All other output is suppressed\n  -V, --version         Display this application version\n      --ansi\\|--no-ansi  Force \\(or disable --no-ansi\\) ANSI output\n  -n, --no-interaction  Do not ask any interactive question\n  -v\\|vv\\|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\n\nHelp:\n  The help command displays help for a given command:\n  \n    console help list\n  \n  To display the list of available commands, please use the list command\\.\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/command_1.txt",
    "content": "<comment>Description:</comment>\n  command 1 description\n\n<comment>Usage:</comment>\n  descriptor:command1\n  alias1\n  alias2\n\n<comment>Help:</comment>\n  command 1 help\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/command_2.txt",
    "content": "<comment>Description:</comment>\n  command 2 description\n\n<comment>Usage:</comment>\n  descriptor:command2 [options] [--] \\<argument_name>\n  descriptor:command2 -o|--option_name \\<argument_name>\n  descriptor:command2 \\<argument_name>\n\n<comment>Arguments:</comment>\n  <info>argument_name</info>      \n\n<comment>Options:</comment>\n  <info>-o, --option_name</info>  \n\n<comment>Help:</comment>\n  command 2 help\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_argument_1.txt",
    "content": "  <info>argument_name</info>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_argument_2.txt",
    "content": "  <info>argument_name</info>  argument description\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_argument_3.txt",
    "content": "  <info>argument_name</info>  argument description<comment> [default: \"default_value\"]</comment>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_argument_4.txt",
    "content": "  <info>argument_name</info>  multiline\n                 argument description\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_argument_with_style.txt",
    "content": "  <info>argument_name</info>  argument description<comment> [default: \"\\<comment>style\\</>\"]</comment>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_definition_1.txt",
    "content": ""
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_definition_2.txt",
    "content": "<comment>Arguments:</comment>\n  <info>argument_name</info>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_definition_3.txt",
    "content": "<comment>Options:</comment>\n  <info>-o, --option_name</info>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_definition_4.txt",
    "content": "<comment>Arguments:</comment>\n  <info>argument_name</info>      \n\n<comment>Options:</comment>\n  <info>-o, --option_name</info>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_option_1.txt",
    "content": "  <info>-o, --option_name</info>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_option_2.txt",
    "content": "  <info>-o, --option_name[=OPTION_NAME]</info>  option description<comment> [default: \"default_value\"]</comment>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_option_3.txt",
    "content": "  <info>-o, --option_name=OPTION_NAME</info>  option description\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_option_4.txt",
    "content": "  <info>-o, --option_name[=OPTION_NAME]</info>  option description<comment> (multiple values allowed)</comment>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_option_5.txt",
    "content": "  <info>-o, --option_name=OPTION_NAME</info>  multiline\n                                 option description\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_option_6.txt",
    "content": "  <info>-o|O, --option_name=OPTION_NAME</info>  option with multiple shortcuts\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_option_with_style.txt",
    "content": "  <info>-o, --option_name=OPTION_NAME</info>  option description<comment> [default: \"\\<comment>style\\</>\"]</comment>\n"
  },
  {
    "path": "src/components/console/spec/fixtures/text/input_option_with_style_array.txt",
    "content": "  <info>-o, --option_name=OPTION_NAME</info>  option description<comment> [default: [\"\\<comment>Hello\\</comment>\",\"\\<info>world\\</info>\"]]</comment><comment> (multiple values allowed)</comment>\n"
  },
  {
    "path": "src/components/console/spec/formatter/null_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct NullFormatterTest < ASPEC::TestCase\n  def test_has_style : Nil\n    ACON::Formatter::Null.new.has_style?(\"error\").should be_false\n  end\n\n  def test_style : Nil\n    ACON::Formatter::Null.new.style(\"error\").should be_a ACON::Formatter::NullStyle\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/formatter/null_style_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct NullStyleTest < ASPEC::TestCase\n  def test_apply : Nil\n    ACON::Formatter::NullStyle.new.apply(\"foo\").should eq \"foo\"\n  end\n\n  def test_set_foreground : Nil\n    style = ACON::Formatter::NullStyle.new\n    style.foreground = :red\n    style.apply(\"foo\").should eq \"foo\"\n  end\n\n  def test_set_background : Nil\n    style = ACON::Formatter::NullStyle.new\n    style.background = :red\n    style.apply(\"foo\").should eq \"foo\"\n  end\n\n  def test_options : Nil\n    style = ACON::Formatter::NullStyle.new\n\n    style.add_option :bold\n    style.apply(\"foo\").should eq \"foo\"\n\n    style.remove_option :bold\n    style.apply(\"foo\").should eq \"foo\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/formatter/output_formatter_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct OutputFormatterTest < ASPEC::TestCase\n  @formatter : ACON::Formatter::Output\n\n  def initialize\n    @formatter = ACON::Formatter::Output.new true\n  end\n\n  def test_format_empty_tag : Nil\n    @formatter.format(\"foo<>bar\").should eq \"foo<>bar\"\n  end\n\n  def test_format_lg_char_escaping : Nil\n    @formatter.format(\"foo\\\\<bar\").should eq \"foo<bar\"\n    @formatter.format(\"foo << bar\").should eq \"foo << bar\"\n    @formatter.format(\"foo << bar \\\\\").should eq \"foo << bar \\\\\"\n    @formatter.format(\"foo << <info>bar \\\\ baz</info> \\\\\").should eq \"foo << \\e[32mbar \\\\ baz\\e[39m \\\\\"\n    @formatter.format(\"\\\\<info>some info\\\\</info>\").should eq \"<info>some info</info>\"\n    ACON::Formatter::Output.escape(\"<info>some info</info>\").should eq \"\\\\<info>some info\\\\</info>\"\n\n    @formatter.format(\"<comment>Some\\\\Path\\\\ToFile does work very well!</comment>\").should eq \"\\e[33mSome\\\\Path\\\\ToFile does work very well!\\e[39m\"\n  end\n\n  def test_format_built_in_styles : Nil\n    @formatter.has_style?(\"error\").should be_true\n    @formatter.has_style?(\"info\").should be_true\n    @formatter.has_style?(\"comment\").should be_true\n    @formatter.has_style?(\"question\").should be_true\n\n    @formatter.format(\"<error>some error</error>\").should eq \"\\e[97;41msome error\\e[39;49m\"\n    @formatter.format(\"<info>some info</info>\").should eq \"\\e[32msome info\\e[39m\"\n    @formatter.format(\"<comment>some comment</comment>\").should eq \"\\e[33msome comment\\e[39m\"\n    @formatter.format(\"<question>some question</question>\").should eq \"\\e[30;46msome question\\e[39;49m\"\n  end\n\n  def test_format_nested_styles : Nil\n    @formatter.format(\"<error>some <info>some info</info> error</error>\").should eq \"\\e[97;41msome \\e[39;49m\\e[32msome info\\e[39m\\e[97;41m error\\e[39;49m\"\n  end\n\n  def test_format_deeply_nested_styles : Nil\n    @formatter.format(\"<error>error<info>info<comment>comment</info>error</error>\").should eq \"\\e[97;41merror\\e[39;49m\\e[32minfo\\e[39m\\e[33mcomment\\e[39m\\e[97;41merror\\e[39;49m\"\n  end\n\n  def test_format_adjacent_styles : Nil\n    @formatter.format(\"<error>some error</error><info>some info</info>\").should eq \"\\e[97;41msome error\\e[39;49m\\e[32msome info\\e[39m\"\n  end\n\n  def test_format_adjacent_styles_not_greedy : Nil\n    @formatter.format(\"(<info>>=2.0,<2.3</info>)\").should eq \"(\\e[32m>=2.0,<2.3\\e[39m)\"\n  end\n\n  def test_format_style_escaping : Nil\n    @formatter.format(%((<info>#{@formatter.class.escape \"z>=2.0,<\\\\<<a2.3\\\\\"}</info>))).should eq \"(\\e[32mz>=2.0,<<<a2.3\\\\\\e[39m)\"\n    @formatter.format(%(<info>#{@formatter.class.escape \"<error>some error</error>\"}</info>)).should eq \"\\e[32m<error>some error</error>\\e[39m\"\n  end\n\n  def test_format_custom_style : Nil\n    style = ACON::Formatter::OutputStyle.new :blue, :white\n    @formatter.set_style \"test\", style\n\n    @formatter.style(\"test\").should eq style\n    @formatter.style(\"info\").should_not eq style\n\n    style = ACON::Formatter::OutputStyle.new :blue, :white\n    @formatter.set_style \"b\", style\n\n    @formatter.format(\"<test>some message</test><b>custom</b>\").should eq \"\\e[34;107msome message\\e[39;49m\\e[34;107mcustom\\e[39;49m\"\n    # TODO: Also assert it works when nested.\n  end\n\n  def test_format_redefine_style : Nil\n    style = ACON::Formatter::OutputStyle.new :blue, :white\n    @formatter.set_style \"info\", style\n\n    @formatter.format(\"<info>some custom message</info>\").should eq \"\\e[34;107msome custom message\\e[39;49m\"\n  end\n\n  def test_format_inline_style : Nil\n    @formatter.format(\"<fg=blue;bg=red>some text</>\").should eq \"\\e[34;41msome text\\e[39;49m\"\n    @formatter.format(\"<fg=blue;bg=red>some text</fg=blue;bg=red>\").should eq \"\\e[34;41msome text\\e[39;49m\"\n  end\n\n  @[DataProvider(\"inline_style_options_provider\")]\n  def test_format_inline_style_options(tag : String, expected : String?, input : String?, truecolor : Bool) : Nil\n    if truecolor && \"truecolor\" != ENV[\"COLORTERM\"]?\n      pending! \"The terminal does not support true colors.\"\n    end\n\n    style_string = tag.strip \"<>\"\n\n    style = @formatter.create_style_from_string style_string\n\n    if expected.nil?\n      style.should be_nil\n      expected = \"#{tag}#{input}</#{style_string}>\"\n      @formatter.format(expected).should eq expected\n    else\n      style.should be_a ACON::Formatter::OutputStyle\n      @formatter.format(\"#{tag}#{input}</>\").should eq expected\n      @formatter.format(\"#{tag}#{input}</#{style_string}>\").should eq expected\n    end\n  end\n\n  def inline_style_options_provider : Tuple\n    {\n      {\"<unknown=_unknown_>\", nil, nil, false},\n      {\"<unknown=_unknown_;a=1;b>\", nil, nil, false},\n      {\"<fg=green;>\", \"\\e[32m[test]\\e[39m\", \"[test]\", false},\n      {\"<fg=green;bg=blue;>\", \"\\e[32;44ma\\e[39;49m\", \"a\", false},\n      {\"<fg=green;options=bold>\", \"\\e[32;1mb\\e[39;22m\", \"b\", false},\n      {\"<fg=green;options=reverse;>\", \"\\e[32;7m<a>\\e[39;27m\", \"<a>\", false},\n      {\"<fg=green;options=bold,underline>\", \"\\e[32;1;4mz\\e[39;22;24m\", \"z\", false},\n      {\"<fg=green;options=bold,underline,reverse;>\", \"\\e[32;1;4;7md\\e[39;22;24;27m\", \"d\", false},\n      {\"<fg=#00ff00;bg=#0000ff>\", \"\\e[38;2;0;255;0;48;2;0;0;255m[test]\\e[39;49m\", \"[test]\", true},\n    }\n  end\n\n  def test_format_non_style_tag : Nil\n    @formatter\n      .format(\"<info>some <tag> <setting=value> styled <p>single-char tag</p></info>\")\n      .should eq \"\\e[32msome \\e[39m\\e[32m<tag>\\e[39m\\e[32m \\e[39m\\e[32m<setting=value>\\e[39m\\e[32m styled \\e[39m\\e[32m<p>\\e[39m\\e[32msingle-char tag\\e[39m\\e[32m</p>\\e[39m\"\n  end\n\n  def test_format_long_string : Nil\n    long = \"\\\\\" * 14_000\n    @formatter.format(\"<error>some error</error>#{long}\").should eq \"\\e[97;41msome error\\e[39;49m#{long}\"\n  end\n\n  def test_has_style : Nil\n    @formatter = ACON::Formatter::Output.new\n\n    @formatter.has_style?(\"error\").should be_true\n    @formatter.has_style?(\"info\").should be_true\n    @formatter.has_style?(\"comment\").should be_true\n    @formatter.has_style?(\"question\").should be_true\n  end\n\n  @[DataProvider(\"decorated_and_non_decorated_output\")]\n  def test_format_not_decorated(input : String, expected_non_decorated_output : String, expected_decorated_output : String, term_emulator : String) : Nil\n    previous_term_emulator = ENV[\"TERMINAL_EMULATOR\"]?\n    ENV[\"TERMINAL_EMULATOR\"] = term_emulator\n\n    begin\n      ACON::Formatter::Output.new(true).format(input).should eq expected_decorated_output\n      ACON::Formatter::Output.new(false).format(input).should eq expected_non_decorated_output\n    ensure\n      if previous_term_emulator\n        ENV[\"TERMINAL_EMULATOR\"] = previous_term_emulator\n      else\n        ENV.delete \"TERMINAL_EMULATOR\"\n      end\n    end\n  end\n\n  def decorated_and_non_decorated_output : Tuple\n    {\n      {\"<error>some error</error>\", \"some error\", \"\\e[97;41msome error\\e[39;49m\", \"foo\"},\n      {\"<info>some info</info>\", \"some info\", \"\\e[32msome info\\e[39m\", \"foo\"},\n      {\"<comment>some comment</comment>\", \"some comment\", \"\\e[33msome comment\\e[39m\", \"foo\"},\n      {\"<question>some question</question>\", \"some question\", \"\\e[30;46msome question\\e[39;49m\", \"foo\"},\n      {\"<fg=red>some text with inline style</>\", \"some text with inline style\", \"\\e[31msome text with inline style\\e[39m\", \"foo\"},\n      {\"<href=idea://open/?file=/path/SomeFile.php&line=12>some URL</>\", \"some URL\", \"\\e]8;;idea://open/?file=/path/SomeFile.php&line=12\\e\\\\some URL\\e]8;;\\e\\\\\", \"foo\"},\n      {\"<href=idea://open/?file=/path/SomeFile.php&line=12>some URL</>\", \"some URL\", \"some URL\", \"JetBrains-JediTerm\"},\n    }\n  end\n\n  def test_format_with_line_breaks : Nil\n    @formatter.format(\"<info>\\nsome text</info>\").should eq \"\\e[32m\\nsome text\\e[39m\"\n    @formatter.format(\"<info>some text\\n</info>\").should eq \"\\e[32msome text\\n\\e[39m\"\n    @formatter.format(\"<info>\\nsome text\\n</info>\").should eq \"\\e[32m\\nsome text\\n\\e[39m\"\n    @formatter.format(\"<info>\\nsome text\\nmore text\\n</info>\").should eq \"\\e[32m\\nsome text\\nmore text\\n\\e[39m\"\n  end\n\n  def test_format_and_wrap : Nil\n    @formatter.format_and_wrap(\"ooo<error>bar</error> bbz\", 2).should eq \"oo\\no\\e[97;41mb\\e[39;49m\\n\\e[97;41mar\\e[39;49m\\nbb\\nz\"\n    @formatter.format_and_wrap(\"pre <error>foo bar baz</error> post\", 2).should eq \"pr\\ne \\e[97;41m\\e[39;49m\\n\\e[97;41mfo\\e[39;49m\\n\\e[97;41mo \\e[39;49m\\n\\e[97;41mba\\e[39;49m\\n\\e[97;41mr \\e[39;49m\\n\\e[97;41mba\\e[39;49m\\n\\e[97;41mz\\e[39;49m \\npo\\nst\"\n    @formatter.format_and_wrap(\"pre <error>foo bar baz</error> post\", 3).should eq \"pre\\e[97;41m\\e[39;49m\\n\\e[97;41mfoo\\e[39;49m\\n\\e[97;41mbar\\e[39;49m\\n\\e[97;41mbaz\\e[39;49m\\npos\\nt\"\n    @formatter.format_and_wrap(\"pre <error>foo bar baz</error> post\", 4).should eq \"pre \\e[97;41m\\e[39;49m\\n\\e[97;41mfoo \\e[39;49m\\n\\e[97;41mbar \\e[39;49m\\n\\e[97;41mbaz\\e[39;49m \\npost\"\n    @formatter.format_and_wrap(\"pre <error>foo bbr baz</error> post\", 5).should eq \"pre \\e[97;41mf\\e[39;49m\\n\\e[97;41moo bb\\e[39;49m\\n\\e[97;41mr baz\\e[39;49m\\npost\"\n\n    @formatter.format_and_wrap(\"Lorem <error>ipsum</error> dolor <info>sit</info> amet\", 4).should eq \"Lore\\nm \\e[97;41mip\\e[39;49m\\n\\e[97;41msum\\e[39;49m \\ndolo\\nr \\e[32msi\\e[39m\\n\\e[32mt\\e[39m am\\net\"\n    @formatter.format_and_wrap(\"Lorem <error>ipsum</error> dolor <info>sit</info> amet\", 8).should eq \"Lorem \\e[97;41mip\\e[39;49m\\n\\e[97;41msum\\e[39;49m dolo\\nr \\e[32msit\\e[39m am\\net\"\n    @formatter.format_and_wrap(\"Lorem <error>ipsum</error> dolor <info>sit</info>, <error>amet</error> et <info>laudantium</info> architecto\", 18).should eq \"Lorem \\e[97;41mipsum\\e[39;49m dolor \\e[32m\\e[39m\\n\\e[32msit\\e[39m, \\e[97;41mamet\\e[39;49m et \\e[32mlauda\\e[39m\\n\\e[32mntium\\e[39m architecto\"\n  end\n\n  def test_format_and_wrap_non_decorated : Nil\n    @formatter = ACON::Formatter::Output.new\n\n    @formatter.format_and_wrap(\"ooo<error>bar</error> baz\", 2).should eq \"oo\\nob\\nar\\nba\\nz\"\n    @formatter.format_and_wrap(\"pre <error>foo bbr baz</error> post\", 2).should eq \"pr\\ne \\nfo\\no \\nbb\\nr \\nba\\nz \\npo\\nst\"\n    @formatter.format_and_wrap(\"pre <error>foo bar baz</error> post\", 3).should eq \"pre\\nfoo\\nbar\\nbaz\\npos\\nt\"\n    @formatter.format_and_wrap(\"pre <error>foo bar baz</error> post\", 4).should eq \"pre \\nfoo \\nbar \\nbaz \\npost\"\n    @formatter.format_and_wrap(\"pre <error>foo bbr baz</error> post\", 5).should eq \"pre f\\noo bb\\nr baz\\npost\"\n\n    @formatter.format_and_wrap(nil, 5).should eq \"\"\n\n    @formatter.format_and_wrap(\"And Then There Were None\", 15).should eq \"And Then There \\nWere None\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/formatter/output_formatter_style_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ACON::Formatter::OutputStyle do\n  it \".new\" do\n    ACON::Formatter::OutputStyle.new(:green, :black, Colorize::Mode[:bold, :underline])\n      .apply(\"foo\").should eq \"\\e[32;40;1;4mfoo\\e[39;49;22;24m\"\n\n    ACON::Formatter::OutputStyle.new(:red, options: Colorize::Mode::Blink)\n      .apply(\"foo\").should eq \"\\e[31;5mfoo\\e[39;25m\"\n\n    ACON::Formatter::OutputStyle.new(background: :white)\n      .apply(\"foo\").should eq \"\\e[107mfoo\\e[49m\"\n\n    ACON::Formatter::OutputStyle.new(\"red\", \"#000000\", Colorize::Mode[:bold, :underline])\n      .apply(\"foo\").should eq \"\\e[31;48;2;0;0;0;1;4mfoo\\e[39;49;22;24m\"\n  end\n\n  describe \"foreground=\" do\n    it \"with ANSI color\" do\n      style = ACON::Formatter::OutputStyle.new\n      style.foreground = :black\n      style.apply(\"foo\").should eq \"\\e[30mfoo\\e[39m\"\n    end\n\n    it \"with default value\" do\n      style = ACON::Formatter::OutputStyle.new\n      style.foreground = :default\n      style.apply(\"foo\").should eq \"foo\"\n    end\n\n    it \"with HEX RGB value\" do\n      style = ACON::Formatter::OutputStyle.new\n      style.foreground = \"#aedfff\"\n      style.apply(\"foo\").should eq \"\\e[38;2;174;223;255mfoo\\e[39m\"\n    end\n\n    it \"with invalid color\" do\n      style = ACON::Formatter::OutputStyle.new\n\n      expect_raises ArgumentError do\n        style.foreground = \"invalid\"\n      end\n    end\n  end\n\n  describe \"background=\" do\n    it \"with ANSI color\" do\n      style = ACON::Formatter::OutputStyle.new\n      style.background = :black\n      style.apply(\"foo\").should eq \"\\e[40mfoo\\e[49m\"\n    end\n\n    it \"with default value\" do\n      style = ACON::Formatter::OutputStyle.new\n      style.background = :default\n      style.apply(\"foo\").should eq \"foo\"\n    end\n\n    it \"with HEX RGB value\" do\n      style = ACON::Formatter::OutputStyle.new\n      style.background = \"#aedfff\"\n      style.apply(\"foo\").should eq \"\\e[48;2;174;223;255mfoo\\e[49m\"\n    end\n\n    it \"with invalid color\" do\n      style = ACON::Formatter::OutputStyle.new\n\n      expect_raises ArgumentError do\n        style.background = \"invalid\"\n      end\n    end\n  end\n\n  it \"add/remove_option\" do\n    style = ACON::Formatter::OutputStyle.new\n\n    style.add_option \"reverse\"\n    style.add_option \"hidden\"\n    style.apply(\"foo\").should eq \"\\e[7;8mfoo\\e[27;28m\"\n\n    style.add_option \"bold\"\n    style.apply(\"foo\").should eq \"\\e[1;7;8mfoo\\e[22;27;28m\"\n\n    style.remove_option \"reverse\"\n    style.apply(\"foo\").should eq \"\\e[1;8mfoo\\e[22;28m\"\n\n    style.add_option \"bold\"\n    style.apply(\"foo\").should eq \"\\e[1;8mfoo\\e[22;28m\"\n\n    style.options = Colorize::Mode::Bold\n    style.apply(\"foo\").should eq \"\\e[1mfoo\\e[22m\"\n  end\n\n  it \"href\" do\n    previous_term_emulator = ENV[\"TERMINAL_EMULATOR\"]?\n    ENV.delete \"TERMINAL_EMULATOR\"\n\n    style = ACON::Formatter::OutputStyle.new\n\n    begin\n      style.href = \"idea://open/?file=/path/SomeFile.php&line=12\"\n      style.apply(\"some URL\").should eq \"\\e]8;;idea://open/?file=/path/SomeFile.php&line=12\\e\\\\some URL\\e]8;;\\e\\\\\"\n    ensure\n      if previous_term_emulator\n        ENV[\"TERMINAL_EMULATOR\"] = previous_term_emulator\n      else\n        ENV.delete \"TERMINAL_EMULATOR\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/formatter/output_formatter_style_stack_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ACON::Formatter::OutputStyleStack do\n  it \"#<<\" do\n    stack = ACON::Formatter::OutputStyleStack.new\n    stack << ACON::Formatter::OutputStyle.new :white, :black\n    stack << (s2 = ACON::Formatter::OutputStyle.new :yellow, :blue)\n\n    stack.current.should eq s2\n\n    stack << (s3 = ACON::Formatter::OutputStyle.new :green, :red)\n\n    stack.current.should eq s3\n  end\n\n  describe \"#pop\" do\n    it \"returns the oldest style\" do\n      stack = ACON::Formatter::OutputStyleStack.new\n      stack << (s1 = ACON::Formatter::OutputStyle.new :white, :black)\n      stack << (s2 = ACON::Formatter::OutputStyle.new :yellow, :blue)\n\n      stack.pop.should eq s2\n      stack.pop.should eq s1\n    end\n\n    it \"returns the default style if empty\" do\n      stack = ACON::Formatter::OutputStyleStack.new\n      style = ACON::Formatter::OutputStyle.new\n\n      stack.pop.should eq style\n    end\n\n    it \"allows popping a specific style\" do\n      stack = ACON::Formatter::OutputStyleStack.new\n      stack << (s1 = ACON::Formatter::OutputStyle.new :white, :black)\n      stack << (s2 = ACON::Formatter::OutputStyle.new :yellow, :blue)\n      stack << ACON::Formatter::OutputStyle.new :green, :red\n\n      stack.pop(s2).should eq s2\n      stack.pop.should eq s1\n    end\n\n    it \"nested styles\" do\n      stack = ACON::Formatter::OutputStyleStack.new\n      stack << (s1 = ACON::Formatter::OutputStyle.new :white, :red)\n      stack << (s2 = ACON::Formatter::OutputStyle.new :green, :default)\n\n      stack.pop(s2).should eq s2\n      stack.pop(s1).should eq s1\n    end\n\n    it \"invalid pop\" do\n      stack = ACON::Formatter::OutputStyleStack.new\n      stack << ACON::Formatter::OutputStyle.new :white, :black\n\n      expect_raises ACON::Exception::InvalidArgument, \"Provided style is not present in the stack.\" do\n        stack.pop ACON::Formatter::OutputStyle.new :yellow, :blue\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/abstract_question_helper_test_case.cr",
    "content": "require \"../spec_helper\"\n\nabstract struct AbstractQuestionHelperTest < ASPEC::TestCase\n  def initialize\n    @helper_set = ACON::Helper::HelperSet.new ACON::Helper::Formatter.new\n\n    @output = ACON::Output::IO.new IO::Memory.new, decorated: false\n  end\n\n  protected def with_input(data : String, interactive : Bool = true, & : ACON::Input::Interface -> Nil) : Nil\n    input_stream = IO::Memory.new data\n    input = ACON::Input::Hash.new\n    input.stream = input_stream\n    input.interactive = interactive\n\n    yield input\n  end\n\n  protected def assert_output_contains(string : String, normalize : Bool = false) : Nil\n    stream = @output.io\n    stream.rewind\n\n    output = stream.to_s\n\n    if normalize\n      output = output.gsub EOL, \"\\n\"\n    end\n\n    output.should contain string\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/athena_question_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./abstract_question_helper_test_case\"\n\nstruct AthenaQuestionTest < AbstractQuestionHelperTest\n  @helper : ACON::Helper::Question\n\n  def initialize\n    @helper = ACON::Helper::AthenaQuestion.new\n\n    super\n  end\n\n  def test_ask_choice_question : Nil\n    heroes = [\"Superman\", \"Batman\", \"Spiderman\"]\n    self.with_input \"\\n1\\n  1  \\nGeorge\\n1\\nGeorge\" do |input|\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes, 2\n      question.max_attempts = 1\n\n      # First answer is empty, so should use default\n      @helper.ask(input, @output, question).should eq \"Spiderman\"\n      self.assert_output_contains \"Who is your favorite superhero? [Spiderman]\"\n\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes\n      question.max_attempts = 1\n\n      @helper.ask(input, @output, question).should eq \"Batman\"\n      @helper.ask(input, @output, question).should eq \"Batman\"\n\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes\n      question.error_message = \"Input '%s' is not a superhero!\"\n      question.max_attempts = 2\n\n      @helper.ask(input, @output, question).should eq \"Batman\"\n      self.assert_output_contains \"Input 'George' is not a superhero!\"\n\n      begin\n        question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes, 1\n        question.max_attempts = 1\n        @helper.ask input, @output, question\n      rescue ex : ACON::Exception::InvalidArgument\n        ex.message.should eq \"Value 'George' is invalid.\"\n      end\n    end\n  end\n\n  def test_ask_multiple_choice : Nil\n    heroes = [\"Superman\", \"Batman\", \"Spiderman\"]\n\n    self.with_input \"1\\n0,2\\n 0 , 2  \\n\\n\\n\" do |input|\n      question = ACON::Question::MultipleChoice.new \"Who is your favorite superhero?\", heroes\n      question.max_attempts = 1\n\n      @helper.ask(input, @output, question).should eq [\"Batman\"]\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Spiderman\"]\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Spiderman\"]\n\n      question = ACON::Question::MultipleChoice.new \"Who is your favorite superhero?\", heroes, \"0,1\"\n      question.max_attempts = 1\n\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Batman\"]\n      self.assert_output_contains \"Who is your favorite superhero? [Superman, Batman]\"\n\n      question = ACON::Question::MultipleChoice.new \"Who is your favorite superhero?\", heroes, \" 0 , 1 \"\n      question.max_attempts = 1\n\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Batman\"]\n      self.assert_output_contains \"Who is your favorite superhero? [Superman, Batman]\"\n    end\n  end\n\n  def test_ask_choice_with_choice_value_as_default : Nil\n    question = ACON::Question::Choice.new \"Who is your favorite superhero?\", [\"Superman\", \"Batman\", \"Spiderman\"], \"Batman\"\n    question.max_attempts = 1\n\n    self.with_input \"Batman\\n\" do |input|\n      @helper.ask(input, @output, question).should eq \"Batman\"\n    end\n\n    self.assert_output_contains \"Who is your favorite superhero? [Batman]\"\n  end\n\n  def test_ask_returns_nil_if_validator_allows_it : Nil\n    question = ACON::Question(String?).new \"Who is your favorite superhero?\", nil\n    question.validator do |value|\n      value\n    end\n\n    self.with_input \"\\n\" do |input|\n      @helper.ask(input, @output, question).should be_nil\n    end\n  end\n\n  def test_ask_escapes_default_value : Nil\n    self.with_input \"\\\\\" do |input|\n      question = ACON::Question.new \"Can I have a backslash?\", \"\\\\\"\n\n      @helper.ask input, @output, question\n      self.assert_output_contains %q(Can I have a backslash? [\\])\n    end\n  end\n\n  def test_ask_format_and_escape_label : Nil\n    question = ACON::Question.new %q(Do you want to use Foo\\Bar <comment>or</comment> Foo\\Baz\\?), \"Foo\\\\Baz\"\n\n    self.with_input \"Foo\\\\Bar\" do |input|\n      @helper.ask input, @output, question\n    end\n\n    self.assert_output_contains %q( Do you want to use Foo\\Bar or Foo\\Baz\\? [Foo\\Baz]:)\n  end\n\n  def test_ask_label_trailing_backslash : Nil\n    question = ACON::Question(String?).new \"Question with a trailing \\\\\", nil\n\n    self.with_input \"sure\" do |input|\n      @helper.ask input, @output, question\n    end\n\n    self.assert_output_contains \"Question with a trailing \\\\\"\n  end\n\n  def test_ask_raises_on_missing_input : Nil\n    self.with_input \"\" do |input|\n      question = ACON::Question(String?).new \"What's your name?\", nil\n\n      expect_raises ACON::Exception::MissingInput, \"Aborted.\" do\n        @helper.ask input, @output, question\n      end\n    end\n  end\n\n  def test_ask_choice_question_padding : Nil\n    question = ACON::Question::Choice.new \"qqq\", {\"foo\" => \"foo\", \"żółw\" => \"bar\", \"łabądź\" => \"baz\"}\n    self.with_input \"foo\\n\" do |input|\n      @helper.ask input, @output, question\n    end\n\n    self.assert_output_contains <<-OUT, true\n     qqq:\n      [foo   ] foo\n      [żółw  ] bar\n      [łabądź] baz\n     >\n    OUT\n  end\n\n  def test_ask_choice_question_custom_prompt : Nil\n    question = ACON::Question::Choice.new \"qqq\", {\"foo\"}\n    question.prompt = \" >ccc> \"\n\n    self.with_input \"foo\\n\" do |input|\n      @helper.ask input, @output, question\n    end\n\n    self.assert_output_contains <<-OUT, true\n     qqq:\n      [0] foo\n     >ccc>\n    OUT\n  end\n\n  def test_ask_multiline_question_includes_help_text : Nil\n    expected = \"Write an essay (press Ctrl+D to continue)\"\n\n    # TODO: Update expected message on windows\n    # expected = \"Write an essay (press Ctrl+Z then Enter to continue)\"\n\n    question = ACON::Question(String?).new \"Write an essay\", nil\n    question.multi_line = true\n\n    self.with_input \"\\\\\" do |input|\n      @helper.ask input, @output, question\n    end\n\n    self.assert_output_contains expected\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/formatter_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def normalize(input : String) : String\n  input.gsub EOL, \"\\n\"\nend\n\ndescribe ACON::Helper::Formatter do\n  it \"#format_section\" do\n    ACON::Helper::Formatter.new.format_section(\"cli\", \"some text to display\").should eq \"<info>[cli]</info> some text to display\"\n  end\n\n  describe \"#format_block\" do\n    it \"formats\" do\n      formatter = ACON::Helper::Formatter.new\n\n      formatter.format_block(\"Some text to display\", \"error\").should eq \"<error> Some text to display </error>\"\n      formatter.format_block({\"Some text to display\", \"foo bar\"}, \"error\").should eq \"<error> Some text to display </error>\\n<error> foo bar              </error>\"\n      formatter.format_block(\"Some text to display\", \"error\", true).should eq normalize <<-BLOCK\n      <error>                        </error>\n      <error>  Some text to display  </error>\n      <error>                        </error>\n      BLOCK\n    end\n\n    it \"formats with diacritic letters\" do\n      formatter = ACON::Helper::Formatter.new\n\n      formatter.format_block(\"Du texte à afficher\", \"error\", true).should eq normalize <<-BLOCK\n      <error>                       </error>\n      <error>  Du texte à afficher  </error>\n      <error>                       </error>\n      BLOCK\n    end\n\n    pending \"formats with double with characters\" do\n    end\n\n    it \"escapes < within the block\" do\n      ACON::Helper::Formatter.new.format_block(\"<info>some info</info>\", \"error\", true).should eq normalize <<-BLOCK\n      <error>                            </error>\n      <error>  \\\\<info>some info\\\\</info>    </error>\n      <error>                            </error>\n      BLOCK\n    end\n  end\n\n  describe \"#truncate\" do\n    it \"with shorter length than message with suffix\" do\n      formatter = ACON::Helper::Formatter.new\n      message = \"testing wrapping\"\n\n      formatter.truncate(message, 4).should eq \"test...\"\n      formatter.truncate(message, 14).should eq \"testing wrappi...\"\n      formatter.truncate(message, 16).should eq \"testing wrapping...\"\n      formatter.truncate(\"zażółć gęślą jaźń\", 12).should eq \"zażółć gęślą...\"\n    end\n\n    it \"with custom suffix\" do\n      ACON::Helper::Formatter.new.truncate(\"testing truncate\", 4, \"!\").should eq \"test!\"\n    end\n\n    it \"with longer length than message with suffix\" do\n      ACON::Helper::Formatter.new.truncate(\"test\", 10).should eq \"test\"\n    end\n\n    it \"with negative length\" do\n      formatter = ACON::Helper::Formatter.new\n      message = \"testing truncate\"\n\n      formatter.truncate(message, -3).should eq \"testing trunc...\"\n      formatter.truncate(message, -100).should eq \"...\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/helper_set_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ACON::Helper::HelperSet do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"when the provided helper type is not an `ACON::Helper::Interface`\" do\n      ASPEC::Methods.assert_compile_time_error \"Helper class type 'String' is not an 'ACON::Helper::Interface'.\", <<-CR\n        require \"../spec_helper.cr\"\n\n        ACON::Helper::HelperSet.new[String]?\n      CR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/helper_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct HelperTest < ASPEC::TestCase\n  @[TestWith(\n    {0, \"< 1 sec\"},\n    {1, \"1 sec\"},\n    {2, \"2 secs\"},\n    {59, \"59 secs\"},\n    {60, \"1 min\"},\n    {61, \"1 min\"},\n    {119, \"1 min\"},\n    {120, \"2 mins\"},\n    {121, \"2 mins\"},\n    {4.minutes, \"4 mins\"},\n    {3599, \"59 mins\"},\n    {3600, \"1 hr\"},\n    {7199, \"1 hr\"},\n    {7200, \"2 hrs\"},\n    {7201, \"2 hrs\"},\n    {86399, \"23 hrs\"},\n    {86400, \"1 day\"},\n    {86401, \"1 day\"},\n    {172_799, \"1 day\"},\n    {172_800, \"2 days\"},\n    {172_801, \"2 days\"},\n  )]\n  def test_format_time(seconds : Int32 | Time::Span, expected : String) : Nil\n    ACON::Helper.format_time(seconds).should eq expected\n  end\n\n  @[TestWith(\n    {\"abc\", \"abc\"},\n    {\"abc<fg=default;bg=default>\", \"abc\"},\n    {\"a\\033[1;36mbc\", \"abc\"},\n    {\"a\\033]8;;http://url\\033\\\\b\\033]8;;\\033\\\\c\", \"abc\"},\n  )]\n  def test_remove_docoration(decorated_text : String, expected : String) : Nil\n    ACON::Helper.remove_decoration(ACON::Formatter::Output.new, decorated_text).should eq expected\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/output_wrapper_spec.cr",
    "content": "struct OutputWrapperTest < ASPEC::TestCase\n  def test_wrap_no_cut : Nil\n    ACON::Helper::OutputWrapper.new.wrap(\n      \"Árvíztűrőtükörfúrógép https://github.com/crystal/crystal Lorem ipsum <comment>dolor</comment> sit amet, consectetur adipiscing elit. Praesent vestibulum nulla quis urna maximus porttitor. Donec ullamcorper risus at <error>libero ornare</error> efficitur.\",\n      20,\n    ).should eq <<-TEXT\n    Árvíztűrőtükörfúrógé\n    p https://github.com/crystal/crystal Lorem ipsum\n    <comment>dolor</comment> sit amet,\n    consectetur\n    adipiscing elit.\n    Praesent vestibulum\n    nulla quis urna\n    maximus porttitor.\n    Donec ullamcorper\n    risus at <error>libero\n    ornare</error> efficitur.\n    TEXT\n  end\n\n  def test_wrap_with_cut : Nil\n    ACON::Helper::OutputWrapper.new(true).wrap(\n      \"Árvíztűrőtükörfúrógép https://github.com/crystal/crystal Lorem ipsum <comment>dolor</comment> sit amet, consectetur adipiscing elit. Praesent vestibulum nulla quis urna maximus porttitor. Donec ullamcorper risus at <error>libero ornare</error> efficitur.\",\n      20,\n    ).should eq <<-TEXT\n    Árvíztűrőtükörfúrógé\n    p\n    https://github.com/c\n    rystal/crystal Lorem\n    ipsum <comment>dolor</comment> sit\n    amet, consectetur\n    adipiscing elit.\n    Praesent vestibulum\n    nulla quis urna\n    maximus porttitor.\n    Donec ullamcorper\n    risus at <error>libero\n    ornare</error> efficitur.\n    TEXT\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/progress_bar_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct ProgressBarTest < ASPEC::TestCase\n  @clock : ACLK::Spec::MockClock\n\n  def initialize\n    ENV[\"COLUMNS\"] = \"120\"\n    @clock = ACLK::Spec::MockClock.new\n  end\n\n  protected def tear_down : Nil\n    ENV.delete \"COLUMNS\"\n  end\n\n  def test_multiple_start : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start\n    bar.advance\n    bar.start\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    1 [->--------------------------]\"),\n      self.generate_output(\"    0 [>---------------------------]\"),\n    )\n  end\n\n  def test_advance : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start at: 15\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"   15 [--------------->------------]\",\n      self.generate_output(\"   16 [---------------->-----------]\"),\n    )\n  end\n\n  def test_resume_with_max : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 5_000, 0\n    bar.start at: 1_000\n\n    self.assert_output(\n      output,\n      \" 1000/5000 [=====>----------------------]  20%\",\n    )\n  end\n\n  def test_regular_time_estimation : Nil\n    bar = ACON::Helper::ProgressBar.new self.output, 1_200, 0, clock: @clock\n\n    bar.start\n    bar.advance\n    bar.advance\n\n    @clock.sleep 1.second\n\n    bar.estimated.should eq 600\n  end\n\n  def test_resumed_time_estimation : Nil\n    bar = ACON::Helper::ProgressBar.new self.output, 1_200, 0, clock: @clock\n\n    bar.start at: 599\n    bar.advance\n\n    @clock.sleep 1.second\n\n    bar.estimated.should eq 1_200\n    bar.remaining.should eq 600\n  end\n\n  def test_advance_with_step : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start\n    bar.advance 5\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    5 [----->----------------------]\"),\n    )\n  end\n\n  def test_advance_multiple_times : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start\n    bar.advance 3\n    bar.advance 2\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    3 [--->------------------------]\"),\n      self.generate_output(\"    5 [----->----------------------]\"),\n    )\n  end\n\n  def test_advance_over_max : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 10, 0\n    bar.progress = 9\n    bar.advance\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"  9/10 [=========================>--]  90%\",\n      self.generate_output(\" 10/10 [============================] 100%\"),\n      self.generate_output(\" 11/11 [============================] 100%\"),\n    )\n  end\n\n  def test_regress : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start\n    bar.advance\n    bar.advance\n    bar.advance -1\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    1 [->--------------------------]\"),\n      self.generate_output(\"    2 [-->-------------------------]\"),\n      self.generate_output(\"    1 [->--------------------------]\"),\n    )\n  end\n\n  def test_regress_multiple_times : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start\n    bar.advance 3\n    bar.advance 3\n    bar.advance -1\n    bar.advance -2\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    3 [--->------------------------]\"),\n      self.generate_output(\"    6 [------>---------------------]\"),\n      self.generate_output(\"    5 [----->----------------------]\"),\n      self.generate_output(\"    3 [--->------------------------]\"),\n    )\n  end\n\n  def test_regress_with_step : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start\n    bar.advance 4\n    bar.advance 4\n    bar.advance -2\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    4 [---->-----------------------]\"),\n      self.generate_output(\"    8 [-------->-------------------]\"),\n      self.generate_output(\"    6 [------>---------------------]\"),\n    )\n  end\n\n  def test_regress_below_min : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 10, 0\n    bar.progress = 1\n    bar.advance -1\n    bar.advance -1\n\n    self.assert_output(\n      output,\n      \"  1/10 [==>-------------------------]  10%\",\n      self.generate_output(\"  0/10 [>---------------------------]   0%\"),\n    )\n  end\n\n  def test_format_max_constructor_no_format : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 10, 0\n    bar.start\n    bar.advance 10\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"  0/10 [>---------------------------]   0%\",\n      self.generate_output(\" 10/10 [============================] 100%\"),\n    )\n  end\n\n  def test_format_max_start_no_format : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start 10\n    bar.advance 10\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"  0/10 [>---------------------------]   0%\",\n      self.generate_output(\" 10/10 [============================] 100%\"),\n    )\n  end\n\n  def test_format_max_constructor_explicit_format_before_start : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 10, 0\n    bar.format = :normal\n    bar.start\n    bar.advance 10\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"  0/10 [>---------------------------]   0%\",\n      self.generate_output(\" 10/10 [============================] 100%\"),\n    )\n  end\n\n  def test_format_max_start_explicit_format_before_start : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.format = :normal\n    bar.start 10\n    bar.advance 10\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"  0/10 [>---------------------------]   0%\",\n      self.generate_output(\" 10/10 [============================] 100%\")\n    )\n  end\n\n  def test_customiations : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 10, 0\n    bar.bar_width = 10\n    bar.bar_character = \"_\"\n    bar.empty_bar_character = \" \"\n    bar.progress_character = \"/\"\n    bar.format = \" %current%/%max% [%bar%] %percent:3s%%\"\n    bar.start\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"  0/10 [/         ]   0%\",\n      self.generate_output(\"  1/10 [_/        ]  10%\"),\n    )\n  end\n\n  def test_display_without_start : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 50, 0\n    bar.display\n\n    self.assert_output(\n      output,\n      \"  0/50 [>---------------------------]   0%\"\n    )\n  end\n\n  def test_display_quiet_verbosity : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output(verbosity: :quiet), 50, 0\n    bar.display\n\n    self.assert_output(\n      output,\n      \"\"\n    )\n  end\n\n  def test_finish_without_start : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 50, 0\n    bar.finish\n\n    self.assert_output(\n      output,\n      \" 50/50 [============================] 100%\"\n    )\n  end\n\n  def test_percent : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 50, 0\n    bar.start\n    bar.display\n    bar.advance\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"  0/50 [>---------------------------]   0%\",\n      self.generate_output(\"  1/50 [>---------------------------]   2%\"),\n      self.generate_output(\"  2/50 [=>--------------------------]   4%\"),\n    )\n  end\n\n  def test_overwrite_with_shorter_line : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 50, 0\n    bar.format = \" %current%/%max% [%bar%] %percent:3s%%\"\n    bar.start\n    bar.display\n    bar.advance\n\n    # Set short format\n    bar.format = \" %current%/%max% [%bar%]\"\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"  0/50 [>---------------------------]   0%\",\n      self.generate_output(\"  1/50 [>---------------------------]   2%\"),\n      self.generate_output(\"  2/50 [=>--------------------------]\"),\n    )\n  end\n\n  def test_overwrite_with_section_output : Nil\n    sections = Array(ACON::Output::Section).new\n    acon_output = self.output\n\n    output = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n\n    bar = ACON::Helper::ProgressBar.new output, 50, 0\n    bar.start\n    bar.display\n    bar.advance\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"  0/50 [>---------------------------]   0%#{EOL}\",\n      \"\\e[1A\\e[0J  1/50 [>---------------------------]   2%#{EOL}\",\n      \"\\e[1A\\e[0J  2/50 [=>--------------------------]   4%#{EOL}\",\n    )\n  end\n\n  def test_overwrite_with_ansi_section_output : Nil\n    ENV[\"COLUMNS\"] = \"43\"\n\n    sections = Array(ACON::Output::Section).new\n    acon_output = self.output\n\n    output = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n\n    bar = ACON::Helper::ProgressBar.new output, 50, 0\n    bar.format = \" \\e[44;37m%current%/%max%\\e[0m [%bar%] %percent:3s%%\"\n    bar.start\n    bar.display\n    bar.advance\n    bar.advance\n\n    self.assert_output(\n      output,\n      \" \\e[44;37m 0/50\\e[0m [>---------------------------]   0%#{EOL}\",\n      \"\\e[1A\\e[0J \\e[44;37m 1/50\\e[0m [>---------------------------]   2%#{EOL}\",\n      \"\\e[1A\\e[0J \\e[44;37m 2/50\\e[0m [=>--------------------------]   4%#{EOL}\",\n    )\n  end\n\n  def test_overwrite_multiple_progress_bars_with_section_output : Nil\n    sections = Array(ACON::Output::Section).new\n    acon_output = self.output\n\n    output1 = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n    output2 = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n\n    bar1 = ACON::Helper::ProgressBar.new output1, 50, 0\n    bar2 = ACON::Helper::ProgressBar.new output2, 50, 0\n\n    bar1.start\n    bar2.start\n\n    bar2.advance\n    bar1.advance\n\n    self.assert_output(\n      acon_output,\n      \"  0/50 [>---------------------------]   0%#{EOL}\",\n      \"  0/50 [>---------------------------]   0%#{EOL}\",\n      \"\\e[1A\\e[0J  1/50 [>---------------------------]   2%#{EOL}\",\n      \"\\e[2A\\e[0J  1/50 [>---------------------------]   2%#{EOL}\",\n      \"\\e[1A\\e[0J  1/50 [>---------------------------]   2%#{EOL}\",\n      \"  1/50 [>---------------------------]   2%#{EOL}\",\n    )\n  end\n\n  def test_message : Nil\n    bar = ACON::Helper::ProgressBar.new self.output, minimum_seconds_between_redraws: 0\n    bar.message.should be_nil\n    bar.set_message \"other message\", \"other-message\"\n    bar.set_message \"my message\"\n\n    bar.message.should eq \"my message\"\n    bar.message(\"other-message\").should eq \"other message\"\n  end\n\n  def test_overwrite_with_new_lines_in_message : Nil\n    ACON::Helper::ProgressBar.set_format_definition \"test\", \"%current%/%max% [%bar%] %percent:3s%% %message% EXISTING TEXT.\"\n\n    bar = ACON::Helper::ProgressBar.new output = self.output, 50, 0\n    bar.format = \"test\"\n    bar.start\n    bar.display\n    bar.set_message \"MESSAGE\\nTEXT!\"\n    bar.advance\n    bar.set_message \"OTHER\\nTEXT!\"\n    bar.advance\n\n    self.assert_output(\n      output,\n      \" 0/50 [>---------------------------]   0% %message% EXISTING TEXT.\",\n      \"\\e[1G\\e[2K 1/50 [>---------------------------]   2% MESSAGE\\nTEXT! EXISTING TEXT.\",\n      \"\\e[1G\\e[2K\\e[1A\\e[1G\\e[2K 2/50 [=>--------------------------]   4% OTHER\\nTEXT! EXISTING TEXT.\",\n    )\n  end\n\n  def test_overwrite_with_section_output_with_newlines_in_message : Nil\n    sections = Array(ACON::Output::Section).new\n    acon_output = self.output\n\n    output = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n    ACON::Helper::ProgressBar.set_format_definition \"test\", \"%current%/%max% [%bar%] %percent:3s%% %message% EXISTING TEXT.\"\n\n    bar = ACON::Helper::ProgressBar.new output, 50, 0\n    bar.format = \"test\"\n    bar.start\n    bar.display\n    bar.set_message \"MESSAGE\\nTEXT!\"\n    bar.advance\n    bar.set_message \"OTHER\\nTEXT!\"\n    bar.advance\n\n    self.assert_output(\n      output,\n      \" 0/50 [>---------------------------]   0% %message% EXISTING TEXT.#{EOL}\",\n      \"\\e[1A\\e[0J 1/50 [>---------------------------]   2% MESSAGE\\nTEXT! EXISTING TEXT.#{EOL}\",\n      \"\\e[2A\\e[0J 2/50 [=>--------------------------]   4% OTHER\\nTEXT! EXISTING TEXT.#{EOL}\",\n    )\n  end\n\n  def test_multiple_sections_with_custom_format : Nil\n    sections = Array(ACON::Output::Section).new\n    acon_output = self.output\n\n    output1 = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n    output2 = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n\n    ACON::Helper::ProgressBar.set_format_definition \"custom\", \"%current%/%max% [%bar%] %percent:3s%% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.\"\n\n    bar1 = ACON::Helper::ProgressBar.new output1, 50, 0\n    bar2 = ACON::Helper::ProgressBar.new output2, 50, 0\n    bar2.format = \"custom\"\n\n    bar1.start\n    bar2.start\n\n    bar1.advance\n    bar2.advance\n\n    self.assert_output(\n      acon_output,\n      \"  0/50 [>---------------------------]   0%#{EOL}\",\n      \" 0/50 [>]   0% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.#{EOL}\",\n      \"\\e[4A\\e[0J 0/50 [>]   0% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.#{EOL}\",\n      \"\\e[3A\\e[0J  1/50 [>---------------------------]   2%#{EOL}\",\n      \" 0/50 [>]   0% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.#{EOL}\",\n      \"\\e[3A\\e[0J 1/50 [>]   2% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.#{EOL}\",\n    )\n  end\n\n  def test_start_with_max : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.format = \"%current%/%max% [%bar%]\"\n    bar.start 50\n    bar.advance\n\n    self.assert_output(\n      output,\n      \" 0/50 [>---------------------------]\",\n      self.generate_output(\" 1/50 [>---------------------------]\"),\n    )\n  end\n\n  def test_set_current_progress : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 50, 0\n    bar.start\n    bar.display\n    bar.advance\n    bar.progress = 15\n    bar.progress = 25\n\n    self.assert_output(\n      output,\n      \"  0/50 [>---------------------------]   0%\",\n      self.generate_output(\"  1/50 [>---------------------------]   2%\"),\n      self.generate_output(\" 15/50 [========>-------------------]  30%\"),\n      self.generate_output(\" 25/50 [==============>-------------]  50%\"),\n    )\n  end\n\n  def test_set_current_progress_before_start : Nil\n    bar = ACON::Helper::ProgressBar.new self.output, minimum_seconds_between_redraws: 0\n    bar.progress = 15\n    bar.start_time.should_not be_nil\n  end\n\n  def test_redraw_frequency : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 6, 0\n    bar.redraw_frequency = 2\n    bar.start\n    bar.progress = 1\n    bar.advance 2\n    bar.advance 2\n    bar.advance\n\n    self.assert_output(\n      output,\n      \" 0/6 [>---------------------------]   0%\",\n      self.generate_output(\" 3/6 [==============>-------------]  50%\"),\n      self.generate_output(\" 5/6 [=======================>----]  83%\"),\n      self.generate_output(\" 6/6 [============================] 100%\"),\n    )\n  end\n\n  def test_redraw_frequency_is_at_least_one_if_zero_given : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.redraw_frequency = 0\n    bar.start\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    1 [->--------------------------]\"),\n    )\n  end\n\n  def test_redraw_frequency_is_at_least_one_if_negative_given : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.redraw_frequency = -1\n    bar.start\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    1 [->--------------------------]\"),\n    )\n  end\n\n  def test_multi_byte_support : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start\n    bar.bar_character = \"■\"\n    bar.advance 3\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    3 [■■■>------------------------]\"),\n    )\n  end\n\n  def test_clear : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 50, 0\n    bar.start\n    bar.advance 25\n    bar.clear\n\n    self.assert_output(\n      output,\n      \"  0/50 [>---------------------------]   0%\",\n      self.generate_output(\" 25/50 [==============>-------------]  50%\"),\n      self.generate_output(\"\"),\n    )\n  end\n\n  def test_percent_not_hundred_before_complete : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 200, 0\n    bar.start\n    bar.display\n    bar.advance 199\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"   0/200 [>---------------------------]   0%\",\n      self.generate_output(\" 199/200 [===========================>]  99%\"),\n      self.generate_output(\" 200/200 [============================] 100%\"),\n    )\n  end\n\n  def test_non_decorated_output : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output(decorated: false), 200, 0\n    bar.start\n\n    200.times do\n      bar.advance\n    end\n\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"   0/200 [>---------------------------]   0%#{EOL}\",\n      \"  20/200 [==>-------------------------]  10%#{EOL}\",\n      \"  40/200 [=====>----------------------]  20%#{EOL}\",\n      \"  60/200 [========>-------------------]  30%#{EOL}\",\n      \"  80/200 [===========>----------------]  40%#{EOL}\",\n      \" 100/200 [==============>-------------]  50%#{EOL}\",\n      \" 120/200 [================>-----------]  60%#{EOL}\",\n      \" 140/200 [===================>--------]  70%#{EOL}\",\n      \" 160/200 [======================>-----]  80%#{EOL}\",\n      \" 180/200 [=========================>--]  90%#{EOL}\",\n      \" 200/200 [============================] 100%\",\n    )\n  end\n\n  def test_non_decorated_output_with_clear : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output(decorated: false), 50, 0\n    bar.start\n    bar.progress = 25\n    bar.clear\n    bar.progress = 50\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"  0/50 [>---------------------------]   0%#{EOL}\",\n      \" 25/50 [==============>-------------]  50%#{EOL}\",\n      \" 50/50 [============================] 100%\",\n    )\n  end\n\n  def test_non_decorated_output_without_max : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output(decorated: false), minimum_seconds_between_redraws: 0\n    bar.start\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]#{EOL}\",\n      \"    1 [->--------------------------]\",\n    )\n  end\n\n  def test_parallel_bars : Nil\n    output = self.output\n    bar1 = ACON::Helper::ProgressBar.new output, 2, minimum_seconds_between_redraws: 0\n    bar2 = ACON::Helper::ProgressBar.new output, 3, minimum_seconds_between_redraws: 0\n    bar2.progress_character = \"#\"\n    bar3 = ACON::Helper::ProgressBar.new output, minimum_seconds_between_redraws: 0\n\n    bar1.start\n    output.print \"\\n\"\n    bar2.start\n    output.print \"\\n\"\n    bar3.start\n\n    1.upto 3 do |idx|\n      # Up two lines\n      output.print \"\\e[2A\"\n\n      if idx <= 2\n        bar1.advance\n      end\n\n      output.print \"\\n\"\n      bar2.advance\n      output.print \"\\n\"\n      bar3.advance\n    end\n\n    output.print \"\\e[2A\"\n    output.print \"\\n\"\n    output.print \"\\n\"\n    bar3.finish\n\n    self.assert_output(\n      output,\n      \" 0/2 [>---------------------------]   0%\\n\",\n      \" 0/3 [#---------------------------]   0%\\n\",\n      \"    0 [>---------------------------]\",\n\n      \"\\e[2A\",\n      self.generate_output(\" 1/2 [==============>-------------]  50%\"),\n      \"\\n\",\n      self.generate_output(\" 1/3 [=========#------------------]  33%\"),\n      \"\\n\",\n      self.generate_output(\"    1 [->--------------------------]\").rstrip,\n\n      \"\\e[2A\",\n      self.generate_output(\" 2/2 [============================] 100%\"),\n      \"\\n\",\n      self.generate_output(\" 2/3 [==================#---------]  66%\"),\n      \"\\n\",\n      self.generate_output(\"    2 [-->-------------------------]\").rstrip,\n\n      \"\\e[2A\",\n      \"\\n\",\n      self.generate_output(\" 3/3 [============================] 100%\"),\n      \"\\n\",\n      self.generate_output(\"    3 [--->------------------------]\").rstrip,\n\n      \"\\e[2A\",\n      \"\\n\",\n      \"\\n\",\n      self.generate_output(\"    3 [============================]\").rstrip,\n    )\n  end\n\n  def test_without_max : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start\n    bar.advance\n    bar.advance\n    bar.advance\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    1 [->--------------------------]\"),\n      self.generate_output(\"    2 [-->-------------------------]\"),\n      self.generate_output(\"    3 [--->------------------------]\"),\n      self.generate_output(\"    3 [============================]\"),\n    )\n  end\n\n  def test_setting_max_during_progression : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.start\n    bar.progress = 2\n    bar.max_steps = 10\n    bar.progress = 5\n    bar.max_steps = 100\n    bar.progress = 10\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    2 [-->-------------------------]\"),\n      self.generate_output(\"  5/10 [==============>-------------]  50%\"),\n      self.generate_output(\"  10/100 [==>-------------------------]  10%\"),\n      self.generate_output(\" 100/100 [============================] 100%\"),\n    )\n  end\n\n  def test_with_small_screen : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n\n    ENV[\"COLUMNS\"] = \"12\"\n    bar.start\n    bar.advance\n    ENV[\"COLUMNS\"] = \"120\"\n\n    self.assert_output(\n      output,\n      \"    0 [>---]\",\n      self.generate_output(\"    1 [->--]\"),\n    )\n  end\n\n  def test_custom_placeholder_format : Nil\n    ACON::Helper::ProgressBar.set_placeholder_formatter \"remaining_steps\" do |bar|\n      \"#{bar.max_steps - bar.progress}\"\n    end\n\n    bar = ACON::Helper::ProgressBar.new output = self.output, 3, 0\n    bar.format = \" %remaining_steps% [%bar%]\"\n\n    bar.start\n    bar.advance\n    bar.finish\n\n    self.assert_output(\n      output,\n      \" 3 [>---------------------------]\",\n      self.generate_output(\" 2 [=========>------------------]\"),\n      self.generate_output(\" 0 [============================]\"),\n    )\n  end\n\n  def test_adding_instance_placeholder_formatter : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 3, 0\n    bar.format = \" %countdown% [%bar%]\"\n    bar.set_placeholder_formatter \"countdown\" do\n      \"#{bar.max_steps - bar.progress}\"\n    end\n\n    bar.start\n    bar.advance\n    bar.finish\n\n    self.assert_output(\n      output,\n      \" 3 [>---------------------------]\",\n      self.generate_output(\" 2 [=========>------------------]\"),\n      self.generate_output(\" 0 [============================]\"),\n    )\n  end\n\n  def test_multiline_format : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 3, 0\n    bar.format = \"%bar%\\nfoobar\"\n\n    bar.start\n    bar.advance\n    bar.clear\n    bar.finish\n\n    self.assert_output(\n      output,\n      \">---------------------------\\nfoobar\",\n      self.generate_output(\"=========>------------------\\nfoobar\"),\n      \"\\e[1G\\e[2K\\e[1A\",\n      self.generate_output(\"\"),\n      self.generate_output(\"============================\"),\n      \"\\nfoobar\",\n    )\n  end\n\n  def test_set_format_no_max : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.format = :normal\n    bar.start\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n    )\n  end\n\n  def test_set_format_with_max : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 10, 0\n    bar.format = :normal\n    bar.start\n\n    self.assert_output(\n      output,\n      \"  0/10 [>---------------------------]   0%\",\n    )\n  end\n\n  def test_unicode : Nil\n    ACON::Helper::ProgressBar.set_format_definition(\n      \"test\",\n      \"%current%/%max% [%bar%] %percent:3s%% %message% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.\"\n    )\n\n    bar = ACON::Helper::ProgressBar.new output = self.output, 10, 0\n    bar.format = \"test\"\n    bar.progress_character = \"💧\"\n    bar.start\n\n    output.io.to_s.should contain \" 0/10 [💧]   0%\"\n  end\n\n  @[TestWith(\n    {\"debug\"},\n    {\"very_verbose\"},\n    {\"verbose\"},\n    {\"normal\"},\n  )]\n  def test_formats_without_max(format : String) : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n    bar.format = format\n    bar.start\n\n    output.io.to_s.should_not be_empty\n  end\n\n  def test_bar_width_with_multiline_format : Nil\n    ENV[\"COLUMNS\"] = \"10\"\n\n    bar = ACON::Helper::ProgressBar.new self.output, minimum_seconds_between_redraws: 0\n    bar.format = \"%bar%\\n0123456789\"\n\n    # Before starting\n    bar.bar_width = 5\n    bar.bar_width.should eq 5\n\n    # After starting\n    bar.start\n    bar.bar_width.should eq 5\n\n    ENV[\"COLUMNS\"] = \"120\"\n  end\n\n  def test_min_and_max_seconds_between_redraws : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, clock: @clock\n    bar.redraw_frequency = 1\n    bar.minimum_seconds_between_redraws = 5\n    bar.maximum_seconds_between_redraws = 10\n\n    bar.start\n    bar.progress = 1\n    @clock.sleep 10.seconds\n    bar.progress = 2\n    @clock.sleep 20.seconds\n    bar.progress = 3\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    2 [-->-------------------------]\"),\n      self.generate_output(\"    3 [--->------------------------]\"),\n    )\n  end\n\n  def test_max_seconds_between_redraws : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0, clock: @clock\n    bar.redraw_frequency = 4 # Disable step based redraw\n    bar.start\n\n    bar.progress = 1 # No threshold hit, no redraw\n    bar.maximum_seconds_between_redraws = 2\n    @clock.sleep 1.second\n    bar.progress = 2 # Still no redraw because it takes 2 seconds for a redraw\n    @clock.sleep 1.second\n    bar.progress = 3 # 1 + 1 = 2 -> redraw\n    bar.progress = 4 # step based redraw freq hit, redraw even without sleep\n    bar.progress = 5 # No threshold hit, no redraw\n    bar.maximum_seconds_between_redraws = 3\n    @clock.sleep 2.seconds\n    bar.progress = 6 # No redraw even though 2 seconds passed. Throttling has priority\n    bar.maximum_seconds_between_redraws = 2\n    bar.progress = 7 # Throttling relaxed, draw\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    3 [--->------------------------]\"),\n      self.generate_output(\"    4 [---->-----------------------]\"),\n      self.generate_output(\"    7 [------->--------------------]\"),\n    )\n  end\n\n  def test_min_seconds_between_redraws : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0, clock: @clock\n    bar.redraw_frequency = 1\n    bar.minimum_seconds_between_redraws = 1\n    bar.start\n\n    bar.progress = 1 # Too fast, should not draw\n    @clock.sleep 1.second\n    bar.progress = 2 # 1 second passed, draw\n    bar.minimum_seconds_between_redraws = 2\n    @clock.sleep 1.second\n    bar.progress = 3 # 1 second passed, but the threshold was changed, should not draw\n    @clock.sleep 1.second\n    bar.progress = 4 # 1 + 1 seconds = 2 seconds passed, draw\n    bar.progress = 5 # No threshold hit, should not draw\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    2 [-->-------------------------]\"),\n      self.generate_output(\"    4 [---->-----------------------]\"),\n    )\n  end\n\n  def test_no_write_when_message_is_same : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 2\n    bar.start\n    bar.advance\n    bar.display\n\n    self.assert_output(\n      output,\n      \" 0/2 [>---------------------------]   0%\",\n      self.generate_output(\" 1/2 [==============>-------------]  50%\"),\n    )\n  end\n\n  def test_iterate : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n\n    result = [] of Int32\n\n    bar.iterate [1, 2] do |value|\n      result << value\n    end\n\n    result.should eq [1, 2]\n\n    self.assert_output(\n      output,\n      \" 0/2 [>---------------------------]   0%\",\n      self.generate_output(\" 1/2 [==============>-------------]  50%\"),\n      self.generate_output(\" 2/2 [============================] 100%\"),\n    )\n  end\n\n  def test_iterate_iterator : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, minimum_seconds_between_redraws: 0\n\n    result = [] of Int32\n\n    bar.iterate [1, 2].each do |value|\n      result << value\n    end\n\n    result.should eq [1, 2]\n\n    self.assert_output(\n      output,\n      \"    0 [>---------------------------]\",\n      self.generate_output(\"    1 [->--------------------------]\"),\n      self.generate_output(\"    2 [-->-------------------------]\"),\n      self.generate_output(\"    2 [============================]\"),\n    )\n  end\n\n  def test_ansi_colors_and_emojis : Nil\n    ENV[\"COLUMNS\"] = \"156\"\n    idx = 0\n\n    ACON::Helper::ProgressBar.set_placeholder_formatter \"custom_memory\" do\n      mem = 100_000 * idx\n\n      colors = idx.zero? ? \"44;37\" : \"41;37\"\n      idx += 1\n\n      \"\\e[#{colors}m #{mem.humanize_bytes} \\e[0m\"\n    end\n\n    bar = ACON::Helper::ProgressBar.new output = self.output, 15, 0\n    bar.format = \" \\033[44;37m %title:-37s% \\033[0m\\n %current%/%max% %bar% %percent:3s%%\\n 🏁  %remaining:-10s% %custom_memory:37s%\"\n    bar.bar_character = done = \"\\033[32m●\\033[0m\"\n    bar.empty_bar_character = empty = \"\\033[31m●\\033[0m\"\n    bar.progress_character = progress = \"\\033[32m➤ \\033[0m\"\n\n    bar.set_message \"Starting the demo... fingers crossed\", \"title\"\n    bar.start\n\n    self.assert_output(\n      output,\n      \" \\033[44;37m Starting the demo... fingers crossed  \\033[0m\\n\",\n      \"  0/15 #{progress}#{empty * 26}   0%\\n\",\n      \" \\xf0\\x9f\\x8f\\x81  < 1 sec                         \\033[44;37m 0B \\033[0m\",\n    )\n\n    output.io.as(IO::Memory).clear\n\n    bar.set_message \"Looks good to me...\", \"title\"\n    bar.advance 4\n\n    self.assert_output(\n      output,\n      self.generate_output(\n        \" \\e[44;37m Looks good to me...                   \\e[0m\\n\",\n        \"  4/15 #{done * 7}#{progress}#{empty * 19}  26%\\n\",\n        \" \\xf0\\x9f\\x8f\\x81  < 1 sec                      \\e[41;37m 98kiB \\e[0m\",\n      )\n    )\n\n    output.io.as(IO::Memory).clear\n\n    bar.set_message \"Thanks, bye\", \"title\"\n    bar.finish\n\n    self.assert_output(\n      output,\n      self.generate_output(\n        \" \\e[44;37m Thanks, bye                           \\e[0m\\n\",\n        \" 15/15 #{done * 28} 100%\\n\",\n        \" \\xf0\\x9f\\x8f\\x81  < 1 sec                     \\e[41;37m 195kiB \\e[0m\",\n      )\n    )\n\n    ENV[\"COLUMNS\"] = \"120\"\n  end\n\n  def test_multiline_format_is_fully_cleared : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output, 3\n    bar.format = \"%current%/%max%\\n%message%\\nFoo\"\n\n    bar.set_message \"1234567890\"\n    bar.start\n    bar.display\n\n    bar.set_message \"ABC\"\n    bar.advance\n    bar.display\n\n    bar.set_message \"A\"\n    bar.advance\n    bar.display\n\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"0/3\\n1234567890\\nFoo\",\n      self.generate_output(\"1/3\\nABC\\nFoo\"),\n      self.generate_output(\"2/3\\nA\\nFoo\"),\n      self.generate_output(\"3/3\\nA\\nFoo\"),\n    )\n  end\n\n  def test_multiline_format_is_fully_correct_with_manual_cleanup : Nil\n    bar = ACON::Helper::ProgressBar.new output = self.output\n    bar.set_message %(Processing \"foobar\"...)\n    bar.format = \"[%bar%]\\n%message%\"\n\n    bar.start\n    bar.clear\n    output.puts \"Foo!\"\n    bar.display\n    bar.finish\n\n    self.assert_output(\n      output,\n      \"[>---------------------------]\\n\",\n      \"Processing \\\"foobar\\\"...\",\n      \"\\x1B[1G\\x1B[2K\\x1B[1A\",\n      self.generate_output(\"\"),\n      \"Foo!#{EOL}\",\n      self.generate_output(\"[--->------------------------]\"),\n      \"\\nProcessing \\\"foobar\\\"...\",\n      self.generate_output(\"[----->----------------------]\\nProcessing \\\"foobar\\\"...\"),\n    )\n  end\n\n  def test_overwrite_with_section_output_and_eol : Nil\n    sections = Array(ACON::Output::Section).new\n    acon_output = self.output\n\n    output = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n\n    bar = ACON::Helper::ProgressBar.new output, 50, 0\n    bar.format = \"[%bar%] %percent:3s%%#{EOL}%message%#{EOL}\"\n    bar.set_message \"\"\n    bar.start\n    bar.display\n    bar.set_message \"Doing something...\"\n    bar.advance\n    bar.set_message \"Doing something foo...\"\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"[>---------------------------]   0%#{EOL}#{EOL}\",\n      \"\\x1b[2A\\x1b[0J[>---------------------------]   2%#{EOL}Doing something...#{EOL}\",\n      \"\\x1b[2A\\x1b[0J[=>--------------------------]   4%#{EOL}Doing something foo...#{EOL}\",\n    )\n  end\n\n  def test_overwrite_with_section_output_and_eol_with_empty_message : Nil\n    sections = Array(ACON::Output::Section).new\n    acon_output = self.output\n\n    output = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n\n    bar = ACON::Helper::ProgressBar.new output, 50, 0\n    bar.format = \"[%bar%] %percent:3s%%#{EOL}%message%\"\n    bar.set_message \"Start\"\n    bar.start\n    bar.display\n    bar.set_message \"\"\n    bar.advance\n    bar.set_message \"Doing something...\"\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"[>---------------------------]   0%#{EOL}Start#{EOL}\",\n      \"\\x1b[2A\\x1b[0J[>---------------------------]   2%#{EOL}\",\n      \"\\x1b[1A\\x1b[0J[=>--------------------------]   4%#{EOL}Doing something...#{EOL}\",\n    )\n  end\n\n  def test_overwrite_with_section_output_and_eol_with_empty_message_comment : Nil\n    sections = Array(ACON::Output::Section).new\n    acon_output = self.output\n\n    output = ACON::Output::Section.new acon_output.io, sections, verbosity: acon_output.verbosity, decorated: acon_output.decorated?, formatter: ACON::Formatter::Output.new\n\n    bar = ACON::Helper::ProgressBar.new output, 50, 0\n    bar.format = \"[%bar%] %percent:3s%%#{EOL}<comment>%message%</comment>\"\n    bar.set_message \"Start\"\n    bar.start\n    bar.display\n    bar.set_message \"\"\n    bar.advance\n    bar.set_message \"Doing something...\"\n    bar.advance\n\n    self.assert_output(\n      output,\n      \"[>---------------------------]   0%#{EOL}\\x1b[33mStart\\x1b[39m#{EOL}\",\n      \"\\x1b[2A\\x1b[0J[>---------------------------]   2%#{EOL}\",\n      \"\\x1b[1A\\x1b[0J[=>--------------------------]   4%#{EOL}\\x1b[33mDoing something...\\x1b[39m#{EOL}\",\n    )\n  end\n\n  private def generate_output(*expected : String) : String\n    self.generate_output expected.join\n  end\n\n  private def generate_output(expected : String) : String\n    count = expected.count '\\n'\n\n    sub_str = if count > 0\n                \"\\e[1G\\e[2K\\e[1A\" * count\n              else\n                \"\"\n              end\n\n    \"#{sub_str}\\e[1G\\e[2K#{expected}\"\n  end\n\n  private def output(decorated : Bool = true, verbosity : ACON::Output::Verbosity = :normal) : ACON::Output::Interface\n    ACON::Output::IO.new IO::Memory.new, decorated: decorated, verbosity: verbosity\n  end\n\n  private def assert_output(output : ACON::Output::Interface, start : String, *frames : String, line : Int32 = __LINE__, file : String = __FILE__) : Nil\n    self.assert_output output, start, frames, line: line, file: file\n  end\n\n  private def assert_output(output : ACON::Output::Interface, start : String, frames : Enumerable(String) = [] of String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil\n    expected = String.build do |io|\n      io << start\n\n      frames.join io\n    end\n\n    output.io.to_s.should eq(expected), line: line, file: file\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/progress_indicator_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct ProgressIndicatorTest < ASPEC::TestCase\n  @clock : ACLK::Spec::MockClock\n\n  def initialize\n    @clock = ACLK::Spec::MockClock.new\n  end\n\n  def test_set_placeholder_formatter : Nil\n    ACON::Helper::ProgressIndicator.set_placeholder_formatter \"custom-message\" do\n      # Return any arbitrary string\n      \"My Custom Message\"\n    end\n\n    ACON::Helper::ProgressIndicator\n      .placeholder_formatter(\"custom-message\")\n      .try(&.call(ACON::Helper::ProgressIndicator.new self.output(decorated: false)))\n      .should eq \"My Custom Message\"\n  end\n\n  def test_default_indicator : Nil\n    indicator = ACON::Helper::ProgressIndicator.new output = self.output, clock: @clock\n\n    indicator.start \"Starting...\"\n    @clock.sleep 101.milliseconds\n    indicator.advance\n    @clock.sleep 101.milliseconds\n    indicator.advance\n    @clock.sleep 101.milliseconds\n    indicator.advance\n    @clock.sleep 101.milliseconds\n    indicator.advance\n    @clock.sleep 101.milliseconds\n    indicator.advance\n    @clock.sleep 101.milliseconds\n    indicator.message = \"Advancing...\"\n    indicator.advance\n    indicator.finish \"Done...\"\n    indicator.start \"Starting Again...\"\n    @clock.sleep 101.milliseconds\n    indicator.advance\n    indicator.finish \"Done Again...\"\n\n    self.assert_output(\n      output,\n      self.generate_output(\" - Starting...\"),\n      self.generate_output(\" \\\\ Starting...\"),\n      self.generate_output(\" | Starting...\"),\n      self.generate_output(\" / Starting...\"),\n      self.generate_output(\" - Starting...\"),\n      self.generate_output(\" \\\\ Starting...\"),\n      self.generate_output(\" \\\\ Advancing...\"),\n      self.generate_output(\" | Advancing...\"),\n      self.generate_output(\" ✔ Done...\"),\n      EOL,\n      self.generate_output(\" - Starting Again...\"),\n      self.generate_output(\" \\\\ Starting Again...\"),\n      self.generate_output(\" ✔ Done Again...\"),\n      EOL,\n    )\n  end\n\n  def test_non_decorated : Nil\n    indicator = ACON::Helper::ProgressIndicator.new output = self.output(decorated: false)\n\n    indicator.start \"Starting...\"\n    indicator.advance\n    indicator.advance\n    indicator.message = \"Midway...\"\n    indicator.advance\n    indicator.advance\n    indicator.finish \"Done...\"\n\n    self.assert_output(\n      output,\n      \" Starting...#{EOL}\",\n      \" Midway...#{EOL}\",\n      \" Done...#{EOL}#{EOL}\",\n    )\n  end\n\n  def test_custom_indicator_values : Nil\n    indicator = ACON::Helper::ProgressIndicator.new output = self.output, indicator_values: %w(a b c), clock: @clock\n\n    indicator.start \"Starting...\"\n    @clock.sleep 101.milliseconds\n    indicator.advance\n    @clock.sleep 101.milliseconds\n    indicator.advance\n    @clock.sleep 101.milliseconds\n    indicator.advance\n\n    self.assert_output(\n      output,\n      self.generate_output(\" a Starting...\"),\n      self.generate_output(\" b Starting...\"),\n      self.generate_output(\" c Starting...\"),\n      self.generate_output(\" a Starting...\"),\n    )\n  end\n\n  def test_custom_finished_indicator_value : Nil\n    indicator = ACON::Helper::ProgressIndicator.new output = self.output, finished_indicator: \"✅\", clock: @clock\n\n    indicator.start \"Starting...\"\n    @clock.sleep 101.milliseconds\n    indicator.finish \"Done\"\n\n    self.assert_output(\n      output,\n      self.generate_output(\" - Starting...\"),\n      self.generate_output(\" ✅ Done\"),\n      EOL\n    )\n  end\n\n  def test_custom_finished_indicator_value_finish : Nil\n    indicator = ACON::Helper::ProgressIndicator.new output = self.output, clock: @clock\n\n    indicator.start \"Starting...\"\n    @clock.sleep 101.milliseconds\n    indicator.finish \"Done\", \"|==|\"\n\n    self.assert_output(\n      output,\n      self.generate_output(\" - Starting...\"),\n      self.generate_output(\" |==| Done\"),\n      EOL\n    )\n  end\n\n  def test_requires_at_least_two_indicator_characters : Nil\n    expect_raises ACON::Exception::InvalidArgument, \"Must have at least 2 indicator value characters.\" do\n      ACON::Helper::ProgressIndicator.new self.output, indicator_values: %w(a)\n    end\n  end\n\n  def test_cannot_start_already_started_indicator : Nil\n    indicator = ACON::Helper::ProgressIndicator.new self.output\n    indicator.start \"Starting...\"\n\n    expect_raises ACON::Exception::Logic, \"Progress indicator is already started.\" do\n      indicator.start \"Starting Again...\"\n    end\n  end\n\n  def test_cannot_advance_unstarted_indicator : Nil\n    indicator = ACON::Helper::ProgressIndicator.new self.output\n\n    expect_raises ACON::Exception::Logic, \"Progress indicator has not yet been started.\" do\n      indicator.advance\n    end\n  end\n\n  def test_cannot_finish_unstarted_indicator : Nil\n    indicator = ACON::Helper::ProgressIndicator.new self.output\n\n    expect_raises ACON::Exception::Logic, \"Progress indicator has not yet been started.\" do\n      indicator.finish \"Finishing...\"\n    end\n  end\n\n  @[TestWith(\n    {ACON::Helper::ProgressIndicator::Format::DEBUG},\n    {ACON::Helper::ProgressIndicator::Format::VERY_VERBOSE},\n    {ACON::Helper::ProgressIndicator::Format::VERBOSE},\n    {ACON::Helper::ProgressIndicator::Format::NORMAL},\n  )]\n  def test_formats(format : ACON::Helper::ProgressIndicator::Format) : Nil\n    indicator = ACON::Helper::ProgressIndicator.new output = self.output, format: format\n    indicator.start \"Starting...\"\n    indicator.advance\n\n    output.io.to_s.should_not be_empty\n  end\n\n  private def generate_output(expected : String) : String\n    count = expected.count '\\n'\n\n    sub_str = if count > 0\n                \"\\033[#{count}A\"\n              else\n                \"\"\n              end\n\n    \"\\x0D\\x1B[2K#{sub_str}#{expected}\"\n  end\n\n  private def output(decorated : Bool = true, verbosity : ACON::Output::Verbosity = :normal) : ACON::Output::Interface\n    ACON::Output::IO.new IO::Memory.new, decorated: decorated, verbosity: verbosity\n  end\n\n  private def assert_output(output : ACON::Output::Interface, start : String, *frames : String, line : Int32 = __LINE__, file : String = __FILE__) : Nil\n    self.assert_output output, start, frames, line: line, file: file\n  end\n\n  private def assert_output(output : ACON::Output::Interface, start : String, frames : Enumerable(String) = [] of String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil\n    expected = String.build do |io|\n      io << start\n\n      frames.join io\n    end\n\n    output.io.to_s.should eq(expected), line: line, file: file\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/question_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./abstract_question_helper_test_case\"\n\nstruct QuestionHelperTest < AbstractQuestionHelperTest\n  @helper : ACON::Helper::Question\n\n  def initialize\n    @helper = ACON::Helper::Question.new\n\n    super\n  end\n\n  def tear_down : Nil\n    ENV.delete \"COLUMNS\"\n  end\n\n  def test_ask_choice_question : Nil\n    heroes = [\"Superman\", \"Batman\", \"Spiderman\"]\n    self.with_input \"\\n1\\n  1  \\nGeorge\\n1\\nGeorge\\n\\n\\n\" do |input|\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes, 2\n      question.max_attempts = 1\n\n      # First answer is empty, so should use default\n      @helper.ask(input, @output, question).should eq \"Spiderman\"\n\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes\n      question.max_attempts = 1\n\n      @helper.ask(input, @output, question).should eq \"Batman\"\n      @helper.ask(input, @output, question).should eq \"Batman\"\n\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes\n      question.error_message = \"Input '%s' is not a superhero!\"\n      question.max_attempts = 2\n\n      @helper.ask(input, @output, question).should eq \"Batman\"\n\n      begin\n        question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes, 1\n        question.max_attempts = 1\n        @helper.ask input, @output, question\n      rescue ex : ACON::Exception::InvalidArgument\n        ex.message.should eq \"Value 'George' is invalid.\"\n      end\n\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes, \"0\"\n      question.max_attempts = 1\n\n      @helper.ask(input, @output, question).should eq \"Superman\"\n    end\n  end\n\n  def test_ask_choice_question_non_interactive : Nil\n    heroes = [\"Superman\", \"Batman\", \"Spiderman\"]\n    self.with_input \"\\n1\\n  1  \\nGeorge\\n1\\nGeorge\\n1\\n\", false do |input|\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes, 0\n      @helper.ask(input, @output, question).should eq \"Superman\"\n\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes, \"Batman\"\n      @helper.ask(input, @output, question).should eq \"Batman\"\n\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes\n      @helper.ask(input, @output, question).should be_nil\n\n      question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes, 0\n      question.validator = nil\n      @helper.ask(input, @output, question).should eq \"Superman\"\n\n      begin\n        question = ACON::Question::Choice.new \"Who is your favorite superhero?\", heroes\n        @helper.ask input, @output, question\n      rescue ex : ACON::Exception::InvalidArgument\n        ex.message.should eq \"Value '' is invalid.\"\n      end\n    end\n  end\n\n  def test_ask_multiple_choice : Nil\n    heroes = [\"Superman\", \"Batman\", \"Spiderman\"]\n\n    self.with_input \"1\\n0,2\\n 0 , 2  \\n\\n\\n\" do |input|\n      question = ACON::Question::MultipleChoice.new \"Who are your favorite superheros?\", heroes\n      question.max_attempts = 1\n\n      @helper.ask(input, @output, question).should eq [\"Batman\"]\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Spiderman\"]\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Spiderman\"]\n\n      question = ACON::Question::MultipleChoice.new \"Who are your favorite superheros?\", heroes, \"0,1\"\n      question.max_attempts = 1\n\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Batman\"]\n\n      question = ACON::Question::MultipleChoice.new \"Who are your favorite superheros?\", heroes, \" 0 , 1 \"\n      question.max_attempts = 1\n\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Batman\"]\n    end\n  end\n\n  def test_ask_multiple_choice_non_interactive : Nil\n    heroes = [\"Superman\", \"Batman\", \"Spiderman\"]\n\n    self.with_input \"1\\n0,2\\n 0 , 2  \", false do |input|\n      question = ACON::Question::MultipleChoice.new \"Who are your favorite superheros?\", heroes, \"0,1\"\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Batman\"]\n\n      question = ACON::Question::MultipleChoice.new \"Who are your favorite superheros?\", heroes, \" 0 , 1 \"\n      question.validator = nil\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Batman\"]\n\n      question = ACON::Question::MultipleChoice.new \"Who are your favorite superheros?\", heroes, \"0,Batman\"\n      @helper.ask(input, @output, question).should eq [\"Superman\", \"Batman\"]\n\n      question = ACON::Question::MultipleChoice.new \"Who are your favorite superheros?\", heroes\n      @helper.ask(input, @output, question).should be_nil\n\n      question = ACON::Question::MultipleChoice.new \"Who are your favorite superheros?\", {\"a\" => \"Batman\", \"b\" => \"Superman\"}, \"a\"\n      @helper.ask(input, @output, question).should eq [\"Batman\"]\n\n      begin\n        question = ACON::Question::MultipleChoice.new \"Who are your favorite superheros?\", heroes, \"\"\n        @helper.ask input, @output, question\n      rescue ex : ACON::Exception::InvalidArgument\n        ex.message.should eq \"Value '' is invalid.\"\n      end\n    end\n  end\n\n  def test_ask : Nil\n    self.with_input \"\\n8AM\\n\" do |input|\n      question = ACON::Question.new \"What time is it?\", \"2PM\"\n      @helper.ask(input, @output, question).should eq \"2PM\"\n\n      question = ACON::Question.new \"What time is it?\", \"2PM\"\n      @helper.ask(input, @output, question).should eq \"8AM\"\n\n      self.assert_output_contains \"What time is it?\"\n    end\n  end\n\n  def test_ask_non_trimmed : Nil\n    question = ACON::Question.new \"What time is it?\", \"2PM\"\n    question.trimmable = false\n\n    self.with_input \" 8AM \" do |input|\n      @helper.ask(input, @output, question).should eq \" 8AM \"\n    end\n\n    self.assert_output_contains \"What time is it?\"\n  end\n\n  # TODO: Add autocompleter tests\n\n  def test_ask_hidden : Nil\n    question = ACON::Question.new \"What time is it?\", \"2PM\"\n    question.hidden = true\n\n    self.with_input \"8AM\\n\" do |input|\n      @helper.ask(input, @output, question).should eq \"8AM\"\n    end\n\n    self.assert_output_contains \"What time is it?\"\n  end\n\n  def test_ask_hidden_non_trimmed : Nil\n    question = ACON::Question.new \"What time is it?\", \"2PM\"\n    question.hidden = true\n    question.trimmable = false\n\n    self.with_input \" 8AM\" do |input|\n      @helper.ask(input, @output, question).should eq \" 8AM\"\n    end\n\n    self.assert_output_contains \"What time is it?\"\n  end\n\n  def test_ask_multi_line : Nil\n    essay = <<-ESSAY\n    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque pretium lectus quis suscipit porttitor. Sed pretium bibendum vestibulum.\n\n    Etiam accumsan, justo vitae imperdiet aliquet, neque est sagittis mauris, sed interdum massa leo id leo.\n\n    Aliquam rhoncus, libero ac blandit convallis, est sapien hendrerit nulla, vitae aliquet tellus orci a odio. Aliquam gravida ante sit amet massa lacinia, ut condimentum purus venenatis.\n\n    Vivamus et erat dictum, euismod neque in, laoreet odio. Aenean vitae tellus at leo vestibulum auctor id eget urna.\n    ESSAY\n\n    question = ACON::Question(String?).new \"Write an essay\", nil\n    question.multi_line = true\n\n    self.with_input essay do |input|\n      @helper.ask(input, @output, question).should eq essay\n    end\n  end\n\n  def test_ask_multi_line_response_with_single_newline : Nil\n    question = ACON::Question(String?).new \"Write an essay\", nil\n    question.multi_line = true\n\n    self.with_input \"\\n\" do |input|\n      @helper.ask(input, @output, question).should be_nil\n    end\n  end\n\n  def test_ask_multi_line_response_with_data_after_newline : Nil\n    question = ACON::Question(String?).new \"Write an essay\", nil\n    question.multi_line = true\n\n    self.with_input \"\\nSome Text\" do |input|\n      @helper.ask(input, @output, question).should be_nil\n    end\n  end\n\n  def test_ask_multi_line_response_multiple_newlines_at_end : Nil\n    question = ACON::Question(String?).new \"Write an essay\", nil\n    question.multi_line = true\n\n    self.with_input \"Some Text\\n\\n\" do |input|\n      @helper.ask(input, @output, question).should eq \"Some Text\"\n    end\n  end\n\n  @[DataProvider(\"confirmation_provider\")]\n  def test_ask_confirmation(answer : String, expected : Bool, default : Bool) : Nil\n    question = ACON::Question::Confirmation.new \"Some question\", default\n\n    self.with_input \"#{answer}\\n\" do |input|\n      @helper.ask(input, @output, question).should eq expected\n    end\n  end\n\n  def confirmation_provider : Tuple\n    {\n      {\"\", true, true},\n      {\"\", false, false},\n      {\"y\", true, false},\n      {\"yes\", true, false},\n      {\"n\", false, true},\n      {\"no\", false, true},\n    }\n  end\n\n  def test_ask_confirmation_custom_true_answer : Nil\n    question = ACON::Question::Confirmation.new \"Some question\", false, /^(j|y)/i\n\n    self.with_input \"j\\ny\\n\" do |input|\n      @helper.ask(input, @output, question).should be_true\n      @helper.ask(input, @output, question).should be_true\n    end\n  end\n\n  def test_ask_and_validate : Nil\n    error = \"This is not a color!\"\n\n    question = ACON::Question.new \" What is your favorite color?\", \"white\"\n    question.max_attempts = 2\n    question.validator do |answer|\n      raise ACON::Exception::Runtime.new error unless answer.in? \"white\", \"black\"\n\n      answer\n    end\n\n    self.with_input \"\\nblack\\n\" do |input|\n      @helper.ask(input, @output, question).should eq \"white\"\n      @helper.ask(input, @output, question).should eq \"black\"\n    end\n\n    self.with_input \"green\\nyellow\\norange\\n\" do |input|\n      expect_raises ACON::Exception::Runtime, error do\n        @helper.ask input, @output, question\n      end\n    end\n  end\n\n  @[DataProvider(\"simple_answer_provider\")]\n  def test_ask_choice_simple_answers(answer, expected : String) : Nil\n    choices = [\n      \"My environment 1\",\n      \"My environment 2\",\n      \"My environment 3\",\n    ]\n\n    question = ACON::Question::Choice.new \"Please select the environment to load\", choices\n    question.max_attempts = 1\n\n    self.with_input \"#{answer}\\n\" do |input|\n      @helper.ask(input, @output, question).should eq expected\n    end\n  end\n\n  def simple_answer_provider : Tuple\n    {\n      {0, \"My environment 1\"},\n      {1, \"My environment 2\"},\n      {2, \"My environment 3\"},\n      {\"My environment 1\", \"My environment 1\"},\n      {\"My environment 2\", \"My environment 2\"},\n      {\"My environment 3\", \"My environment 3\"},\n    }\n  end\n\n  @[DataProvider(\"special_character_provider\")]\n  def test_ask_special_characters_multiple_choice(answer : String, expected : Array(String)) : Nil\n    choices = [\n      \".\",\n      \"src\",\n    ]\n\n    question = ACON::Question::MultipleChoice.new \"Please select the environment to load\", choices\n    question.max_attempts = 1\n\n    self.with_input \"#{answer}\\n\" do |input|\n      @helper.ask(input, @output, question).should eq expected\n    end\n  end\n\n  def special_character_provider : Tuple\n    {\n      {\".\", [\".\"]},\n      {\".,src\", [\".\", \"src\"]},\n    }\n  end\n\n  @[DataProvider(\"answer_provider\")]\n  def test_ask_choice_hash_choices(answer : String, expected : String) : Nil\n    choices = {\n      \"env_1\" => \"My environment 1\",\n      \"env_2\" => \"My environment\",\n      \"env_3\" => \"My environment\",\n    }\n\n    question = ACON::Question::Choice.new \"Please select the environment to load\", choices\n    question.max_attempts = 1\n\n    self.with_input \"#{answer}\\n\" do |input|\n      @helper.ask(input, @output, question).should eq expected\n    end\n  end\n\n  def answer_provider : Tuple\n    {\n      {\"env_1\", \"My environment 1\"},\n      {\"env_2\", \"My environment\"},\n      {\"env_3\", \"My environment\"},\n      {\"My environment 1\", \"My environment 1\"},\n    }\n  end\n\n  def test_ask_ambiguous_choice : Nil\n    choices = {\n      \"env_1\" => \"My first environment\",\n      \"env_2\" => \"My environment\",\n      \"env_3\" => \"My environment\",\n    }\n\n    question = ACON::Question::Choice.new \"Please select the environment to load\", choices\n    question.max_attempts = 1\n\n    self.with_input \"My environment\\n\" do |input|\n      expect_raises ACON::Exception::InvalidArgument, \"The provided answer is ambiguous. Value should be one of 'env_2' or 'env_3'.\" do\n        @helper.ask input, @output, question\n      end\n    end\n  end\n\n  def test_ask_non_interactive : Nil\n    question = ACON::Question.new \"Some question\", \"some answer\"\n\n    self.with_input \"yes\", false do |input|\n      @helper.ask(input, @output, question).should eq \"some answer\"\n    end\n  end\n\n  def test_ask_raises_on_missing_input : Nil\n    question = ACON::Question.new \"Some question\", \"some answer\"\n\n    self.with_input \"\" do |input|\n      expect_raises ACON::Exception::MissingInput, \"Aborted.\" do\n        @helper.ask input, @output, question\n      end\n    end\n  end\n\n  # TODO: What to do if the input is \"\"?\n\n  def test_question_validator_repeats_the_prompt : Nil\n    tries = 0\n\n    app = ACON::Application.new \"foo\"\n    app.auto_exit = false\n    app.register \"question\" do |input, output|\n      question = ACON::Question(String?).new \"This is a promptable question\", nil\n      question.validator do |answer|\n        tries += 1\n\n        raise \"\" unless answer.presence\n\n        answer\n      end\n\n      ACON::Helper::Question.new.ask input, output, question\n\n      ACON::Command::Status::SUCCESS\n    end\n\n    tester = ACON::Spec::ApplicationTester.new app\n    tester.inputs = [\"\", \"not-empty\"]\n\n    tester.run(command: \"question\", interactive: true).should eq ACON::Command::Status::SUCCESS\n    tries.should eq 2\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/table_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct TableSpec < ASPEC::TestCase\n  @output : IO\n\n  protected def get_table_contents(table_name : String) : String\n    File.read(File.join __DIR__, \"..\", \"fixtures\", \"helper\", \"table\", \"#{table_name}.txt\") # .gsub(EOL, \"\\n\")\n  end\n\n  def initialize\n    @output = IO::Memory.new\n  end\n\n  protected def tear_down : Nil\n    @output.close\n  end\n\n  def test_rows_headers_overloads : Nil\n    ACON::Helper::Table.new(output = self.io_output)\n      .headers([\"1\", \"2\", 3])\n      .headers([4, 5, 6])\n      .headers(false, true, false)\n      .add_row([\"Foo\", 123, 19.075])\n      .add_row(\"Bar\", 456, false)\n      .add_rows([\n        [\"Baz\"],\n        [\"Biz\"],\n      ])\n      .row(0, %w(a b c))\n      .render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +-------+------+-------+\n    | false | true | false |\n    +-------+------+-------+\n    | a     | b    | c     |\n    | Bar   | 456  | false |\n    | Baz   |      |       |\n    | Biz   |      |       |\n    +-------+------+-------+\n\n    TABLE\n  end\n\n  @[DataProvider(\"render_provider\")]\n  def test_render(headers, rows, style : String, expected : String, decorated : Bool) : Nil\n    table = ACON::Helper::Table.new output = self.io_output decorated\n    table\n      .headers(headers)\n      .rows(rows)\n      .style(style)\n      .render\n\n    self.output_content(output).should eq expected.gsub EOL, \"\\n\"\n  end\n\n  def render_provider : Hash\n    books = [\n      [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n      [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\"],\n      [\"960-425-059-0\", \"The Lord of the Rings\", \"J. R. R. Tolkien\"],\n      [\"80-902734-1-6\", \"And Then There Were None\", \"Agatha Christie\"],\n    ]\n\n    {\n      \"Default style\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        books,\n        style = \"default\",\n        self.get_table_contents(style),\n        false,\n      },\n      \"Markdown style\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        books,\n        style = \"markdown\",\n        self.get_table_contents(style),\n        false,\n      },\n      \"Compact style\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        books,\n        style = \"compact\",\n        self.get_table_contents(style),\n        false,\n      },\n      \"Borderless style\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        books,\n        style = \"borderless\",\n        self.get_table_contents(style),\n        false,\n      },\n      \"Box style\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        books,\n        style = \"box\",\n        self.get_table_contents(style),\n        false,\n      },\n      \"Double box with separator\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        [\n          [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n          [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\"],\n          ACON::Helper::Table::Separator.new,\n          [\"960-425-059-0\", \"The Lord of the Rings\", \"J. R. R. Tolkien\"],\n          [\"80-902734-1-6\", \"And Then There Were None\", \"Agatha Christie\"],\n        ],\n        \"double-box\",\n        self.get_table_contents(\"double_box_separator\"),\n        false,\n      },\n      \"Default missing cell values\" => {\n        [[\"ISBN\", \"Title\"]],\n        [\n          [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n          [\"9971-5-0210-0\"],\n          [\"960-425-059-0\", \"The Lord of the Rings\", \"J. R. R. Tolkien\"],\n          [\"80-902734-1-6\", \"And Then There Were None\", \"Agatha Christie\"],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_missing_cell_values\"),\n        false,\n      },\n      \"Default no headers\" => {\n        [[] of String],\n        [\n          [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n          [\"9971-5-0210-0\"],\n          [\"960-425-059-0\", \"The Lord of the Rings\", \"J. R. R. Tolkien\"],\n          [\"80-902734-1-6\", \"And Then There Were None\", \"Agatha Christie\"],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_headerless\"),\n        false,\n      },\n      \"Default multiline cells\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        [\n          [\"99921-58-10-7\", \"Divine\\nComedy\", \"Dante Alighieri\"],\n          [\"9971-5-0210-2\", \"Harry Potter\\nand the Chamber of Secrets\", \"Rowling\\nJoanne K.\"],\n          [\"9971-5-0210-2\", \"Harry Potter\\nand the Chamber of Secrets\", \"Rowling\\nJoanne K.\"],\n          [\"960-425-059-0\", \"The Lord of the Rings\", \"J. R. R.\\nTolkien\"],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_multiline_cells\"),\n        false,\n      },\n      \"Default no rows\" => {\n        [[\"ISBN\", \"Title\"]],\n        [] of String,\n        \"default\",\n        self.get_table_contents(\"default_no_rows\"),\n        false,\n      },\n      \"Default no rows or headers\" => {\n        [[] of String],\n        [] of String,\n        \"default\",\n        \"\",\n        false,\n      },\n      \"Default tags used for output formatting\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        [\n          [\"<info>99921-58-10-7</info>\", \"<error>Divine Comedy</error>\", \"<fg=blue;bg=white>Dante Alighieri</fg=blue;bg=white>\"],\n          [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"<info>Charles Dickens</>\"],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_formatting_tags\"),\n        false,\n      },\n      \"Default tags not used for output formatting\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        [\n          [\"<strong>99921-58-10-700</strong>\", \"<f>Divine Com</f>\", \"Dante Alighieri\"],\n          [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\"],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_non_formatting_tags\"),\n        false,\n      },\n      \"Default cells with colspan\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        [\n          [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n          ACON::Helper::Table::Separator.new,\n          [\n            ACON::Helper::Table::Cell.new(\"Divine Comedy(Dante Alighieri)\", colspan: 3),\n          ],\n          ACON::Helper::Table::Separator.new,\n          [\n            ACON::Helper::Table::Cell.new(\"Arduino: A Quick-Start Guide\", colspan: 2),\n            \"Mark Schmidt\",\n          ],\n          ACON::Helper::Table::Separator.new,\n          [\n            \"9971-5-0210-0\",\n            ACON::Helper::Table::Cell.new(\"A Tale of \\nTwo Cities\", colspan: 2),\n          ],\n          ACON::Helper::Table::Separator.new,\n          [\n            ACON::Helper::Table::Cell.new(\"Cupiditate dicta atque porro, tempora exercitationem modi animi nulla nemo vel nihil!\", colspan: 3),\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_colspan\"),\n        false,\n      },\n      \"Default cell after colspan contains line break\" => {\n        [[\"Foo\", \"Bar\", \"Baz\"]],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"foo\\nbar\", colspan: 2),\n            \"baz\\nqux\",\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_line_break_after_colspan_cell\"),\n        false,\n      },\n      \"Default cell after colspan contains multiple line breaks\" => {\n        [[\"Foo\", \"Bar\", \"Baz\"]],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"foo\\nbar\", colspan: 2),\n            \"baz\\nqux\\nquux\",\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_line_breaks_after_colspan_cell\"),\n        false,\n      },\n      \"Default cell with rowspan\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"9971-5-0210-0\", rowspan: 3),\n            ACON::Helper::Table::Cell.new(\"Divine Comedy\", rowspan: 2),\n            \"Dante Alighieri\",\n          ],\n          [] of String,\n          [\"The Lord of \\nthe Rings\", \"J. R. \\nR. Tolkien\"],\n          ACON::Helper::Table::Separator.new,\n          [\"80-902734-1-6\", ACON::Helper::Table::Cell.new(\"And Then \\nThere \\nWere None\", rowspan: 3), \"Agatha Christie\"],\n          [\"80-902734-1-7\", \"Test\"],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_rowspan\"),\n        false,\n      },\n      \"Default cell with rowspan and rowspan\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"9971-5-0210-0\", rowspan: 2, colspan: 2),\n            \"Dante Alighieri\",\n          ],\n          [\"Charles Dickens\"],\n          ACON::Helper::Table::Separator.new,\n          [\n            \"Dante Alighieri\",\n            ACON::Helper::Table::Cell.new(\"9971-5-0210-0\", rowspan: 3, colspan: 2),\n          ],\n          [\"J. R. R. Tolkien\"],\n          [\"J. R. R\"],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_rowspan_and_colspan\"),\n        false,\n      },\n      \"Default cell with rowspan and colspan that contain new lines\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"9971\\n-5-\\n021\\n0-0\", rowspan: 2, colspan: 2),\n            \"Dante Alighieri\",\n          ],\n          [\"Charles Dickens\"],\n          ACON::Helper::Table::Separator.new,\n          [\n            \"Dante Alighieri\",\n            ACON::Helper::Table::Cell.new(\"9971\\n-5-\\n021\\n0-0\", rowspan: 2, colspan: 2),\n          ],\n          [\"Charles Dickens\"],\n          ACON::Helper::Table::Separator.new,\n          [\n            ACON::Helper::Table::Cell.new(\"9971\\n-5-\\n021\\n0-0\", rowspan: 2, colspan: 2),\n            ACON::Helper::Table::Cell.new(\"Dante \\nAlighieri\", rowspan: 2, colspan: 1),\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_rowspan_and_colspan_and_line_breaks\"),\n        false,\n      },\n      \"Default cell with rowspan and colspan without table separators\" => {\n        [[\"ISBN\", \"Title\", \"Author\"]],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"9971\\n-5-\\n021\\n0-0\", rowspan: 2, colspan: 2),\n            \"Dante Alighieri\",\n          ],\n          [\"Charles Dickens\"],\n          [\n            \"Dante Alighieri\",\n            ACON::Helper::Table::Cell.new(\"9971\\n-5-\\n021\\n0-0\", rowspan: 2, colspan: 2),\n          ],\n          [\"Charles Dickens\"],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_rowspan_and_colspan_no_separators\"),\n        false,\n      },\n      \"Default cell with rowspan and colspan with separators inside a rowspan\" => {\n        [[\"ISBN\", \"Author\"]],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"9971-5-0210-0\", rowspan: 3, colspan: 1),\n            \"Dante Alighieri\",\n          ],\n          [ACON::Helper::Table::Separator.new],\n          [\"Charles Dickens\"],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_rowspan_and_colspan_separator_in_rowspan\"),\n        false,\n      },\n      \"Default cell with multiple header lines\" => {\n        [\n          [ACON::Helper::Table::Cell.new(\"Main title\", colspan: 3)],\n          [\"ISBN\", \"Title\", \"Author\"],\n        ],\n        [] of String,\n        \"default\",\n        self.get_table_contents(\"default_multiple_header_lines\"),\n        false,\n      },\n      \"Default row with multiple cells\" => {\n        [[] of String],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"1\", colspan: 3),\n            ACON::Helper::Table::Cell.new(\"2\", colspan: 2),\n            ACON::Helper::Table::Cell.new(\"3\", colspan: 2),\n            ACON::Helper::Table::Cell.new(\"4\", colspan: 2),\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_row_with_multiple_cells\"),\n        false,\n      },\n      \"Default colspan and table cells with comment style\" => {\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"<comment>Long Title</comment>\", colspan: 3),\n          ],\n        ],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"9971-5-0210-0\", colspan: 3),\n          ],\n          ACON::Helper::Table::Separator.new,\n          [\n            \"Dante Alighieri\",\n            \"J. R. R. Tolkien\",\n            \"J. R. R\",\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_colspan_and_table_cell_with_comment_style\"),\n        true,\n      },\n      \"Default row with formatted cells containing a newline\" => {\n        [[] of String],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"<error>Dont break\\nhere</error>\", colspan: 2),\n          ],\n          ACON::Helper::Table::Separator.new,\n          [\n            \"foo\",\n            ACON::Helper::Table::Cell.new(\"<error>Dont break\\nhere</error>\", rowspan: 2),\n          ],\n          [\n            \"bar\",\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_formatted_row_with_line_breaks\"),\n        true,\n      },\n      \"Default cells with rowspan and colspan with alignment\" => {\n        [\n          ACON::Helper::Table::Cell.new(\"ISBN\", style: ACON::Helper::Table::CellStyle.new(align: :right)),\n          \"Title\",\n          ACON::Helper::Table::Cell.new(\"Author\", style: ACON::Helper::Table::CellStyle.new(align: :center)),\n        ],\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"<fg=red>978</>\", style: ACON::Helper::Table::CellStyle.new(align: :center)),\n            \"De Monarchia\",\n            ACON::Helper::Table::Cell.new(\n              \"Dante Alighieri \\nspans multiple rows rows Dante Alighieri \\nspans multiple rows rows\",\n              rowspan: 2,\n              style: ACON::Helper::Table::CellStyle.new(align: :center)\n            ),\n          ],\n          [\n            \"<info>99921-58-10-7</info>\",\n            \"Divine Comedy\",\n          ],\n          ACON::Helper::Table::Separator.new,\n          [\n            ACON::Helper::Table::Cell.new(\"<error>test</error>\", colspan: 2, style: ACON::Helper::Table::CellStyle.new(align: :center)),\n            ACON::Helper::Table::Cell.new(\"tttt\", style: ACON::Helper::Table::CellStyle.new(align: :right)),\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_rowspan_and_colspan_and_alignment\"),\n        false,\n      },\n      \"Default cells with rowspan and colspan with fg,bg\" => {\n        [] of String,\n        [\n          [\n            ACON::Helper::Table::Cell.new(\"<fg=red>978</>\", style: ACON::Helper::Table::CellStyle.new(foreground: \"black\", background: \"green\")),\n            \"De Monarchia\",\n            ACON::Helper::Table::Cell.new(\"Dante Alighieri \\nspans multiple rows rows Dante Alighieri \\nspans multiple rows rows\", rowspan: 2, style: ACON::Helper::Table::CellStyle.new(foreground: \"red\", background: \"green\", align: :center)),\n          ],\n          [\n            \"<info>99921-58-10-7</info>\",\n            \"Divine Comedy\",\n          ],\n          ACON::Helper::Table::Separator.new,\n          [\n            ACON::Helper::Table::Cell.new(\"<error>test</error>\", colspan: 2, style: ACON::Helper::Table::CellStyle.new(foreground: \"red\", background: \"green\", align: :center)),\n            ACON::Helper::Table::Cell.new(\"tttt\", style: ACON::Helper::Table::CellStyle.new(foreground: \"red\", background: \"green\", align: :right)),\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_rowspan_and_colspan_and_fgbg\"),\n        true,\n      },\n      \"Default cells with rowspan and colspan > 1 with custom cell format\" => {\n        [\n          ACON::Helper::Table::Cell.new(\"ISBN\", style: ACON::Helper::Table::CellStyle.new(format: \"<fg=black;bg=cyan>%s</>\")),\n          \"Title\",\n          \"Author\",\n        ],\n        [\n          [\n            \"978-0521567817\",\n            \"De Monarchia\",\n            ACON::Helper::Table::Cell.new(\"Dante Alighieri\\nspans multiple rows\", rowspan: 2, style: ACON::Helper::Table::CellStyle.new(format: \"<info>%s</info>\")),\n          ],\n          [\"978-0804169127\", \"Divine Comedy\"],\n          [\n            ACON::Helper::Table::Cell.new(\"test\", colspan: 2, style: ACON::Helper::Table::CellStyle.new(format: \"<error>%s</error>\")),\n            \"tttt\",\n          ],\n        ],\n        \"default\",\n        self.get_table_contents(\"default_cells_with_rowspan_and_colspan_and_custom_format\"),\n        true,\n      },\n    }\n  end\n\n  # TODO: Enable when multi byte string widths are supported\n  def ptest_render_multi_byte : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .headers([\"🍝\"])\n      .rows([[1234]])\n      .style(\"default\")\n      .render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +------+\n    | 🍝   |\n    +------+\n    | 1234 |\n    +------+\n\n    TABLE\n  end\n\n  def test_render_table_cell_numeric_int_value : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .rows([[ACON::Helper::Table::Cell.new(1234)]])\n      .render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +------+\n    | 1234 |\n    +------+\n\n    TABLE\n  end\n\n  def test_render_table_cell_numeric_float_value : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .rows([[ACON::Helper::Table::Cell.new(3.14)]])\n      .render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +------+\n    | 3.14 |\n    +------+\n\n    TABLE\n  end\n\n  def test_render_custom_style : Nil\n    style = ACON::Helper::Table::Style.new\n    style\n      .horizontal_border_chars('.')\n      .vertical_border_chars('.')\n      .default_crossing_char('.')\n\n    ACON::Helper::Table.set_style_definition \"dotfull\", style\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .headers([\"Foo\"])\n      .rows([[\"Bar\"]])\n      .style(\"dotfull\")\n      .render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    .......\n    . Foo .\n    .......\n    . Bar .\n    .......\n\n    TABLE\n  end\n\n  def test_render_multiple_times : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .rows([[ACON::Helper::Table::Cell.new(\"foo\", colspan: 2)]])\n      .render\n\n    table.render\n    table.render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +----+---+\n    | foo    |\n    +----+---+\n    +----+---+\n    | foo    |\n    +----+---+\n    +----+---+\n    | foo    |\n    +----+---+\n\n    TABLE\n  end\n\n  def test_column_style : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .headers([\"ISBN\", \"Title\", \"Author\", \"Price\"])\n      .rows([\n        [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\", \"9.95\"],\n        [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"],\n      ])\n\n    style = ACON::Helper::Table::Style.new\n      .align(:right)\n\n    table.column_style 3, style\n    table.column_style(3).should eq style\n\n    table.render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +---------------+----------------------+-----------------+--------+\n    | ISBN          | Title                | Author          |  Price |\n    +---------------+----------------------+-----------------+--------+\n    | 99921-58-10-7 | Divine Comedy        | Dante Alighieri |   9.95 |\n    | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |\n    +---------------+----------------------+-----------------+--------+\n\n    TABLE\n  end\n\n  def test_column_width : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .headers([\"ISBN\", \"Title\", \"Author\", \"Price\"])\n      .rows([\n        [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\", \"9.95\"],\n        [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"],\n      ])\n      .column_width(0, 15)\n      .column_width(3, 10)\n\n    style = ACON::Helper::Table::Style.new\n      .align(:right)\n\n    table.column_style 3, style\n\n    table.render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +-----------------+----------------------+-----------------+------------+\n    | ISBN            | Title                | Author          |      Price |\n    +-----------------+----------------------+-----------------+------------+\n    | 99921-58-10-7   | Divine Comedy        | Dante Alighieri |       9.95 |\n    | 9971-5-0210-0   | A Tale of Two Cities | Charles Dickens |     139.25 |\n    +-----------------+----------------------+-----------------+------------+\n\n    TABLE\n  end\n\n  def test_column_widths : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .headers([\"ISBN\", \"Title\", \"Author\", \"Price\"])\n      .rows([\n        [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\", \"9.95\"],\n        [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"],\n      ])\n      .column_widths(15, 0, -1, 10)\n\n    style = ACON::Helper::Table::Style.new\n      .align(:right)\n\n    table.column_style 3, style\n\n    table.render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +-----------------+----------------------+-----------------+------------+\n    | ISBN            | Title                | Author          |      Price |\n    +-----------------+----------------------+-----------------+------------+\n    | 99921-58-10-7   | Divine Comedy        | Dante Alighieri |       9.95 |\n    | 9971-5-0210-0   | A Tale of Two Cities | Charles Dickens |     139.25 |\n    +-----------------+----------------------+-----------------+------------+\n\n    TABLE\n  end\n\n  def test_column_widths_enumerable : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .headers([\"ISBN\", \"Title\", \"Author\", \"Price\"])\n      .rows([\n        [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\", \"9.95\"],\n        [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"],\n      ])\n      .column_widths({15, 0, -1, 10})\n\n    style = ACON::Helper::Table::Style.new\n      .align(:right)\n\n    table.column_style 3, style\n\n    table.render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +-----------------+----------------------+-----------------+------------+\n    | ISBN            | Title                | Author          |      Price |\n    +-----------------+----------------------+-----------------+------------+\n    | 99921-58-10-7   | Divine Comedy        | Dante Alighieri |       9.95 |\n    | 9971-5-0210-0   | A Tale of Two Cities | Charles Dickens |     139.25 |\n    +-----------------+----------------------+-----------------+------------+\n\n    TABLE\n  end\n\n  def test_column_max_width : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n    table\n      .rows([\n        [\"Divine Comedy\", \"A Tale of Two Cities\", \"The Lord of the Rings\", \"And Then There Were None\"],\n      ])\n      .column_max_width(1, 5)\n      .column_max_width(2, 10)\n      .column_max_width(3, 15)\n      .render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +---------------+-------+------------+-----------------+\n    | Divine Comedy | A Tal | The Lord o | And Then There  |\n    |               | e of  | f the Ring | Were None       |\n    |               | Two C | s          |                 |\n    |               | ities |            |                 |\n    +---------------+-------+------------+-----------------+\n\n    TABLE\n  end\n\n  def test_column_max_width_with_headers : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n\n    table\n      .headers([\n        [\n          \"Publication\",\n          \"Very long header with a lot of information\",\n        ],\n      ])\n      .rows([\n        [\n          \"1954\",\n          \"The Lord of the Rings, by J.R.R. Tolkien\",\n        ],\n      ])\n      .column_max_width(1, 30)\n      .render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +-------------+--------------------------------+\n    | Publication | Very long header with a lot of |\n    |             | information                    |\n    +-------------+--------------------------------+\n    | 1954        | The Lord of the Rings, by J.R. |\n    |             | R. Tolkien                     |\n    +-------------+--------------------------------+\n\n    TABLE\n  end\n\n  def test_column_max_width_trailing_backslash : Nil\n    table = ACON::Helper::Table.new output = self.io_output\n\n    table\n      .rows([\n        [\"1234\\\\6\"],\n      ])\n      .column_max_width(0, 5)\n      .render\n\n    self.output_content(output).should eq self.normalize <<-'TABLE'\n    +-------+\n    | 1234\\ |\n    | 6     |\n    +-------+\n\n    TABLE\n  end\n\n  def test_render_max_width_colspan : Nil\n    ACON::Helper::Table.new(output = self.io_output)\n      .rows([\n        [ACON::Helper::Table::Cell.new(\"Lorem ipsum dolor sit amet, <fg=white;bg=green>consectetur</> adipiscing elit, <fg=white;bg=red>sed</> do <fg=white;bg=red>eiusmod</> tempor\", colspan: 3)],\n        ACON::Helper::Table::Separator.new,\n        [ACON::Helper::Table::Cell.new(\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor\", colspan: 3)],\n        ACON::Helper::Table::Separator.new,\n        [ACON::Helper::Table::Cell.new(\"Lorem ipsum <fg=white;bg=red>dolor</> sit amet, consectetur \", colspan: 2), \"hello world\"],\n        ACON::Helper::Table::Separator.new,\n        [\"hello <fg=white;bg=green>world</>\", ACON::Helper::Table::Cell.new(\"Lorem ipsum dolor sit amet, <fg=white;bg=green>consectetur</> adipiscing elit\", colspan: 2)],\n        ACON::Helper::Table::Separator.new,\n        [\"hello \", ACON::Helper::Table::Cell.new(\"world\", colspan: 1), \"Lorem ipsum dolor sit amet, consectetur\"],\n        ACON::Helper::Table::Separator.new,\n        [\"Athena \", ACON::Helper::Table::Cell.new(\"Test\", colspan: 1), \"Lorem <fg=white;bg=green>ipsum</> dolor sit amet, consectetur\"],\n      ])\n      .column_max_width(0, 15)\n      .column_max_width(1, 15)\n      .column_max_width(2, 15)\n      .render\n\n    self.output_content(output).should eq self.normalize <<-'TABLE'\n    +-----------------+-----------------+-----------------+\n    | Lorem ipsum dolor sit amet, consectetur adipi       |\n    | scing elit, sed do eiusmod tempor                   |\n    +-----------------+-----------------+-----------------+\n    | Lorem ipsum dolor sit amet, consectetur adipi       |\n    | scing elit, sed do eiusmod tempor                   |\n    +-----------------+-----------------+-----------------+\n    | Lorem ipsum dolor sit amet, co    | hello world     |\n    | nsectetur                         |                 |\n    +-----------------+-----------------+-----------------+\n    | hello world     | Lorem ipsum dolor sit amet, co    |\n    |                 | nsectetur adipiscing elit         |\n    +-----------------+-----------------+-----------------+\n    | hello           | world           | Lorem ipsum dol |\n    |                 |                 | or sit amet, co |\n    |                 |                 | nsectetur       |\n    +-----------------+-----------------+-----------------+\n    | Athena          | Test            | Lorem ipsum dol |\n    |                 |                 | or sit amet, co |\n    |                 |                 | nsectetur       |\n    +-----------------+-----------------+-----------------+\n\n    TABLE\n  end\n\n  def test_hyperlink_and_max_width : Nil\n    table = ACON::Helper::Table.new output = self.io_output true\n\n    table\n      .rows([\n        [\"<href=Lorem>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor</>\"],\n      ])\n      .column_max_width(0, 17)\n      .render\n\n    self.output_content(output).should eq <<-TABLE\n    +-------------------+\n    | \\e]8;;Lorem\\e\\\\Lorem ipsum dolor\\e]8;;\\e\\\\ |\n    | \\e]8;;Lorem\\e\\\\sit amet, consect\\e]8;;\\e\\\\ |\n    | \\e]8;;Lorem\\e\\\\etur adipiscing e\\e]8;;\\e\\\\ |\n    | \\e]8;;Lorem\\e\\\\lit, sed do eiusm\\e]8;;\\e\\\\ |\n    | \\e]8;;Lorem\\e\\\\od tempor\\e]8;;\\e\\\\         |\n    +-------------------+\n\n    TABLE\n  end\n\n  def test_append_row : Nil\n    sections = [] of ACON::Output::Section\n\n    output = self.io_output true\n\n    table = ACON::Helper::Table.new ACON::Output::Section.new output.io, sections, output.verbosity, output.decorated?, ACON::Formatter::Output.new\n\n    table\n      .headers([\"ISBN\", \"Title\", \"Author\", \"Price\"])\n      .rows([\n        [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\", \"9.95\"],\n      ])\n      .render\n\n    table.append_row [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"]\n    table.append_row \"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +---------------+---------------+-----------------+-------+\n    |\u001b[32m ISBN          \u001b[39m|\u001b[32m Title         \u001b[39m|\u001b[32m Author          \u001b[39m|\u001b[32m Price \u001b[39m|\n    +---------------+---------------+-----------------+-------+\n    | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 9.95  |\n    +---------------+---------------+-----------------+-------+\n    \u001b[5A\u001b[0J+---------------+----------------------+-----------------+--------+\n    |\u001b[32m ISBN          \u001b[39m|\u001b[32m Title                \u001b[39m|\u001b[32m Author          \u001b[39m|\u001b[32m Price  \u001b[39m|\n    +---------------+----------------------+-----------------+--------+\n    | 99921-58-10-7 | Divine Comedy        | Dante Alighieri | 9.95   |\n    | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |\n    +---------------+----------------------+-----------------+--------+\n    \u001b[6A\u001b[0J+---------------+----------------------+-----------------+--------+\n    |\u001b[32m ISBN          \u001b[39m|\u001b[32m Title                \u001b[39m|\u001b[32m Author          \u001b[39m|\u001b[32m Price  \u001b[39m|\n    +---------------+----------------------+-----------------+--------+\n    | 99921-58-10-7 | Divine Comedy        | Dante Alighieri | 9.95   |\n    | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |\n    | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |\n    +---------------+----------------------+-----------------+--------+\n\n    TABLE\n  end\n\n  def test_append_row_doesnt_clear_if_not_rendered : Nil\n    sections = [] of ACON::Output::Section\n\n    output = self.io_output true\n\n    table = ACON::Helper::Table.new ACON::Output::Section.new output.io, sections, output.verbosity, output.decorated?, ACON::Formatter::Output.new\n\n    table\n      .headers([\"ISBN\", \"Title\", \"Author\", \"Price\"])\n      .rows([\n        [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\", \"9.95\"],\n      ])\n\n    table.append_row \"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +---------------+----------------------+-----------------+--------+\n    |\u001b[32m ISBN          \u001b[39m|\u001b[32m Title                \u001b[39m|\u001b[32m Author          \u001b[39m|\u001b[32m Price  \u001b[39m|\n    +---------------+----------------------+-----------------+--------+\n    | 99921-58-10-7 | Divine Comedy        | Dante Alighieri | 9.95   |\n    | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |\n    +---------------+----------------------+-----------------+--------+\n\n    TABLE\n  end\n\n  def test_append_row_without_decoration : Nil\n    sections = [] of ACON::Output::Section\n\n    output = self.io_output\n\n    table = ACON::Helper::Table.new ACON::Output::Section.new output.io, sections, output.verbosity, output.decorated?, ACON::Formatter::Output.new\n\n    table\n      .headers([\"ISBN\", \"Title\", \"Author\", \"Price\"])\n      .rows([\n        [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\", \"9.95\"],\n      ])\n      .render\n\n    table.append_row \"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +---------------+---------------+-----------------+-------+\n    | ISBN          | Title         | Author          | Price |\n    +---------------+---------------+-----------------+-------+\n    | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 9.95  |\n    +---------------+---------------+-----------------+-------+\n    +---------------+----------------------+-----------------+--------+\n    | ISBN          | Title                | Author          | Price  |\n    +---------------+----------------------+-----------------+--------+\n    | 99921-58-10-7 | Divine Comedy        | Dante Alighieri | 9.95   |\n    | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |\n    +---------------+----------------------+-----------------+--------+\n\n    TABLE\n  end\n\n  def test_append_row_first_row : Nil\n    sections = [] of ACON::Output::Section\n\n    output = self.io_output true\n\n    table = ACON::Helper::Table.new ACON::Output::Section.new output.io, sections, output.verbosity, output.decorated?, ACON::Formatter::Output.new\n\n    table\n      .headers([\"ISBN\", \"Title\", \"Author\", \"Price\"])\n      .render\n\n    table.append_row \"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +------+-------+--------+-------+\n    |\u001b[32m ISBN \u001b[39m|\u001b[32m Title \u001b[39m|\u001b[32m Author \u001b[39m|\u001b[32m Price \u001b[39m|\n    +------+-------+--------+-------+\n    \u001b[3A\u001b[0J+---------------+----------------------+-----------------+--------+\n    |\u001b[32m ISBN          \u001b[39m|\u001b[32m Title                \u001b[39m|\u001b[32m Author          \u001b[39m|\u001b[32m Price  \u001b[39m|\n    +---------------+----------------------+-----------------+--------+\n    | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |\n    +---------------+----------------------+-----------------+--------+\n\n    TABLE\n  end\n\n  def test_append_row_no_section_output : Nil\n    table = ACON::Helper::Table.new self.io_output\n\n    expect_raises ACON::Exception::Logic, \"Appending a row is only supported when using a Athena::Console::Output::Section output, got Athena::Console::Output::IO.\" do\n      table.append_row \"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"\n    end\n  end\n\n  def test_missing_table_definition : Nil\n    table = ACON::Helper::Table.new self.io_output\n\n    expect_raises ACON::Exception::InvalidArgument, \"The table style 'absent' is not defined.\" do\n      table.style \"absent\"\n    end\n  end\n\n  def test_style_definition_missing : Nil\n    expect_raises ACON::Exception::InvalidArgument, \"The table style 'absent' is not defined.\" do\n      ACON::Helper::Table.style_definition \"absent\"\n    end\n  end\n\n  @[DataProvider(\"title_provider\")]\n  def test_render_titles(header_title : String, footer_title : String, style : String, expected : String) : Nil\n    ACON::Helper::Table.new(output = self.io_output)\n      .header_title(header_title)\n      .footer_title(footer_title)\n      .headers([\"ISBN\", \"Title\", \"Author\"])\n      .rows([\n        [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n        [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\"],\n        [\"960-425-059-0\", \"The Lord of the Rings\", \"J. R. R. Tolkien\"],\n        [\"80-902734-1-6\", \"And Then There Were None\", \"Agatha Christie\"],\n      ])\n      .style(style)\n      .render\n\n    self.output_content(output).should eq expected.gsub EOL, \"\\n\"\n  end\n\n  def title_provider : Tuple\n    {\n      {\n        \"Books\",\n        \"Page 1/2\",\n        \"default\",\n        <<-'TABLE'\n        +---------------+----------- Books --------+------------------+\n        | ISBN          | Title                    | Author           |\n        +---------------+--------------------------+------------------+\n        | 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n        | 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens  |\n        | 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n        | 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n        +---------------+--------- Page 1/2 -------+------------------+\n\n        TABLE\n      },\n      {\n        \"Multiline\\nheader\\nhere\",\n        \"footer\",\n        \"default\",\n        <<-'TABLE'\n        +---------------+--- Multiline\n        header\n        here +------------------+\n        | ISBN          | Title                    | Author           |\n        +---------------+--------------------------+------------------+\n        | 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n        | 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens  |\n        | 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n        | 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n        +---------------+---------- footer --------+------------------+\n\n        TABLE\n      },\n      {\n        \"Books\",\n        \"Page 1/2\",\n        \"box\",\n        <<-'TABLE'\n        ┌───────────────┬─────────── Books ────────┬──────────────────┐\n        │ ISBN          │ Title                    │ Author           │\n        ├───────────────┼──────────────────────────┼──────────────────┤\n        │ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  │\n        │ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  │\n        │ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien │\n        │ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  │\n        └───────────────┴───────── Page 1/2 ───────┴──────────────────┘\n\n        TABLE\n      },\n      {\n        \"Boooooooooooooooooooooooooooooooooooooooooooooooooooooooks\",\n        \"Page 1/999999999999999999999999999999999999999999999999999\",\n        \"default\",\n        <<-'TABLE'\n        +- Booooooooooooooooooooooooooooooooooooooooooooooooooooo... -+\n        | ISBN          | Title                    | Author           |\n        +---------------+--------------------------+------------------+\n        | 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n        | 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens  |\n        | 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n        | 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n        +- Page 1/99999999999999999999999999999999999999999999999... -+\n\n        TABLE\n      },\n    }\n  end\n\n  def test_render_titles_no_headers : Nil\n    ACON::Helper::Table.new(output = self.io_output)\n      .header_title(\"Reproducer\")\n      .rows([\n        [\"Value\", \"123-456\"],\n        [\"Some other value\", \"789-0\"],\n      ])\n      .render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    +-------- Reproducer --------+\n    | Value            | 123-456 |\n    | Some other value | 789-0   |\n    +------------------+---------+\n\n    TABLE\n  end\n\n  def test_box_style_with_colspan : Nil\n    boxed = ACON::Helper::Table::Style.new\n      .horizontal_border_chars('─')\n      .vertical_border_chars('│')\n      .crossing_chars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├')\n\n    ACON::Helper::Table.new(output = self.io_output)\n      .style(boxed)\n      .headers(\"ISBN\", \"Title\", \"Author\")\n      .rows([\n        [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n        ACON::Helper::Table::Separator.new,\n        [ACON::Helper::Table::Cell.new(\"This value spans 3 columns.\", colspan: 3)],\n      ])\n      .render\n\n    self.output_content(output).should eq self.normalize <<-TABLE\n    ┌───────────────┬───────────────┬─────────────────┐\n    │ ISBN          │ Title         │ Author          │\n    ├───────────────┼───────────────┼─────────────────┤\n    │ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │\n    ├───────────────┼───────────────┼─────────────────┤\n    │ This value spans 3 columns.                     │\n    └───────────────┴───────────────┴─────────────────┘\n\n    TABLE\n  end\n\n  @[DataProvider(\"horizontal_provider\")]\n  def test_render_horizontal(headers, rows, expected)\n    ACON::Helper::Table.new(output = self.io_output)\n      .headers(headers)\n      .rows(rows)\n      .horizontal\n      .render\n\n    self.output_content(output).should eq expected.gsub EOL, \"\\n\"\n  end\n\n  def horizontal_provider : Tuple\n    {\n      {\n        %w(foo bar baz),\n        [\n          %w(one two tree),\n          %w(1 2 3),\n        ],\n        <<-'TABLE'\n        +-----+------+---+\n        | foo | one  | 1 |\n        | bar | two  | 2 |\n        | baz | tree | 3 |\n        +-----+------+---+\n\n        TABLE\n      },\n      {\n        %w(foo bar baz),\n        [\n          %w(one two),\n          %w(1),\n        ],\n        <<-'TABLE'\n        +-----+-----+---+\n        | foo | one | 1 |\n        | bar | two |   |\n        | baz |     |   |\n        +-----+-----+---+\n\n        TABLE\n      },\n      {\n        %w(foo bar baz),\n        [\n          %w(one two tree),\n          ACON::Helper::Table::Separator.new,\n          %w(1 2 3),\n        ],\n        <<-'TABLE'\n        +-----+------+---+\n        | foo | one  | 1 |\n        | bar | two  | 2 |\n        | baz | tree | 3 |\n        +-----+------+---+\n\n        TABLE\n      },\n    }\n  end\n\n  @[DataProvider(\"vertical_provider\")]\n  def test_render_vertical(headers, rows, expected, style : String, header_title, footer_title)\n    ACON::Helper::Table.new(output = self.io_output)\n      .headers(headers)\n      .rows(rows)\n      .style(style)\n      .header_title(header_title)\n      .footer_title(footer_title)\n      .vertical\n      .render\n\n    self.output_content(output).should eq expected.gsub EOL, \"\\n\"\n  end\n\n  def vertical_provider : Hash\n    books = [\n      [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\", \"9.95\"],\n      [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\", \"139.25\"],\n    ]\n\n    {\n      \"With header for all\" => {\n        %w(ISBN Title Author Price),\n        books,\n        <<-'TABLE',\n        +------------------------------+\n        |   ISBN: 99921-58-10-7        |\n        |  Title: Divine Comedy        |\n        | Author: Dante Alighieri      |\n        |  Price: 9.95                 |\n        |------------------------------|\n        |   ISBN: 9971-5-0210-0        |\n        |  Title: A Tale of Two Cities |\n        | Author: Charles Dickens      |\n        |  Price: 139.25               |\n        +------------------------------+\n\n        TABLE\n        \"default\",\n        nil,\n        nil,\n      },\n      \"With header for none\" => {\n        %w(),\n        books,\n        <<-'TABLE',\n        +----------------------+\n        | 99921-58-10-7        |\n        | Divine Comedy        |\n        | Dante Alighieri      |\n        | 9.95                 |\n        |----------------------|\n        | 9971-5-0210-0        |\n        | A Tale of Two Cities |\n        | Charles Dickens      |\n        | 139.25               |\n        +----------------------+\n\n        TABLE\n        \"default\",\n        nil,\n        nil,\n      },\n      \"With header for some\" => {\n        %w(ISBN Title Author),\n        books,\n        <<-'TABLE',\n        +------------------------------+\n        |   ISBN: 99921-58-10-7        |\n        |  Title: Divine Comedy        |\n        | Author: Dante Alighieri      |\n        |       : 9.95                 |\n        |------------------------------|\n        |   ISBN: 9971-5-0210-0        |\n        |  Title: A Tale of Two Cities |\n        | Author: Charles Dickens      |\n        |       : 139.25               |\n        +------------------------------+\n\n        TABLE\n        \"default\",\n        nil,\n        nil,\n      },\n      \"With row for some headers\" => {\n        %w(foo bar baz),\n        [\n          %w(one two),\n          %w(1),\n        ],\n        <<-'TABLE',\n        +----------+\n        | foo: one |\n        | bar: two |\n        | baz:     |\n        |----------|\n        | foo: 1   |\n        | bar:     |\n        | baz:     |\n        +----------+\n\n        TABLE\n        \"default\",\n        nil,\n        nil,\n      },\n      \"With table separator\" => {\n        %w(foo bar baz),\n        [\n          %w(one two tree),\n          ACON::Helper::Table::Separator.new,\n          %w(1 2 3),\n        ],\n        <<-'TABLE',\n        +-----------+\n        | foo: one  |\n        | bar: two  |\n        | baz: tree |\n        |-----------|\n        | foo: 1    |\n        | bar: 2    |\n        | baz: 3    |\n        +-----------+\n\n        TABLE\n        \"default\",\n        nil,\n        nil,\n      },\n      \"With line breaks\" => {\n        %w(ISBN Title Author Price),\n        [\n          [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\", \"9.95\"],\n          [\"9971-5-0210-0\", \"A Tale\\nof Two Cities\", \"Charles Dickens\", \"139.25\"],\n        ],\n        <<-'TABLE',\n        +-------------------------+\n        |   ISBN: 99921-58-10-7   |\n        |  Title: Divine Comedy   |\n        | Author: Dante Alighieri |\n        |  Price: 9.95            |\n        |-------------------------|\n        |   ISBN: 9971-5-0210-0   |\n        |  Title: A Tale          |\n        |         of Two Cities   |\n        | Author: Charles Dickens |\n        |  Price: 139.25          |\n        +-------------------------+\n\n        TABLE\n        \"default\",\n        nil,\n        nil,\n      },\n      \"With formatting tags\" => {\n        %w(ISBN Title Author),\n        [\n          [\"<info>99921-58-10-7</info>\", \"<error>Divine Comedy</error>\", \"<fg=blue;bg=white>Dante Alighieri</fg=blue;bg=white>\"],\n          [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"<info>Charles Dickens</>\"],\n        ],\n        <<-'TABLE',\n        +------------------------------+\n        |   ISBN: 99921-58-10-7        |\n        |  Title: Divine Comedy        |\n        | Author: Dante Alighieri      |\n        |------------------------------|\n        |   ISBN: 9971-5-0210-0        |\n        |  Title: A Tale of Two Cities |\n        | Author: Charles Dickens      |\n        +------------------------------+\n\n        TABLE\n        \"default\",\n        nil,\n        nil,\n      },\n      \"With colspan\" => {\n        %w(ISBN Title Author),\n        [\n          [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n          [ACON::Helper::Table::Cell.new(\"Cupiditate dicta atque porro, tempora exercitationem modi animi nulla nemo vel nihil!\", colspan: 3)],\n          [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\"],\n        ],\n        <<-'TABLE',\n        +---------------------------------------------------------------------------------------+\n        |   ISBN: 99921-58-10-7                                                                 |\n        |  Title: Divine Comedy                                                                 |\n        | Author: Dante Alighieri                                                               |\n        |---------------------------------------------------------------------------------------|\n        | Cupiditate dicta atque porro, tempora exercitationem modi animi nulla nemo vel nihil! |\n        |---------------------------------------------------------------------------------------|\n        |   ISBN: 9971-5-0210-0                                                                 |\n        |  Title: A Tale of Two Cities                                                          |\n        | Author: Charles Dickens                                                               |\n        +---------------------------------------------------------------------------------------+\n\n        TABLE\n        \"default\",\n        nil,\n        nil,\n      },\n      \"With colspans but no header\" => {\n        %w(),\n        [\n          [ACON::Helper::Table::Cell.new(\"Lorem ipsum dolor sit amet, <fg=white;bg=green>consectetur</> adipiscing elit, <fg=white;bg=red>sed</> do <fg=white;bg=red>eiusmod</> tempor\", colspan: 3)],\n          ACON::Helper::Table::Separator.new,\n          [ACON::Helper::Table::Cell.new(\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor\", colspan: 3)],\n          ACON::Helper::Table::Separator.new,\n          [ACON::Helper::Table::Cell.new(\"Lorem ipsum <fg=white;bg=red>dolor</> sit amet, consectetur \", colspan: 2), \"hello world\"],\n          ACON::Helper::Table::Separator.new,\n          [\"hello <fg=white;bg=green>world</>\", ACON::Helper::Table::Cell.new(\"Lorem ipsum dolor sit amet, <fg=white;bg=green>consectetur</> adipiscing elit\", colspan: 2)],\n          ACON::Helper::Table::Separator.new,\n          [\"hello \", ACON::Helper::Table::Cell.new(\"world\", colspan: 1), \"Lorem ipsum dolor sit amet, consectetur\"],\n          ACON::Helper::Table::Separator.new,\n          [\"Symfony \", ACON::Helper::Table::Cell.new(\"Test\", colspan: 1), \"Lorem <fg=white;bg=green>ipsum</> dolor sit amet, consectetur\"],\n        ],\n        <<-'TABLE',\n        +--------------------------------------------------------------------------------+\n        | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor |\n        |--------------------------------------------------------------------------------|\n        | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor |\n        |--------------------------------------------------------------------------------|\n        | Lorem ipsum dolor sit amet, consectetur                                        |\n        | hello world                                                                    |\n        |--------------------------------------------------------------------------------|\n        | hello world                                                                    |\n        | Lorem ipsum dolor sit amet, consectetur adipiscing elit                        |\n        |--------------------------------------------------------------------------------|\n        | hello                                                                          |\n        | world                                                                          |\n        | Lorem ipsum dolor sit amet, consectetur                                        |\n        |--------------------------------------------------------------------------------|\n        | Symfony                                                                        |\n        | Test                                                                           |\n        | Lorem ipsum dolor sit amet, consectetur                                        |\n        +--------------------------------------------------------------------------------+\n\n        TABLE\n        \"default\",\n        nil,\n        nil,\n      },\n      \"Borderless style\" => {\n        %w(ISBN Title Author Price),\n        books,\n        self.get_table_contents(\"borderless_vertical\"),\n        \"borderless\",\n        nil,\n        nil,\n      },\n      \"Compact style\" => {\n        %w(ISBN Title Author Price),\n        books,\n        self.get_table_contents(\"compact_vertical\"),\n        \"compact\",\n        nil,\n        nil,\n      },\n      \"Suggested style\" => {\n        %w(ISBN Title Author Price),\n        books,\n        self.get_table_contents(\"suggested_vertical\"),\n        \"suggested\",\n        nil,\n        nil,\n      },\n      \"Box style\" => {\n        %w(ISBN Title Author Price),\n        books,\n        <<-'TABLE',\n        ┌──────────────────────────────┐\n        │   ISBN: 99921-58-10-7        │\n        │  Title: Divine Comedy        │\n        │ Author: Dante Alighieri      │\n        │  Price: 9.95                 │\n        │──────────────────────────────│\n        │   ISBN: 9971-5-0210-0        │\n        │  Title: A Tale of Two Cities │\n        │ Author: Charles Dickens      │\n        │  Price: 139.25               │\n        └──────────────────────────────┘\n\n        TABLE\n        \"box\",\n        nil,\n        nil,\n      },\n      \"Double box style\" => {\n        %w(ISBN Title Author Price),\n        books,\n        <<-'TABLE',\n        ╔══════════════════════════════╗\n        ║   ISBN: 99921-58-10-7        ║\n        ║  Title: Divine Comedy        ║\n        ║ Author: Dante Alighieri      ║\n        ║  Price: 9.95                 ║\n        ║──────────────────────────────║\n        ║   ISBN: 9971-5-0210-0        ║\n        ║  Title: A Tale of Two Cities ║\n        ║ Author: Charles Dickens      ║\n        ║  Price: 139.25               ║\n        ╚══════════════════════════════╝\n\n        TABLE\n        \"double-box\",\n        nil,\n        nil,\n      },\n      \"With titles\" => {\n        %w(ISBN Title Author Price),\n        books,\n        <<-'TABLE',\n        +----------- Books ------------+\n        |   ISBN: 99921-58-10-7        |\n        |  Title: Divine Comedy        |\n        | Author: Dante Alighieri      |\n        |  Price: 9.95                 |\n        |------------------------------|\n        |   ISBN: 9971-5-0210-0        |\n        |  Title: A Tale of Two Cities |\n        | Author: Charles Dickens      |\n        |  Price: 139.25               |\n        +---------- Page 1/2 ----------+\n\n        TABLE\n        \"default\",\n        \"Books\",\n        \"Page 1/2\",\n      },\n    }\n  end\n\n  private def output_content(output : ACON::Output::IO) : String\n    self.normalize output.to_s\n  end\n\n  private def io_output(decorated : Bool = false) : ACON::Output::IO\n    ACON::Output::IO.new @output, decorated: decorated\n  end\n\n  private def normalize(input : String) : String\n    input.gsub EOL, \"\\n\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/helper/table_style_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct TableStyleSpec < ASPEC::TestCase\n  def test_getter_setters : Nil\n    style = ACON::Helper::Table::Style\n      .new\n      .align(:right)\n      .border_format(\"BF\")\n      .padding_char('c')\n      .header_title_format(\"HTF\")\n      .footer_title_format(\"FTF\")\n      .cell_header_format(\"CHF\")\n      .cell_row_format(\"CRF\")\n      .cell_row_content_format(\"CRCF\")\n      .horizontal_border_chars('o', 'i')\n      .vertical_border_chars('v', 'u')\n      .default_crossing_char('x')\n\n    style.align.should eq ACON::Helper::Table::Alignment::RIGHT\n    style.border_format.should eq \"BF\"\n    style.padding_char.should eq 'c'\n    style.header_title_format.should eq \"HTF\"\n    style.footer_title_format.should eq \"FTF\"\n    style.cell_header_format.should eq \"CHF\"\n    style.cell_row_format.should eq \"CRF\"\n    style.cell_row_content_format.should eq \"CRCF\"\n    style.border_chars.should eq({\"o\", \"v\", \"i\", \"u\"})\n    style.crossing_chars.should eq({\"x\", \"x\", \"x\", \"x\", \"x\", \"x\", \"x\", \"x\", \"x\", \"x\", \"x\", \"x\"})\n\n    style.crossing_chars(\"c\", \"tl\", \"tm\", \"tr\", \"mr\", \"br\", \"bm\", \"bl\", \"ml\", \"tlb\", \"tmb\", \"trb\")\n    style.crossing_chars.should eq({\"c\", \"tl\", \"tm\", \"tr\", \"mr\", \"br\", \"bm\", \"bl\", \"ml\", \"tlb\", \"tmb\", \"trb\"})\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/argument_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ACON::Input::Argument do\n  describe \".new\" do\n    it \"disallows blank names\" do\n      expect_raises ACON::Exception::InvalidArgument, \"An argument name cannot be blank.\" do\n        ACON::Input::Argument.new \"\"\n      end\n\n      expect_raises ACON::Exception::InvalidArgument, \"An argument name cannot be blank.\" do\n        ACON::Input::Argument.new \"   \"\n      end\n    end\n  end\n\n  describe \"#default=\" do\n    describe \"when the argument is required\" do\n      it \"raises if not nil\" do\n        argument = ACON::Input::Argument.new \"foo\", :required\n\n        expect_raises ACON::Exception::Logic, \"Cannot set a default value when the argument is required.\" do\n          argument.default = \"bar\"\n        end\n      end\n\n      it \"allows nil\" do\n        ACON::Input::Argument.new(\"foo\", :required).default = nil\n      end\n    end\n\n    describe \"array\" do\n      it \"nil value\" do\n        argument = ACON::Input::Argument.new \"foo\", ACON::Input::Argument::Mode[:optional, :is_array]\n        argument.default = nil\n        argument.default.should eq [] of String\n      end\n\n      it \"non array\" do\n        argument = ACON::Input::Argument.new \"foo\", ACON::Input::Argument::Mode[:optional, :is_array]\n\n        expect_raises ACON::Exception::Logic, \"Default value for an array argument must be an array.\" do\n          argument.default = \"bar\"\n        end\n      end\n    end\n  end\n\n  describe \"#complete\" do\n    it \"with an array\" do\n      values = [\"foo\", \"bar\"]\n      suggestions = ACON::Completion::Suggestions.new\n\n      argument = ACON::Input::Argument.new \"foo\", suggested_values: values\n\n      argument.has_completion?.should be_true\n\n      argument.complete ACON::Completion::Input.new, suggestions\n\n      suggestions.suggested_values.map(&.value).should eq [\"foo\", \"bar\"]\n    end\n\n    it \"with an block\" do\n      values = [\"foo\", \"bar\"]\n      suggestions = ACON::Completion::Suggestions.new\n      callback = Proc(ACON::Completion::Input, Array(String)).new { values }\n\n      argument = ACON::Input::Argument.new \"foo\", suggested_values: callback\n\n      argument.has_completion?.should be_true\n\n      argument.complete ACON::Completion::Input.new, suggestions\n\n      suggestions.suggested_values.map(&.value).should eq [\"foo\", \"bar\"]\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/argv_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct ARGVTest < ASPEC::TestCase\n  def test_parse : Nil\n    input = ACON::Input::ARGV.new [\"foo\"]\n\n    input.bind ACON::Input::Definition.new ACON::Input::Argument.new \"name\"\n    input.arguments.should eq({\"name\" => \"foo\"})\n\n    input.bind ACON::Input::Definition.new ACON::Input::Argument.new \"name\"\n    input.arguments.should eq({\"name\" => \"foo\"})\n  end\n\n  def test_array_argument : Nil\n    input = ACON::Input::ARGV.new [\"foo\", \"bar\", \"baz\", \"bat\"]\n    input.bind ACON::Input::Definition.new ACON::Input::Argument.new \"name\", :is_array\n\n    input.arguments.should eq({\"name\" => [\"foo\", \"bar\", \"baz\", \"bat\"]})\n  end\n\n  def test_array_option : Nil\n    input = ACON::Input::ARGV.new [\"--name=foo\", \"--name=bar\", \"--name=baz\"]\n    input.bind ACON::Input::Definition.new ACON::Input::Option.new \"name\", value_mode: ACON::Input::Option::Value[:optional, :is_array]\n    input.options.should eq({\"name\" => [\"foo\", \"bar\", \"baz\"]})\n\n    input = ACON::Input::ARGV.new [\"--name\", \"foo\", \"--name\", \"bar\", \"--name\", \"baz\"]\n    input.bind ACON::Input::Definition.new ACON::Input::Option.new \"name\", value_mode: ACON::Input::Option::Value[:optional, :is_array]\n    input.options.should eq({\"name\" => [\"foo\", \"bar\", \"baz\"]})\n\n    input = ACON::Input::ARGV.new [\"--name=foo\", \"--name=bar\", \"--name=\"]\n    input.bind ACON::Input::Definition.new ACON::Input::Option.new \"name\", value_mode: ACON::Input::Option::Value[:optional, :is_array]\n    input.options.should eq({\"name\" => [\"foo\", \"bar\", \"\"]})\n\n    input = ACON::Input::ARGV.new [\"--name=foo\", \"--name=bar\", \"--name\", \"--anotherOption\"]\n    input.bind ACON::Input::Definition.new(\n      ACON::Input::Option.new(\"name\", value_mode: ACON::Input::Option::Value[:optional, :is_array]),\n      ACON::Input::Option.new(\"anotherOption\", value_mode: :none),\n    )\n    input.options.should eq({\"name\" => [\"foo\", \"bar\", nil], \"anotherOption\" => true})\n  end\n\n  def test_parse_negative_number_after_double_dash : Nil\n    input = ACON::Input::ARGV.new [\"--\", \"-1\"]\n    input.bind ACON::Input::Definition.new ACON::Input::Argument.new \"number\"\n    input.arguments.should eq({\"number\" => \"-1\"})\n\n    input = ACON::Input::ARGV.new [\"-f\", \"bar\", \"--\", \"-1\"]\n    input.bind ACON::Input::Definition.new(\n      ACON::Input::Argument.new(\"number\"),\n      ACON::Input::Option.new(\"foo\", \"f\", :optional),\n    )\n\n    input.options.should eq({\"foo\" => \"bar\"})\n    input.arguments.should eq({\"number\" => \"-1\"})\n  end\n\n  def test_parse_empty_string_argument : Nil\n    input = ACON::Input::ARGV.new [\"-f\", \"bar\", \"\"]\n    input.bind ACON::Input::Definition.new(\n      ACON::Input::Argument.new(\"empty\"),\n      ACON::Input::Option.new(\"foo\", \"f\", :optional),\n    )\n\n    input.options.should eq({\"foo\" => \"bar\"})\n    input.arguments.should eq({\"empty\" => \"\"})\n  end\n\n  @[DataProvider(\"parse_options_provider\")]\n  def test_parse_options(input_args : Array(String), options : Array(ACON::Input::Option | ACON::Input::Argument), expected : Hash) : Nil\n    input = ACON::Input::ARGV.new input_args\n    input.bind ACON::Input::Definition.new options\n\n    input.options.should eq expected\n  end\n\n  def parse_options_provider : Hash\n    {\n      \"long options without a value\" => {\n        [\"--foo\"],\n        [ACON::Input::Option.new(\"foo\")],\n        {\"foo\" => true},\n      },\n      \"long options with a required value (with a = separator)\" => {\n        [\"--foo=bar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :required)],\n        {\"foo\" => \"bar\"},\n      },\n      \"long options with a required value (with a space separator)\" => {\n        [\"--foo\", \"bar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :required)],\n        {\"foo\" => \"bar\"},\n      },\n      \"long options with optional value which is empty (with a = separator) as empty string\" => {\n        [\"--foo=\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional)],\n        {\"foo\" => \"\"},\n      },\n      \"long options with optional value without value specified or an empty string (with a = separator) followed by an argument as empty string\" => {\n        [\"--foo=\", \"bar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional), ACON::Input::Argument.new(\"name\", :required)],\n        {\"foo\" => \"\"},\n      },\n      \"long options with optional value which is empty (with a = separator) preceded by an argument\" => {\n        [\"bar\", \"--foo\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional), ACON::Input::Argument.new(\"name\", :required)],\n        {\"foo\" => nil},\n      },\n      \"long options with optional value which is empty as empty string even followed by an argument\" => {\n        [\"--foo\", \"\", \"bar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional), ACON::Input::Argument.new(\"name\", :required)],\n        {\"foo\" => \"\"},\n      },\n      \"long options with optional value specified with no separator and no value as nil\" => {\n        [\"--foo\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional)],\n        {\"foo\" => nil},\n      },\n      \"short options without a value\" => {\n        [\"-f\"],\n        [ACON::Input::Option.new(\"foo\", \"f\")],\n        {\"foo\" => true},\n      },\n      \"short options with a required value (with no separator)\" => {\n        [\"-fbar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :required)],\n        {\"foo\" => \"bar\"},\n      },\n      \"short options with a required value (with a space separator)\" => {\n        [\"-f\", \"bar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :required)],\n        {\"foo\" => \"bar\"},\n      },\n      \"short options with an optional empty value\" => {\n        [\"-f\", \"\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional)],\n        {\"foo\" => \"\"},\n      },\n      \"short options with an optional empty value followed by an argument\" => {\n        [\"-f\", \"\", \"foo\"],\n        [ACON::Input::Argument.new(\"name\"), ACON::Input::Option.new(\"foo\", \"f\", :optional)],\n        {\"foo\" => \"\"},\n      },\n      \"short options with an optional empty value followed by an option\" => {\n        [\"-f\", \"\", \"-b\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional), ACON::Input::Option.new(\"bar\", \"b\")],\n        {\"foo\" => \"\", \"bar\" => true},\n      },\n      \"short options with an optional value which is not present\" => {\n        [\"-f\", \"-b\", \"foo\"],\n        [ACON::Input::Argument.new(\"name\"), ACON::Input::Option.new(\"foo\", \"f\", :optional), ACON::Input::Option.new(\"bar\", \"b\")],\n        {\"foo\" => nil, \"bar\" => true},\n      },\n      \"short options when they are aggregated as a single one\" => {\n        [\"-fb\"],\n        [ACON::Input::Option.new(\"foo\", \"f\"), ACON::Input::Option.new(\"bar\", \"b\")],\n        {\"foo\" => true, \"bar\" => true},\n      },\n      \"short options when they are aggregated as a single one and the last one has a required value\" => {\n        [\"-fb\", \"bar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\"), ACON::Input::Option.new(\"bar\", \"b\", :required)],\n        {\"foo\" => true, \"bar\" => \"bar\"},\n      },\n      \"short options when they are aggregated as a single one and the last one has an optional value\" => {\n        [\"-fb\", \"bar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\"), ACON::Input::Option.new(\"bar\", \"b\", :optional)],\n        {\"foo\" => true, \"bar\" => \"bar\"},\n      },\n      \"short options when they are aggregated as a single one and the last one has an optional value with no separator\" => {\n        [\"-fbbar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\"), ACON::Input::Option.new(\"bar\", \"b\", :optional)],\n        {\"foo\" => true, \"bar\" => \"bar\"},\n      },\n      \"short options when they are aggregated as a single one and one of them takes a value\" => {\n        [\"-fbbar\"],\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional), ACON::Input::Option.new(\"bar\", \"b\", :optional)],\n        {\"foo\" => \"bbar\", \"bar\" => nil},\n      },\n    }\n  end\n\n  @[DataProvider(\"parse_options_negatable_provider\")]\n  def test_parse_options_negatble(input_args : Array(String), options : Array(ACON::Input::Option | ACON::Input::Argument), expected : Hash) : Nil\n    input = ACON::Input::ARGV.new input_args\n    input.bind ACON::Input::Definition.new options\n\n    input.options.should eq expected\n  end\n\n  def parse_options_negatable_provider : Hash\n    {\n      \"long options without a value - negatable\" => {\n        [\"--foo\"],\n        [ACON::Input::Option.new(\"foo\", value_mode: :negatable)],\n        {\"foo\" => true},\n      },\n      \"long options without a value - no value negatable\" => {\n        [\"--foo\"],\n        [ACON::Input::Option.new(\"foo\", value_mode: ACON::Input::Option::Value[:none, :negatable])],\n        {\"foo\" => true},\n      },\n      \"negated long options without a value - negatable\" => {\n        [\"--no-foo\"],\n        [ACON::Input::Option.new(\"foo\", value_mode: :negatable)],\n        {\"foo\" => false},\n      },\n      \"negated long options without a value - no value negatable\" => {\n        [\"--no-foo\"],\n        [ACON::Input::Option.new(\"foo\", value_mode: ACON::Input::Option::Value[:none, :negatable])],\n        {\"foo\" => false},\n      },\n      \"missing negated option uses default - negatable\" => {\n        [] of String,\n        [ACON::Input::Option.new(\"foo\", value_mode: :negatable)],\n        {\"foo\" => nil},\n      },\n      \"missing negated option uses default - no value negatable\" => {\n        [] of String,\n        [ACON::Input::Option.new(\"foo\", value_mode: ACON::Input::Option::Value[:none, :negatable])],\n        {\"foo\" => nil},\n      },\n      \"missing negated option uses default - bool default\" => {\n        [] of String,\n        [ACON::Input::Option.new(\"foo\", value_mode: :negatable, default: false)],\n        {\"foo\" => false},\n      },\n    }\n  end\n\n  def test_to_s : Nil\n    input = ACON::Input::ARGV.new \"-b\", \"bar\"\n    input.to_s.should eq \"-b bar\"\n  end\n\n  def test_to_s_complex : Nil\n    input = ACON::Input::ARGV.new \"-f\", \"--bar=foo\", \"a b c d\", \"A\\nB'C\"\n\n    {% if flag? :windows %}\n      input.to_s.should eq \"-f --bar=foo \\\"a b c d\\\" A\\nB'C\"\n    {% else %}\n      input.to_s.should eq \"-f --bar=foo 'a b c d' 'A\\nB'\\\"'\\\"'C'\"\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/definition_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct InputDefinitionTest < ASPEC::TestCase\n  getter arg_foo : ACON::Input::Argument { ACON::Input::Argument.new \"foo\" }\n  getter arg_foo1 : ACON::Input::Argument { ACON::Input::Argument.new \"foo\" }\n  getter arg_foo2 : ACON::Input::Argument { ACON::Input::Argument.new \"foo2\", :required }\n  getter arg_bar : ACON::Input::Argument { ACON::Input::Argument.new \"bar\" }\n\n  getter opt_foo : ACON::Input::Option { ACON::Input::Option.new \"foo\", \"f\" }\n  getter opt_foo1 : ACON::Input::Option { ACON::Input::Option.new \"foobar\", \"f\" }\n  getter opt_foo2 : ACON::Input::Option { ACON::Input::Option.new \"foo\", \"p\" }\n  getter opt_bar : ACON::Input::Option { ACON::Input::Option.new \"bar\", \"b\" }\n  getter opt_multi : ACON::Input::Option { ACON::Input::Option.new \"multi\", \"m|mm|mmm\" }\n\n  def test_new_arguments : Nil\n    definition = ACON::Input::Definition.new\n    definition.arguments.should be_empty\n\n    # Splat\n    definition = ACON::Input::Definition.new self.arg_foo, self.arg_bar\n    definition.arguments.should eq({\"foo\" => self.arg_foo, \"bar\" => self.arg_bar})\n\n    # Array\n    definition = ACON::Input::Definition.new [self.arg_foo, self.arg_bar]\n    definition.arguments.should eq({\"foo\" => self.arg_foo, \"bar\" => self.arg_bar})\n\n    # Hash\n    definition = ACON::Input::Definition.new({\"foo\" => self.arg_foo, \"bar\" => self.arg_bar})\n    definition.arguments.should eq({\"foo\" => self.arg_foo, \"bar\" => self.arg_bar})\n  end\n\n  def test_new_options : Nil\n    definition = ACON::Input::Definition.new\n    definition.options.should be_empty\n\n    # Splat\n    definition = ACON::Input::Definition.new self.opt_foo, self.opt_bar\n    definition.options.should eq({\"foo\" => self.opt_foo, \"bar\" => self.opt_bar})\n\n    # Array\n    definition = ACON::Input::Definition.new [self.opt_foo, self.opt_bar]\n    definition.options.should eq({\"foo\" => self.opt_foo, \"bar\" => self.opt_bar})\n\n    # Hash\n    definition = ACON::Input::Definition.new({\"foo\" => self.opt_foo, \"bar\" => self.opt_bar})\n    definition.options.should eq({\"foo\" => self.opt_foo, \"bar\" => self.opt_bar})\n  end\n\n  def test_set_arguments : Nil\n    definition = ACON::Input::Definition.new\n\n    definition.arguments = [self.arg_foo]\n    definition.arguments.should eq({\"foo\" => self.arg_foo})\n\n    definition.arguments = [self.arg_bar]\n    definition.arguments.should eq({\"bar\" => self.arg_bar})\n  end\n\n  def test_add_arguments : Nil\n    definition = ACON::Input::Definition.new\n\n    definition << [self.arg_foo]\n    definition.arguments.should eq({\"foo\" => self.arg_foo})\n\n    definition << [self.arg_bar]\n    definition.arguments.should eq({\"foo\" => self.arg_foo, \"bar\" => self.arg_bar})\n  end\n\n  def test_add_argument : Nil\n    definition = ACON::Input::Definition.new\n\n    definition << self.arg_foo\n    definition.arguments.should eq({\"foo\" => self.arg_foo})\n\n    definition << self.arg_bar\n    definition.arguments.should eq({\"foo\" => self.arg_foo, \"bar\" => self.arg_bar})\n  end\n\n  def test_add_argument_must_have_unique_names : Nil\n    definition = ACON::Input::Definition.new self.arg_foo\n\n    expect_raises ACON::Exception::Logic, \"An argument with the name 'foo' already exists.\" do\n      definition << self.arg_foo\n    end\n  end\n\n  def test_add_argument_array_argument_must_be_last : Nil\n    definition = ACON::Input::Definition.new ACON::Input::Argument.new \"foo_array\", :is_array\n\n    expect_raises ACON::Exception::Logic, \"Cannot add a required argument 'foo' after Array argument 'foo_array'.\" do\n      definition << ACON::Input::Argument.new \"foo\"\n    end\n  end\n\n  def test_add_argument_required_argument_cannot_follow_optional : Nil\n    definition = ACON::Input::Definition.new self.arg_foo\n\n    expect_raises ACON::Exception::Logic, \"Cannot add required argument 'foo2' after the optional argument 'foo'.\" do\n      definition << self.arg_foo2\n    end\n  end\n\n  def test_argument : Nil\n    definition = ACON::Input::Definition.new self.arg_foo\n\n    definition.argument(\"foo\").should be self.arg_foo\n    definition.argument(0).should be self.arg_foo\n  end\n\n  def test_argument_missing : Nil\n    definition = ACON::Input::Definition.new self.arg_foo\n\n    expect_raises ACON::Exception::InvalidArgument, \"The argument 'bar' does not exist.\" do\n      definition.argument \"bar\"\n    end\n  end\n\n  def test_has_argument : Nil\n    definition = ACON::Input::Definition.new self.arg_foo\n\n    definition.has_argument?(\"foo\").should be_true\n    definition.has_argument?(0).should be_true\n    definition.has_argument?(\"bar\").should be_false\n    definition.has_argument?(1).should be_false\n  end\n\n  def test_required_argument_count : Nil\n    definition = ACON::Input::Definition.new\n\n    definition << self.arg_foo2\n    definition.required_argument_count.should eq 1\n\n    definition << self.arg_foo\n    definition.required_argument_count.should eq 1\n  end\n\n  def test_argument_count : Nil\n    definition = ACON::Input::Definition.new\n\n    definition << self.arg_foo2\n    definition.argument_count.should eq 1\n\n    definition << self.arg_foo\n    definition.argument_count.should eq 2\n\n    definition << ACON::Input::Argument.new \"foo_array\", :is_array\n    definition.argument_count.should eq Int32::MAX\n  end\n\n  def test_argument_defaults : Nil\n    definition = ACON::Input::Definition.new(\n      ACON::Input::Argument.new(\"foo1\", :optional),\n      ACON::Input::Argument.new(\"foo2\", :optional, \"\", \"default\"),\n      ACON::Input::Argument.new(\"foo3\", ACON::Input::Argument::Mode[:optional, :is_array]),\n    )\n\n    definition.argument_defaults.should eq({\"foo1\" => nil, \"foo2\" => \"default\", \"foo3\" => [] of String})\n\n    definition = ACON::Input::Definition.new(\n      ACON::Input::Argument.new(\"foo4\", ACON::Input::Argument::Mode[:optional, :is_array], default: [\"1\", \"2\"]),\n    )\n\n    definition.argument_defaults.should eq({\"foo4\" => [\"1\", \"2\"]})\n  end\n\n  def test_set_options : Nil\n    definition = ACON::Input::Definition.new\n\n    definition.options = [self.opt_foo]\n    definition.options.should eq({\"foo\" => self.opt_foo})\n\n    definition.options = [self.opt_bar]\n    definition.options.should eq({\"bar\" => self.opt_bar})\n  end\n\n  def test_set_options_clears_options : Nil\n    definition = ACON::Input::Definition.new [self.opt_foo]\n    definition.options = [self.opt_bar]\n\n    expect_raises ACON::Exception::InvalidArgument, \"The '-f' option does not exist.\" do\n      definition.option_for_shortcut \"f\"\n    end\n  end\n\n  def test_add_options : Nil\n    definition = ACON::Input::Definition.new\n\n    definition << [self.opt_foo]\n    definition.options.should eq({\"foo\" => self.opt_foo})\n\n    definition << [self.opt_bar]\n    definition.options.should eq({\"foo\" => self.opt_foo, \"bar\" => self.opt_bar})\n  end\n\n  def test_add_option : Nil\n    definition = ACON::Input::Definition.new\n\n    definition << self.opt_foo\n    definition.options.should eq({\"foo\" => self.opt_foo})\n\n    definition << self.opt_bar\n    definition.options.should eq({\"foo\" => self.opt_foo, \"bar\" => self.opt_bar})\n  end\n\n  def test_add_option_must_have_unique_names : Nil\n    definition = ACON::Input::Definition.new self.opt_foo\n\n    expect_raises ACON::Exception::Logic, \"An option named 'foo' already exists.\" do\n      definition << self.opt_foo2\n    end\n  end\n\n  def test_add_option_duplicate_negated : Nil\n    definition = ACON::Input::Definition.new ACON::Input::Option.new \"no-foo\"\n\n    expect_raises ACON::Exception::Logic, \"An option named 'no-foo' already exists.\" do\n      definition << ACON::Input::Option.new \"foo\", value_mode: :negatable\n    end\n  end\n\n  def test_add_option_duplicate_negated_reverse_option : Nil\n    definition = ACON::Input::Definition.new ACON::Input::Option.new \"foo\", value_mode: :negatable\n\n    expect_raises ACON::Exception::Logic, \"An option named 'no-foo' already exists.\" do\n      definition << ACON::Input::Option.new \"no-foo\"\n    end\n  end\n\n  def test_add_option_duplicate_shortcut : Nil\n    definition = ACON::Input::Definition.new self.opt_foo\n\n    expect_raises ACON::Exception::Logic, \"An option with shortcut 'f' already exists.\" do\n      definition << self.opt_foo1\n    end\n  end\n\n  def test_option : Nil\n    definition = ACON::Input::Definition.new self.opt_foo\n\n    definition.option(\"foo\").should be self.opt_foo\n    definition.option(0).should be self.opt_foo\n  end\n\n  def test_option_missing : Nil\n    definition = ACON::Input::Definition.new self.opt_foo\n\n    expect_raises ACON::Exception::InvalidArgument, \"The '--bar' option does not exist.\" do\n      definition.option \"bar\"\n    end\n  end\n\n  def test_has_option : Nil\n    definition = ACON::Input::Definition.new self.opt_foo\n\n    definition.has_option?(\"foo\").should be_true\n    definition.has_option?(0).should be_true\n    definition.has_option?(\"bar\").should be_false\n    definition.has_option?(1).should be_false\n  end\n\n  def test_has_shortcut : Nil\n    definition = ACON::Input::Definition.new self.opt_foo\n\n    definition.has_shortcut?(\"f\").should be_true\n    definition.has_shortcut?(\"p\").should be_false\n  end\n\n  def test_option_for_shortcut : Nil\n    definition = ACON::Input::Definition.new self.opt_foo\n\n    definition.option_for_shortcut(\"f\").should be self.opt_foo\n  end\n\n  def test_option_for_shortcut_multi : Nil\n    definition = ACON::Input::Definition.new self.opt_multi\n\n    definition.option_for_shortcut(\"m\").should be self.opt_multi\n    definition.option_for_shortcut(\"mmm\").should be self.opt_multi\n  end\n\n  def test_option_for_shortcut_invalid : Nil\n    definition = ACON::Input::Definition.new self.opt_foo\n\n    expect_raises ACON::Exception::InvalidArgument, \"The '-l' option does not exist.\" do\n      definition.option_for_shortcut \"l\"\n    end\n  end\n\n  def test_option_defaults : Nil\n    definition = ACON::Input::Definition.new(\n      ACON::Input::Option.new(\"foo1\", value_mode: :none),\n      ACON::Input::Option.new(\"foo2\", value_mode: :required),\n      ACON::Input::Option.new(\"foo3\", value_mode: :required, default: \"default\"),\n      ACON::Input::Option.new(\"foo4\", value_mode: :optional),\n      ACON::Input::Option.new(\"foo5\", value_mode: :optional, default: \"default\"),\n      ACON::Input::Option.new(\"foo6\", value_mode: ACON::Input::Option::Value[:optional, :is_array]),\n      ACON::Input::Option.new(\"foo7\", value_mode: ACON::Input::Option::Value[:optional, :is_array], default: [\"1\", \"2\"]),\n    )\n\n    definition.option_defaults.should eq({\n      \"foo1\" => false,\n      \"foo2\" => nil,\n      \"foo3\" => \"default\",\n      \"foo4\" => nil,\n      \"foo5\" => \"default\",\n      \"foo6\" => [] of String,\n      \"foo7\" => [\"1\", \"2\"],\n    })\n  end\n\n  def test_negation_to_name : Nil\n    definition = ACON::Input::Definition.new ACON::Input::Option.new \"foo\", value_mode: :negatable\n    definition.negation_to_name(\"no-foo\").should eq \"foo\"\n  end\n\n  def test_negation_to_name_invalid : Nil\n    definition = ACON::Input::Definition.new ACON::Input::Option.new \"foo\", value_mode: :negatable\n\n    expect_raises ACON::Exception::InvalidArgument, \"The '--no-bar' option does not exist.\" do\n      definition.negation_to_name \"no-bar\"\n    end\n  end\n\n  @[DataProvider(\"synopsis_provider\")]\n  def test_synopsis(definition : ACON::Input::Definition, expected : String) : Nil\n    definition.synopsis.should eq expected\n  end\n\n  def synopsis_provider : Hash\n    {\n      \"puts optional options in square brackets\" => {ACON::Input::Definition.new(ACON::Input::Option.new(\"foo\")), \"[--foo]\"},\n      \"separates shortcuts with a pipe\"          => {ACON::Input::Definition.new(ACON::Input::Option.new(\"foo\", \"f\")), \"[-f|--foo]\"},\n      \"uses shortcut as value placeholder\"       => {ACON::Input::Definition.new(ACON::Input::Option.new(\"foo\", \"f\", :required)), \"[-f|--foo FOO]\"},\n      \"puts optional values in square brackets\"  => {ACON::Input::Definition.new(ACON::Input::Option.new(\"foo\", \"f\", :optional)), \"[-f|--foo [FOO]]\"},\n\n      \"puts arguments in angle brackets\"              => {ACON::Input::Definition.new(ACON::Input::Argument.new(\"foo\", :required)), \"<foo>\"},\n      \"puts optional arguments square brackets\"       => {ACON::Input::Definition.new(ACON::Input::Argument.new(\"foo\", :optional)), \"[<foo>]\"},\n      \"chains optional arguments inside brackets\"     => {ACON::Input::Definition.new(ACON::Input::Argument.new(\"foo\"), ACON::Input::Argument.new(\"bar\")), \"[<foo> [<bar>]]\"},\n      \"uses an ellipsis for array arguments\"          => {ACON::Input::Definition.new(ACON::Input::Argument.new(\"foo\", :is_array)), \"[<foo>...]\"},\n      \"uses an ellipsis for required array arguments\" => {ACON::Input::Definition.new(ACON::Input::Argument.new(\"foo\", ACON::Input::Argument::Mode[:required, :is_array])), \"<foo>...\"},\n\n      \"puts [--] between options and arguments\" => {ACON::Input::Definition.new(ACON::Input::Option.new(\"foo\"), ACON::Input::Argument.new(\"foo\", :required)), \"[--foo] [--] <foo>\"},\n    }\n  end\n\n  def test_synopsis_short : Nil\n    definition = ACON::Input::Definition.new(\n      ACON::Input::Option.new(\"foo\"),\n      ACON::Input::Option.new(\"bar\"),\n      ACON::Input::Argument.new(\"baz\"),\n    )\n\n    definition.synopsis(true).should eq \"[options] [--] [<baz>]\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/hash_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct HashTest < ASPEC::TestCase\n  def test_first_argument : Nil\n    ACON::Input::Hash.new.first_argument.should be_nil\n    ACON::Input::Hash.new(name: \"George\").first_argument.should eq \"George\"\n    ACON::Input::Hash.new(\"--foo\": \"bar\", name: \"George\").first_argument.should eq \"George\"\n  end\n\n  def test_has_parameter : Nil\n    input = ACON::Input::Hash.new(name: \"George\", \"--foo\": \"bar\")\n    input.has_parameter?(\"--foo\").should be_true\n    input.has_parameter?(\"--bar\").should be_false\n\n    ACON::Input::Hash.new(\"--foo\").has_parameter?(\"--foo\").should be_true\n\n    input = ACON::Input::Hash.new \"--foo\", \"--\", \"--bar\"\n    input.has_parameter?(\"--bar\").should be_true\n    input.has_parameter?(\"--bar\", only_params: true).should be_false\n  end\n\n  def test_get_parameter : Nil\n    input = ACON::Input::Hash.new(name: \"George\", \"--foo\": \"bar\")\n    input.parameter(\"--foo\").should eq \"bar\"\n    input.parameter(\"--bar\", \"default\").should eq \"default\"\n\n    ACON::Input::Hash.new(\"George\": nil, \"--foo\": \"bar\").parameter(\"--foo\").should eq \"bar\"\n\n    input = ACON::Input::Hash.new(\"--foo\": nil, \"--\": nil, \"--bar\": \"baz\")\n    input.parameter(\"--bar\").should eq \"baz\"\n    input.parameter(\"--bar\", \"default\", true).should eq \"default\"\n  end\n\n  def test_parse_arguments : Nil\n    input = ACON::Input::Hash.new(\n      {\"name\" => \"foo\"},\n      ACON::Input::Definition.new ACON::Input::Argument.new \"name\"\n    )\n\n    input.arguments.should eq({\"name\" => \"foo\"})\n  end\n\n  @[DataProvider(\"option_provider\")]\n  def test_parse_options(args : Hash(String, _), options : Array(ACON::Input::Option), expected_options : ::Hash) : Nil\n    input = ACON::Input::Hash.new args, ACON::Input::Definition.new options\n\n    input.options.should eq expected_options\n  end\n\n  def option_provider : Hash\n    {\n      \"long option\" => {\n        {\n          \"--foo\" => \"bar\",\n        },\n        [ACON::Input::Option.new(\"foo\")],\n        {\"foo\" => \"bar\"},\n      },\n      \"long option with default\" => {\n        {\n          \"--foo\" => \"bar\",\n        },\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional, \"\", \"default\")],\n        {\"foo\" => \"bar\"},\n      },\n      \"uses default value if not passed\" => {\n        Hash(String, String).new,\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional, \"\", \"default\")],\n        {\"foo\" => \"default\"},\n      },\n      \"uses passed value even with default\" => {\n        {\"--foo\" => nil},\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional, \"\", \"default\")],\n        {\"foo\" => nil},\n      },\n      \"short option\" => {\n        {\"-f\" => \"bar\"},\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional, \"\", \"default\")],\n        {\"foo\" => \"bar\"},\n      },\n      \"does not parse args after --\" => {\n        {\"--\" => nil, \"-f\" => \"bar\"},\n        [ACON::Input::Option.new(\"foo\", \"f\", :optional, \"\", \"default\")],\n        {\"foo\" => \"default\"},\n      },\n      \"handles only --\" => {\n        {\"--\" => nil},\n        Array(ACON::Input::Option).new,\n        Hash(String, String).new,\n      },\n    }\n  end\n\n  @[DataProvider(\"invalid_input_provider\")]\n  def test_parse_invalid_input(args : Hash(String, _), definition : ACON::Input::Definition, error_class : ::Exception.class, error_message : String) : Nil\n    expect_raises error_class, error_message do\n      ACON::Input::Hash.new args, definition\n    end\n  end\n\n  def invalid_input_provider : Tuple\n    {\n      {\n        {\"foo\" => \"foo\"},\n        ACON::Input::Definition.new(ACON::Input::Argument.new(\"name\")),\n        ACON::Exception::InvalidArgument,\n        \"The 'foo' argument does not exist.\",\n      },\n      {\n        {\"--foo\" => nil},\n        ACON::Input::Definition.new(ACON::Input::Option.new(\"foo\", \"f\", :required)),\n        ACON::Exception::InvalidOption,\n        \"The '--foo' option requires a value.\",\n      },\n      {\n        {\"--foo\" => \"foo\"},\n        ACON::Input::Definition.new,\n        ACON::Exception::InvalidOption,\n        \"The '--foo' option does not exist.\",\n      },\n      {\n        {\"-o\" => \"foo\"},\n        ACON::Input::Definition.new,\n        ACON::Exception::InvalidOption,\n        \"The '-o' option does not exist.\",\n      },\n    }\n  end\n\n  def test_to_s_complex_mix : Nil\n    input = ACON::Input::Hash.new \"-f\": nil, \"-b\": \"bar\", \"--foo\": \"b a z\", \"--lala\": nil, \"test\": \"Foo\", \"test2\": \"A\\nB'C\"\n\n    {% if flag? :windows %}\n      input.to_s.should eq \"-f -b bar --foo=\\\"b a z\\\" --lala Foo A\\nB'C\"\n    {% else %}\n      input.to_s.should eq \"-f -b bar --foo='b a z' --lala Foo 'A\\nB'\\\"'\\\"'C'\"\n    {% end %}\n  end\n\n  def test_to_s_array_options : Nil\n    input = ACON::Input::Hash.new \"-b\": [\"bval_1\", \"bval_2\"], \"--f\": [\"fval_1\", \"fval_2\"]\n    input.to_s.should eq \"-b bval_1 -b bval_2 --f=fval_1 --f=fval_2\"\n  end\n\n  def test_to_s_array_argument : Nil\n    input = ACON::Input::Hash.new \"array_arg\": [\"val_1\", \"val_2\"]\n    input.to_s.should eq \"val_1 val_2\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/input_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ACON::Input do\n  describe \"options\" do\n    it \"parses long option\" do\n      input = ACON::Input::Hash.new(\n        {\"--name\" => \"foo\"},\n        ACON::Input::Definition.new(ACON::Input::Option.new(\"name\"))\n      )\n\n      input.option(\"name\").should eq \"foo\"\n\n      input.set_option \"name\", \"bar\"\n      input.option(\"name\").should eq \"bar\"\n      input.options.should eq({\"name\" => \"bar\"})\n    end\n\n    it \"parses short option\" do\n      input = ACON::Input::Hash.new(\n        {\"-n\" => \"foo\"},\n        ACON::Input::Definition.new(ACON::Input::Option.new(\"name\", shortcut: \"n\"))\n      )\n\n      input.option(\"name\").should eq \"foo\"\n\n      input.set_option \"name\", \"bar\"\n      input.option(\"name\").should eq \"bar\"\n      input.options.should eq({\"name\" => \"bar\"})\n    end\n\n    it \"uses default when not provided\" do\n      input = ACON::Input::Hash.new(\n        {\"--name\" => \"foo\"},\n        ACON::Input::Definition.new(\n          ACON::Input::Option.new(\"name\"),\n          ACON::Input::Option.new(\"bar\", nil, :optional, \"\", \"default\")\n        )\n      )\n\n      input.option(\"bar\").should eq \"default\"\n      input.options.should eq({\"name\" => \"foo\", \"bar\" => \"default\"})\n    end\n\n    it \"should parse explicit empty string value\" do\n      input = ACON::Input::Hash.new(\n        {\"--name\" => \"foo\", \"--bar\" => \"\"},\n        ACON::Input::Definition.new(\n          ACON::Input::Option.new(\"name\"),\n          ACON::Input::Option.new(\"bar\", nil, :optional, \"\", \"default\")\n        )\n      )\n\n      input.option(\"bar\").should eq \"\"\n      input.options.should eq({\"name\" => \"foo\", \"bar\" => \"\"})\n    end\n\n    it \"should parse explicit nil value\" do\n      input = ACON::Input::Hash.new(\n        {\"--name\" => \"foo\", \"--bar\" => nil},\n        ACON::Input::Definition.new(\n          ACON::Input::Option.new(\"name\"),\n          ACON::Input::Option.new(\"bar\", nil, :optional, \"\", \"default\")\n        )\n      )\n\n      input.option(\"bar\").should be_nil\n      input.options.should eq({\"name\" => \"foo\", \"bar\" => nil})\n    end\n\n    describe \"negatable option\" do\n      it \"non negated\" do\n        input = ACON::Input::Hash.new(\n          {\"--name\" => nil},\n          ACON::Input::Definition.new(\n            ACON::Input::Option.new(\"name\", value_mode: :negatable)\n          )\n        )\n\n        input.has_option?(\"name\").should be_true\n        input.has_option?(\"no-name\").should be_true\n        input.option(\"name\").should eq \"true\"\n        input.option(\"no-name\").should eq \"false\"\n      end\n\n      it \"negated\" do\n        input = ACON::Input::Hash.new(\n          {\"--no-name\" => nil},\n          ACON::Input::Definition.new(\n            ACON::Input::Option.new(\"name\", value_mode: :negatable)\n          )\n        )\n\n        input.option(\"name\").should eq \"false\"\n        input.option(\"no-name\").should eq \"true\"\n      end\n\n      it \"with default\" do\n        input = ACON::Input::Hash.new(\n          Hash(String, String).new,\n          ACON::Input::Definition.new(\n            ACON::Input::Option.new(\"name\", value_mode: :negatable, default: nil)\n          )\n        )\n\n        input.option(\"name\").should be_nil\n        input.option(\"no-name\").should be_nil\n      end\n    end\n\n    it \"set invalid option\" do\n      input = ACON::Input::Hash.new(\n        {\"--name\" => \"foo\"},\n        ACON::Input::Definition.new(\n          ACON::Input::Option.new(\"name\"),\n          ACON::Input::Option.new(\"bar\", nil, :optional, \"\", \"default\")\n        )\n      )\n\n      expect_raises ACON::Exception::InvalidArgument, \"The 'foo' option does not exist.\" do\n        input.set_option \"foo\", \"foo\"\n      end\n    end\n\n    it \"get invalid option\" do\n      input = ACON::Input::Hash.new(\n        {\"--name\" => \"foo\"},\n        ACON::Input::Definition.new(\n          ACON::Input::Option.new(\"name\"),\n          ACON::Input::Option.new(\"bar\", nil, :optional, \"\", \"default\")\n        )\n      )\n\n      expect_raises ACON::Exception::InvalidArgument, \"The 'foo' option does not exist.\" do\n        input.option \"foo\"\n      end\n    end\n\n    describe \"#option(T)\" do\n      it \"optional option with default accessed via non nilable type\" do\n        input = ACON::Input::Hash.new(\n          Hash(String, String).new,\n          ACON::Input::Definition.new(\n            ACON::Input::Option.new(\"name\", nil, :optional, default: \"bar\"),\n          )\n        )\n\n        option = input.option \"name\", String\n        typeof(option).should eq String\n        option.should eq \"bar\"\n      end\n\n      it \"optional option without default accessed via nilable type\" do\n        input = ACON::Input::Hash.new(\n          {\"--name2\" => \"foo\"},\n          ACON::Input::Definition.new(\n            ACON::Input::Option.new(\"name\"),\n            ACON::Input::Option.new(\"name2\"),\n          )\n        )\n\n        option = input.option \"name2\", String?\n        typeof(option).should eq String?\n        option.should eq \"foo\"\n      end\n\n      it \"required option with default accessed via non nilable type\" do\n        input = ACON::Input::Hash.new(\n          {\"--name\" => \"foo\"},\n          ACON::Input::Definition.new(\n            ACON::Input::Option.new(\"name\", nil, :required),\n          )\n        )\n\n        option = input.option \"name\", String\n        typeof(option).should eq String\n        option.should eq \"foo\"\n      end\n\n      it \"negatable option accessed via non bool type\" do\n        input = ACON::Input::Hash.new(\n          {\"--name\" => \"true\"},\n          ACON::Input::Definition.new(\n            ACON::Input::Option.new(\"name\", nil, :negatable),\n          )\n        )\n\n        expect_raises ACON::Exception::Logic, \"Cannot cast negatable option 'name' to non 'Bool?' type.\" do\n          input.option \"name\", Int32\n        end\n      end\n\n      it \"negatable option with default accessed via non nilable type\" do\n        input = ACON::Input::Hash.new(\n          {\"--name\" => \"true\"},\n          ACON::Input::Definition.new(\n            ACON::Input::Option.new(\"name\", nil, :negatable),\n          )\n        )\n\n        option = input.option \"name\", Bool\n        typeof(option).should eq Bool\n        option.should be_true\n\n        option = input.option \"no-name\", Bool\n        typeof(option).should eq Bool\n        option.should be_false\n      end\n\n      it \"option that doesnt exist\" do\n        input = ACON::Input::Hash.new(\n          {\"--name\" => \"foo\"},\n          ACON::Input::Definition.new(\n            ACON::Input::Option.new(\"name\"),\n          )\n        )\n\n        expect_raises ACON::Exception::InvalidArgument, \"The 'foo' option does not exist.\" do\n          input.option \"foo\"\n        end\n      end\n    end\n  end\n\n  describe \"arguments\" do\n    it do\n      input = ACON::Input::Hash.new(\n        {\"name\" => \"foo\"},\n        ACON::Input::Definition.new(\n          ACON::Input::Argument.new(\"name\"),\n        )\n      )\n\n      input.argument(\"name\").should eq \"foo\"\n      input.set_argument \"name\", \"bar\"\n      input.argument(\"name\").should eq \"bar\"\n      input.arguments.should eq({\"name\" => \"bar\"})\n    end\n\n    it do\n      input = ACON::Input::Hash.new(\n        {\"name\" => \"foo\"},\n        ACON::Input::Definition.new(\n          ACON::Input::Argument.new(\"name\"),\n          ACON::Input::Argument.new(\"bar\", :optional, \"\", \"default\")\n        )\n      )\n\n      input.argument(\"bar\").should eq \"default\"\n      typeof(input.argument(\"bar\")).should eq String?\n      input.arguments.should eq({\"name\" => \"foo\", \"bar\" => \"default\"})\n    end\n\n    it \"set invalid option\" do\n      input = ACON::Input::Hash.new(\n        {\"name\" => \"foo\"},\n        ACON::Input::Definition.new(\n          ACON::Input::Argument.new(\"name\"),\n          ACON::Input::Argument.new(\"bar\", :optional, \"\", \"default\")\n        )\n      )\n\n      expect_raises ACON::Exception::InvalidArgument, \"The 'foo' argument does not exist.\" do\n        input.set_argument \"foo\", \"foo\"\n      end\n    end\n\n    it \"get invalid option\" do\n      input = ACON::Input::Hash.new(\n        {\"name\" => \"foo\"},\n        ACON::Input::Definition.new(\n          ACON::Input::Argument.new(\"name\"),\n          ACON::Input::Argument.new(\"bar\", :optional, \"\", \"default\")\n        )\n      )\n\n      expect_raises ACON::Exception::InvalidArgument, \"The 'foo' argument does not exist.\" do\n        input.argument \"foo\"\n      end\n    end\n\n    describe \"#argument(T)\" do\n      it \"optional arg without default raises when accessed via non nilable type\" do\n        input = ACON::Input::Hash.new(\n          {\"name\" => \"foo\"},\n          ACON::Input::Definition.new(\n            ACON::Input::Argument.new(\"name\"),\n          )\n        )\n\n        expect_raises ACON::Exception::Logic, \"Cannot cast optional argument 'name' to non-nilable type 'String' without a default.\" do\n          input.argument \"name\", String\n        end\n      end\n\n      it \"optional arg with default accessed via non nilable type\" do\n        input = ACON::Input::Hash.new(\n          Hash(String, String).new,\n          ACON::Input::Definition.new(\n            ACON::Input::Argument.new(\"name\", default: \"bar\"),\n          )\n        )\n\n        arg = input.argument \"name\", String\n        typeof(arg).should eq String\n        arg.should eq \"bar\"\n      end\n\n      it \"optional arg without default accessed via nilable type\" do\n        input = ACON::Input::Hash.new(\n          {\"name2\" => \"foo\"},\n          ACON::Input::Definition.new(\n            ACON::Input::Argument.new(\"name\"),\n            ACON::Input::Argument.new(\"name2\"),\n          )\n        )\n\n        arg = input.argument \"name2\", String?\n        typeof(arg).should eq String?\n        arg.should eq \"foo\"\n      end\n\n      it \"arg that doesnt exist\" do\n        input = ACON::Input::Hash.new(\n          {\"name\" => \"foo\"},\n          ACON::Input::Definition.new(\n            ACON::Input::Argument.new(\"name\"),\n          )\n        )\n\n        expect_raises ACON::Exception::InvalidArgument, \"The 'foo' argument does not exist.\" do\n          input.argument \"foo\"\n        end\n      end\n    end\n  end\n\n  describe \"#validate\" do\n    it \"missing arguments\" do\n      input = ACON::Input::Hash.new\n      input.bind ACON::Input::Definition.new ACON::Input::Argument.new(\"name\", :required)\n\n      expect_raises ACON::Exception::Runtime, \"Not enough arguments (missing: 'name').\" do\n        input.validate\n      end\n    end\n\n    it \"missing required argument\" do\n      input = ACON::Input::Hash.new bar: \"baz\"\n      input.bind ACON::Input::Definition.new(\n        ACON::Input::Argument.new(\"name\", :required),\n        ACON::Input::Argument.new(\"bar\", :optional)\n      )\n\n      expect_raises ACON::Exception::Runtime, \"Not enough arguments (missing: 'name').\" do\n        input.validate\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/option_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ACON::Input::Option do\n  describe \".new\" do\n    it \"normalizes the name\" do\n      ACON::Input::Option.new(\"--foo\").name.should eq \"foo\"\n    end\n\n    it \"disallows blank names\" do\n      expect_raises ACON::Exception::InvalidArgument, \"An option name cannot be blank.\" do\n        ACON::Input::Option.new \"\"\n      end\n\n      expect_raises ACON::Exception::InvalidArgument, \"An option name cannot be blank.\" do\n        ACON::Input::Option.new \"   \"\n      end\n    end\n\n    describe \"shortcut\" do\n      it \"array\" do\n        ACON::Input::Option.new(\"foo\", [\"a\", \"b\"]).shortcut.should eq \"a|b\"\n      end\n\n      it \"string\" do\n        ACON::Input::Option.new(\"foo\", \"-a|b\").shortcut.should eq \"a|b\"\n      end\n\n      it \"string with whitespace\" do\n        ACON::Input::Option.new(\"foo\", \"a|  -b\").shortcut.should eq \"a|b\"\n      end\n\n      it \"string with different characters\" do\n        expect_raises ACON::Exception::InvalidArgument, \"An option shortcut must consist of the same character, got 'ab'.\" do\n          ACON::Input::Option.new \"foo\", \"ab\"\n        end\n\n        expect_raises ACON::Exception::InvalidArgument, \"An option shortcut must consist of the same character, got 'aab'.\" do\n          ACON::Input::Option.new \"foo\", \"a|aa|aab\"\n        end\n      end\n\n      it \"array with different characters\" do\n        expect_raises ACON::Exception::InvalidArgument, \"An option shortcut must consist of the same character, got 'ab'.\" do\n          ACON::Input::Option.new \"foo\", [\"a\", \"ab\"]\n        end\n      end\n\n      it \"blank\" do\n        expect_raises ACON::Exception::InvalidArgument, \"An option shortcut cannot be blank.\" do\n          ACON::Input::Option.new \"foo\", [] of String\n        end\n\n        expect_raises ACON::Exception::InvalidArgument, \"An option shortcut cannot be blank.\" do\n          ACON::Input::Option.new \"foo\", \"\"\n        end\n\n        expect_raises ACON::Exception::InvalidArgument, \"An option shortcut cannot be blank.\" do\n          ACON::Input::Option.new \"foo\", \"   \"\n        end\n      end\n    end\n\n    describe \"value_mode\" do\n      it \"NONE | IS_ARRAY\" do\n        expect_raises ACON::Exception::InvalidArgument, \"Cannot have VALUE::IS_ARRAY option mode when the option does not accept a value.\" do\n          ACON::Input::Option.new \"foo\", value_mode: ACON::Input::Option::Value::NONE | ACON::Input::Option::Value::IS_ARRAY\n        end\n      end\n\n      it \"NEGATABLE with value\" do\n        expect_raises ACON::Exception::InvalidArgument, \"Cannot have VALUE::NEGATABLE option mode if the option also accepts a value.\" do\n          ACON::Input::Option.new \"foo\", value_mode: ACON::Input::Option::Value::REQUIRED | ACON::Input::Option::Value::NEGATABLE\n        end\n      end\n    end\n  end\n\n  describe \"#default=\" do\n    it \"does not allow a default if using Value::NONE\" do\n      expect_raises ACON::Exception::Logic, \"Cannot set a default value when using Value::NONE mode.\" do\n        ACON::Input::Option.new \"foo\", default: \"bar\"\n      end\n    end\n\n    describe \"array\" do\n      it \"nil value\" do\n        option = ACON::Input::Option.new \"foo\", value_mode: ACON::Input::Option::Value::OPTIONAL | ACON::Input::Option::Value::IS_ARRAY\n        option.default = nil\n        option.default.should eq [] of String\n      end\n\n      it \"non array\" do\n        option = ACON::Input::Option.new \"foo\", value_mode: ACON::Input::Option::Value::OPTIONAL | ACON::Input::Option::Value::IS_ARRAY\n\n        expect_raises ACON::Exception::Logic, \"Default value for an array option must be an array.\" do\n          option.default = \"bar\"\n        end\n      end\n    end\n  end\n\n  describe \"#complete\" do\n    it \"with an array\" do\n      values = [\"foo\", \"bar\"]\n      suggestions = ACON::Completion::Suggestions.new\n\n      argument = ACON::Input::Option.new \"foo\", value_mode: :required, suggested_values: values\n\n      argument.has_completion?.should be_true\n\n      argument.complete ACON::Completion::Input.new, suggestions\n\n      suggestions.suggested_values.map(&.value).should eq [\"foo\", \"bar\"]\n    end\n\n    it \"with an block\" do\n      values = [\"foo\", \"bar\"]\n      suggestions = ACON::Completion::Suggestions.new\n      callback = Proc(ACON::Completion::Input, Array(String)).new { values }\n\n      argument = ACON::Input::Option.new \"foo\", value_mode: :required, suggested_values: callback\n\n      argument.has_completion?.should be_true\n\n      argument.complete ACON::Completion::Input.new, suggestions\n\n      suggestions.suggested_values.map(&.value).should eq [\"foo\", \"bar\"]\n    end\n\n    it \"when option accepts no value\" do\n      expect_raises ACON::Exception::Logic, \"Cannot set suggested values if the option does not accept a value.\" do\n        ACON::Input::Option.new \"foo\", suggested_values: [\"foo\"]\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/string_line_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct StringLineTest < ASPEC::TestCase\n  @[DataProvider(\"tokenize_data\")]\n  def test_tokenize(input : String, tokens : Array(String)) : Nil\n    input = ACON::Input::StringLine.new input\n    input.@tokens.should eq tokens\n  end\n\n  def tokenize_data : Hash\n    {\n      \"empty string\"                                    => {\"\", [] of String},\n      \"arguments\"                                       => {\"foo\", [\"foo\"]},\n      \"ignores whitespace between arguments\"            => {\"  foo  \", [\"foo\"]},\n      \"single quoted arguments\"                         => {\"'foo'\", [\"foo\"]},\n      \"double quoted arguments\"                         => {\"\\\"foo\\\"\", [\"foo\"]},\n      \"whitespace characters within string\"             => {\"'a\\rb\\nc\\td'\", [\"a\\rb\\nc\\td\"]},\n      \"whitespace characters between args as spaces\"    => {\"'a'\\r'b'\\n'c'\\t'd'\", [\"a\", \"b\", \"c\", \"d\"]},\n      \"escaped double quoted arguments\"                 => { %(\\\\\"foo\\\\\"), [\"\\\"foo\\\"\"] },\n      \"escaped single quoted arguments\"                 => { %(\\\\'foo\\\\'), [\"'foo'\"] },\n      \"short option\"                                    => {\"-a\", [\"-a\"]},\n      \"aggregated short options\"                        => {\"-azc\", [\"-azc\"]},\n      \"short option with value\"                         => {\"-awithavalue\", [\"-awithavalue\"]},\n      \"short option with double quoted value\"           => { %(-a\"foo bar\"), [\"-afoo bar\"] },\n      \"short option with multiple double quoted values\" => { %(-a\"foo bar\"\"foo bar\"), [\"-afoo barfoo bar\"] },\n      \"short option with single quoted value\"           => { %(-a'foo bar'), [\"-afoo bar\"] },\n      \"short option with multiple single quoted values\" => { %(-a'foo bar''foo bar'), [\"-afoo barfoo bar\"] },\n      \"long option\"                                     => {\"--long-option\", [\"--long-option\"]},\n      \"long option with value\"                          => {\"--long-option=foo\", [\"--long-option=foo\"]},\n      \"long option with double quoted value\"            => { %(--long-option=\"foo bar\"), [\"--long-option=foo bar\"] },\n      \"long option with multiple double quoted values\"  => { %(--long-option=\"foo bar\"\"another\"), [\"--long-option=foo baranother\"] },\n      \"long option with single quoted value\"            => { %(--long-option='foo bar'), [\"--long-option=foo bar\"] },\n      \"long option with multiple single quoted values\"  => { %(--long-option='foo bar''another'), [\"--long-option=foo baranother\"] },\n      \"several arguments and options\"                   => {\"foo -a -ffoo --long bar\", [\"foo\", \"-a\", \"-ffoo\", \"--long\", \"bar\"]},\n      \"quoted quotes\"                                   => {\"--arg=\\\\\\\"'Jenny'\\\\''s'\\\\\\\"\", [\"--arg=\\\"Jenny's\\\"\"]},\n      \"quoted single quote with escaped quote\"          => {\"'A\\nB\\\\'C'\", [\"A\\nB'C\"]},\n    }\n  end\n\n  def test_to_s : Nil\n    input = ACON::Input::StringLine.new \"-f foo\"\n    input.to_s.should eq \"-f foo\"\n\n    {% if flag? :windows %}\n      input = ACON::Input::StringLine.new %(-f --bar=foo \"a b c d\")\n      input.to_s.should eq \"-f --bar=foo \\\"a b c d\\\"\"\n\n      input = ACON::Input::StringLine.new %(-f --bar=foo 'a b c d' 'A\\nB\\\\'C')\n      input.to_s.should eq \"-f --bar=foo \\\"a b c d\\\" A\\nB'C\"\n    {% else %}\n      input = ACON::Input::StringLine.new %(-f --bar=foo \"a b c d\")\n      input.to_s.should eq \"-f --bar=foo 'a b c d'\"\n\n      input = ACON::Input::StringLine.new %(-f --bar=foo 'a b c d' 'A\\nB\\\\'C')\n      input.to_s.should eq \"-f --bar=foo 'a b c d' 'A\\nB'\\\"'\\\"'C'\"\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/value/array_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe ACON::Input::Value::Array do\n  describe \".new\" do\n    it \"without args\" do\n      array = ACON::Input::Value::Array.new\n      array.value.should be_empty\n      array.should be_empty\n    end\n\n    it \"with args\" do\n      array = ACON::Input::Value::Array.from_array [1, \"foo\", false]\n      array.value.size.should eq 3\n      array << 10\n      array.value.size.should eq 4\n    end\n  end\n\n  it \"#to_s\" do\n    ACON::Input::Value::Array\n      .from_array([1, \"foo\", false])\n      .to_s\n      .should eq %(1,foo,false)\n  end\n\n  describe \"#get\" do\n    it \"non-nilable\" do\n      ACON::Input::Value::Array\n        .from_array(arr = [1, 2, 3])\n        .get(Array(Int32))\n        .should eq arr\n    end\n\n    it \"nilable\" do\n      ACON::Input::Value::Array\n        .from_array(arr = [\"foo\", \"bar\"])\n        .get(Array(String)?)\n        .should eq arr\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/value/bool_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe ACON::Input::Value::Bool do\n  describe \"#get\" do\n    describe Bool do\n      it \"non-nilable\" do\n        val = ACON::Input::Value::Bool.new(false).get Bool\n        typeof(val).should eq Bool\n        val.should be_false\n      end\n\n      it \"nilable\" do\n        val = ACON::Input::Value::Bool.new(true).get Bool?\n        typeof(val).should eq Bool?\n        val.should be_true\n      end\n    end\n\n    describe String do\n      it \"non-nilable\" do\n        val = ACON::Input::Value::Bool.new(false).get String\n        typeof(val).should eq String\n        val.should eq \"false\"\n      end\n\n      it \"nilable\" do\n        val = ACON::Input::Value::Bool.new(true).get String?\n        typeof(val).should eq String?\n        val.should eq \"true\"\n      end\n    end\n\n    describe Int do\n      it \"non-nilable\" do\n        expect_raises ACON::Exception::Logic, \"'false' is not a valid 'Int32'.\" do\n          ACON::Input::Value::Bool.new(false).get Int32\n        end\n\n        expect_raises ACON::Exception::Logic, \"'true' is not a valid 'UInt8'.\" do\n          ACON::Input::Value::Bool.new(true).get UInt8\n        end\n      end\n\n      it \"nilable\" do\n        expect_raises ACON::Exception::Logic, \"'false' is not a valid '(Int32 | Nil)'.\" do\n          ACON::Input::Value::Bool.new(false).get Int32?\n        end\n\n        expect_raises ACON::Exception::Logic, \"'true' is not a valid '(UInt8 | Nil)'.\" do\n          ACON::Input::Value::Bool.new(true).get UInt8?\n        end\n      end\n    end\n\n    describe Float do\n      it \"non-nilable\" do\n        expect_raises ACON::Exception::Logic, \"'false' is not a valid 'Float32'.\" do\n          ACON::Input::Value::Bool.new(false).get Float32\n        end\n\n        expect_raises ACON::Exception::Logic, \"'true' is not a valid 'Float64'.\" do\n          ACON::Input::Value::Bool.new(true).get Float64\n        end\n      end\n\n      it \"nilable\" do\n        expect_raises ACON::Exception::Logic, \"'false' is not a valid '(Float32 | Nil)'.\" do\n          ACON::Input::Value::Bool.new(false).get Float32?\n        end\n\n        expect_raises ACON::Exception::Logic, \"'true' is not a valid '(Float64 | Nil)'.\" do\n          ACON::Input::Value::Bool.new(true).get Float64?\n        end\n      end\n    end\n\n    describe Array do\n      describe String do\n        it \"non-nilable\" do\n          expect_raises ACON::Exception::Logic, \"'false' is not a valid 'Array(String)'.\" do\n            ACON::Input::Value::Bool.new(false).get Array(String)\n          end\n        end\n\n        it \"nilable\" do\n          expect_raises ACON::Exception::Logic, \"'false' is not a valid '(Array(String) | Nil)'.\" do\n            ACON::Input::Value::Bool.new(false).get Array(String)?\n          end\n        end\n\n        it \"nilable generic value\" do\n          expect_raises ACON::Exception::Logic, \"'true' is not a valid '(Array(String | Nil) | Nil)'.\" do\n            ACON::Input::Value::Bool.new(true).get Array(String?)?\n          end\n        end\n      end\n\n      describe Int32 do\n        it \"non-nilable\" do\n          expect_raises ACON::Exception::Logic, \"'false' is not a valid 'Array(Int32)'.\" do\n            ACON::Input::Value::Bool.new(false).get Array(Int32)\n          end\n        end\n\n        it \"nilable\" do\n          expect_raises ACON::Exception::Logic, \"'false' is not a valid '(Array(Int32) | Nil)'.\" do\n            ACON::Input::Value::Bool.new(false).get Array(Int32)?\n          end\n        end\n\n        it \"nilable generic value\" do\n          expect_raises ACON::Exception::Logic, \"'true' is not a valid '(Array(Int32 | Nil) | Nil)'.\" do\n            ACON::Input::Value::Bool.new(true).get Array(Int32?)?\n          end\n        end\n      end\n\n      describe Bool do\n        it \"non-nilable\" do\n          expect_raises ACON::Exception::Logic, \"'false' is not a valid 'Array(Bool)'.\" do\n            ACON::Input::Value::Bool.new(false).get Array(Bool)\n          end\n        end\n\n        it \"nilable\" do\n          expect_raises ACON::Exception::Logic, \"'false' is not a valid '(Array(Bool) | Nil)'.\" do\n            ACON::Input::Value::Bool.new(false).get Array(Bool)?\n          end\n        end\n\n        it \"nilable generic value\" do\n          expect_raises ACON::Exception::Logic, \"'true' is not a valid '(Array(Bool | Nil) | Nil)'.\" do\n            ACON::Input::Value::Bool.new(true).get Array(Bool?)?\n          end\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/value/nil_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe ACON::Input::Value::Number do\n  describe \"#get\" do\n    it Bool do\n      expect_raises ACON::Exception::Logic, \"'123' is not a valid 'Bool'.\" do\n        ACON::Input::Value::Number.new(123).get Bool\n      end\n    end\n\n    it String do\n      val = ACON::Input::Value::Number.new(123).get String\n      typeof(val).should eq String\n      val.should eq \"123\"\n    end\n\n    it Int do\n      val = ACON::Input::Value::Number.new(123).get Int32\n      typeof(val).should eq Int32\n      val.should eq 123\n\n      val = ACON::Input::Value::Number.new(123_u8).get UInt8\n      typeof(val).should eq UInt8\n      val.should eq 123_u8\n    end\n\n    it Float do\n      val = ACON::Input::Value::Number.new(4.69).get Float32\n      typeof(val).should eq Float32\n      val.should eq 4.69_f32\n\n      val = ACON::Input::Value::Number.new(4.69).get Float64\n      typeof(val).should eq Float64\n      val.should eq 4.69\n    end\n\n    describe Array do\n      it String do\n        expect_raises ACON::Exception::Logic, \"'123' is not a valid 'Array(String)'.\" do\n          ACON::Input::Value::Number.new(123).get Array(String)\n        end\n      end\n\n      it Int32 do\n        expect_raises ACON::Exception::Logic, \"'123' is not a valid '(Array(Int32) | Nil)'.\" do\n          ACON::Input::Value::Number.new(123).get Array(Int32)?\n        end\n      end\n\n      it Bool do\n        expect_raises ACON::Exception::Logic, \"'123' is not a valid 'Array(Bool)'.\" do\n          ACON::Input::Value::Number.new(123).get Array(Bool)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/value/number_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe ACON::Input::Value::Number do\n  describe \"#get\" do\n    describe Bool do\n      it \"non-nilable\" do\n        expect_raises ACON::Exception::Logic, \"'123' is not a valid 'Bool'.\" do\n          ACON::Input::Value::Number.new(123).get Bool\n        end\n      end\n\n      it \"nilable\" do\n        expect_raises ACON::Exception::Logic, \"'123' is not a valid '(Bool | Nil)'.\" do\n          ACON::Input::Value::Number.new(123).get Bool?\n        end\n      end\n    end\n\n    describe String do\n      it \"non-nilable\" do\n        val = ACON::Input::Value::Number.new(123).get String\n        typeof(val).should eq String\n        val.should eq \"123\"\n      end\n\n      it \"nilable\" do\n        val = ACON::Input::Value::Number.new(123).get String?\n        typeof(val).should eq String?\n        val.should eq \"123\"\n      end\n    end\n\n    describe Int do\n      it \"non-nilable\" do\n        val = ACON::Input::Value::Number.new(123).get Int32\n        typeof(val).should eq Int32\n        val.should eq 123\n\n        val = ACON::Input::Value::Number.new(123_u8).get UInt8\n        typeof(val).should eq UInt8\n        val.should eq 123_u8\n      end\n\n      it \"non-nilable\" do\n        val = ACON::Input::Value::Number.new(123).get Int32?\n        typeof(val).should eq Int32?\n        val.should eq 123\n\n        val = ACON::Input::Value::Number.new(123_u8).get UInt8?\n        typeof(val).should eq UInt8?\n        val.should eq 123_u8\n      end\n    end\n\n    describe Float do\n      it \"non-nilable\" do\n        val = ACON::Input::Value::Number.new(4.69).get Float32\n        typeof(val).should eq Float32\n        val.should eq 4.69_f32\n\n        val = ACON::Input::Value::Number.new(4.69).get Float64\n        typeof(val).should eq Float64\n        val.should eq 4.69\n      end\n\n      it \"non-nilable\" do\n        val = ACON::Input::Value::Number.new(4.69).get Float32?\n        typeof(val).should eq Float32?\n        val.should eq 4.69_f32\n\n        val = ACON::Input::Value::Number.new(4.69).get Float64?\n        typeof(val).should eq Float64?\n        val.should eq 4.69\n      end\n    end\n\n    describe Array do\n      describe String do\n        it \"non-nilable\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid 'Array(String)'.\" do\n            ACON::Input::Value::Number.new(123).get Array(String)\n          end\n        end\n\n        it \"nilable\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid '(Array(String) | Nil)'.\" do\n            ACON::Input::Value::Number.new(123).get Array(String)?\n          end\n        end\n\n        it \"nilable generic value\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid '(Array(String | Nil) | Nil)'.\" do\n            ACON::Input::Value::Number.new(123).get Array(String?)?\n          end\n        end\n      end\n\n      describe Int32 do\n        it \"non-nilable\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid 'Array(Int32)'.\" do\n            ACON::Input::Value::Number.new(123).get Array(Int32)\n          end\n        end\n\n        it \"nilable\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid '(Array(Int32) | Nil)'.\" do\n            ACON::Input::Value::Number.new(123).get Array(Int32)?\n          end\n        end\n\n        it \"nilable generic value\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid '(Array(Int32 | Nil) | Nil)'.\" do\n            ACON::Input::Value::Number.new(123).get Array(Int32?)?\n          end\n        end\n      end\n\n      describe Bool do\n        it \"non-nilable\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid 'Array(Bool)'.\" do\n            ACON::Input::Value::Number.new(123).get Array(Bool)\n          end\n        end\n\n        it \"nilable\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid '(Array(Bool) | Nil)'.\" do\n            ACON::Input::Value::Number.new(123).get Array(Bool)?\n          end\n        end\n\n        it \"nilable generic value\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid '(Array(Bool | Nil) | Nil)'.\" do\n            ACON::Input::Value::Number.new(123).get Array(Bool?)?\n          end\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/input/value/string_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe ACON::Input::Value::String do\n  describe \"#get\" do\n    describe Bool do\n      describe \"non-nilable\" do\n        it \"true\" do\n          val = ACON::Input::Value::String.new(\"true\").get Bool\n          typeof(val).should eq Bool\n          val.should be_true\n        end\n\n        it \"false\" do\n          val = ACON::Input::Value::String.new(\"false\").get Bool\n          typeof(val).should eq Bool\n          val.should be_false\n        end\n\n        it \"invalid\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid 'Bool'.\" do\n            ACON::Input::Value::String.new(\"123\").get Bool\n          end\n        end\n      end\n\n      describe \"nilable\" do\n        it \"valid\" do\n          val = ACON::Input::Value::String.new(\"true\").get Bool?\n          typeof(val).should eq Bool?\n          val.should be_true\n        end\n\n        it \"invalid\" do\n          expect_raises ACON::Exception::Logic, \"'123' is not a valid 'Bool?'.\" do\n            ACON::Input::Value::String.new(\"123\").get Bool?\n          end\n        end\n      end\n    end\n\n    describe String do\n      it \"non-nilable\" do\n        val = ACON::Input::Value::String.new(\"foo\").get String\n        typeof(val).should eq String\n        val.should eq \"foo\"\n      end\n\n      it \"nilable\" do\n        val = ACON::Input::Value::String.new(\"foo\").get String?\n        typeof(val).should eq String?\n        val.should eq \"foo\"\n      end\n    end\n\n    describe Int do\n      it \"non-nilable\" do\n        string = ACON::Input::Value::String.new \"123\"\n\n        val = string.get Int32\n        typeof(val).should eq Int32\n        val.should eq 123\n\n        val = string.get(UInt8)\n        typeof(val).should eq UInt8\n        val.should eq 123_u8\n      end\n\n      it \"nilable\" do\n        string = ACON::Input::Value::String.new \"123\"\n\n        val = string.get Int32?\n        typeof(val).should eq Int32?\n        val.should eq 123\n\n        val = string.get UInt8?\n        typeof(val).should eq UInt8?\n        val.should eq 123_u8\n      end\n\n      it \"non number\" do\n        expect_raises ACON::Exception::Logic, \"'foo' is not a valid 'Int32'.\" do\n          ACON::Input::Value::String.new(\"foo\").get Int32\n        end\n\n        expect_raises ACON::Exception::Logic, \"'foo' is not a valid 'Int32'.\" do\n          ACON::Input::Value::String.new(\"foo\").get Int32?\n        end\n      end\n    end\n\n    describe Float do\n      it \"non-nilable\" do\n        string = ACON::Input::Value::String.new \"4.57\"\n\n        val = string.get Float64\n        typeof(val).should eq Float64\n        val.should eq 4.57\n\n        val = string.get Float32\n        typeof(val).should eq Float32\n        val.should eq 4.57_f32\n      end\n\n      it \"nilable\" do\n        string = ACON::Input::Value::String.new \"4.57\"\n\n        val = string.get Float64?\n        typeof(val).should eq Float64?\n        val.should eq 4.57\n\n        val = string.get Float32?\n        typeof(val).should eq Float32?\n        val.should eq 4.57_f32\n      end\n\n      it \"non number\" do\n        expect_raises ACON::Exception::Logic, \"'foo' is not a valid 'Float64'.\" do\n          ACON::Input::Value::String.new(\"foo\").get Float64\n        end\n\n        expect_raises ACON::Exception::Logic, \"'foo' is not a valid 'Float64'.\" do\n          ACON::Input::Value::String.new(\"foo\").get Float64?\n        end\n      end\n    end\n\n    describe Array do\n      describe String do\n        it \"non-nilable\" do\n          val = ACON::Input::Value::String.new(\"foo,bar,baz\").get Array(String)\n          typeof(val).should eq Array(String)\n          val.should eq [\"foo\", \"bar\", \"baz\"]\n        end\n\n        it \"nilable\" do\n          val = ACON::Input::Value::String.new(\"foo,bar,baz\").get Array(String)?\n          typeof(val).should eq Array(String)?\n          val.should eq [\"foo\", \"bar\", \"baz\"]\n        end\n\n        it \"nilable generic value\" do\n          val = ACON::Input::Value::String.new(\"foo,bar,baz\").get Array(String?)?\n          typeof(val).should eq Array(String?)?\n          val.should eq [\"foo\", \"bar\", \"baz\"]\n        end\n      end\n\n      describe Int32 do\n        it \"non-nilable\" do\n          val = ACON::Input::Value::String.new(\"1,2,3\").get Array(Int32)\n          typeof(val).should eq Array(Int32)\n          val.should eq [1, 2, 3]\n        end\n\n        it \"nilable\" do\n          val = ACON::Input::Value::String.new(\"1,2,3\").get Array(Int32)?\n          typeof(val).should eq Array(Int32)?\n          val.should eq [1, 2, 3]\n        end\n\n        it \"nilable generic value\" do\n          val = ACON::Input::Value::String.new(\"1,2,3\").get Array(Int32?)?\n          typeof(val).should eq Array(Int32?)?\n          val.should eq [1, 2, 3]\n        end\n      end\n\n      describe Bool do\n        it \"non-nilable\" do\n          val = ACON::Input::Value::String.new(\"false,true,true\").get Array(Bool)\n          typeof(val).should eq Array(Bool)\n          val.should eq [false, true, true]\n        end\n\n        it \"nilable\" do\n          val = ACON::Input::Value::String.new(\"false,true,true\").get Array(Bool)?\n          typeof(val).should eq Array(Bool)?\n          val.should eq [false, true, true]\n        end\n\n        it \"nilable generic value\" do\n          val = ACON::Input::Value::String.new(\"false,true,true\").get Array(Bool?)?\n          typeof(val).should eq Array(Bool?)?\n          val.should eq [false, true, true]\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/output/console_section_output_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct ConsoleSectionOutputTest < ASPEC::TestCase\n  @io : IO::Memory\n\n  def initialize\n    @io = IO::Memory.new\n  end\n\n  def test_adding_multiple_sections : Nil\n    sections = Array(ACON::Output::Section).new\n    ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n    ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    sections.size.should eq 2\n  end\n\n  def test_clear_all : Nil\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output.puts \"Foo#{EOL}Bar\"\n    output.clear\n\n    @io.to_s.should eq \"Foo#{EOL}Bar#{EOL}\\e[2A\\e[0J\"\n  end\n\n  def test_clear_number_of_lines : Nil\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output.puts \"Foo\\nBar\\nBaz\\nFooBar\"\n    output.clear 2\n\n    @io.to_s.should eq \"Foo\\nBar\\nBaz\\nFooBar#{EOL}\\e[2A\\e[0J\"\n  end\n\n  def test_clear_number_more_than_current_size : Nil\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output.puts \"Foo\"\n    output.clear 2\n\n    @io.to_s.should eq \"Foo#{EOL}\\e[2A\\e[0J\"\n  end\n\n  def test_clear_number_of_lines_multiple_sections : Nil\n    sections = Array(ACON::Output::Section).new\n    output1 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n    output2 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output2.puts \"Foo\"\n    output2.puts \"Bar\"\n    output2.clear 1\n    output1.puts \"Baz\"\n\n    @io.to_s.should eq \"Foo#{EOL}Bar#{EOL}\\e[1A\\e[0J\\e[1A\\e[0JBaz#{EOL}Foo#{EOL}\"\n  end\n\n  def test_clear_number_of_lines_multiple_sections_preserves_empty_lines : Nil\n    sections = Array(ACON::Output::Section).new\n    output1 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n    output2 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output2.puts \"#{EOL}foo\"\n    output2.clear 1\n    output1.puts \"bar\"\n\n    @io.to_s.should eq \"#{EOL}foo#{EOL}\\e[1A\\e[0J\\e[1A\\e[0Jbar#{EOL}#{EOL}\"\n  end\n\n  def test_clear_with_question : Nil\n    input = ACON::Input::Hash.new\n    input.stream = IO::Memory.new \"Batman & Robin\\n\"\n    input.interactive = true\n\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    ACON::Helper::Question.new.ask input, output, ACON::Question(String?).new(\"What's your favorite superhero?\", nil)\n    output.clear\n\n    @io.to_s.should eq \"What's your favorite superhero?#{EOL}\\e[2A\\e[0J\"\n  end\n\n  def test_clear_after_overwrite_clear_correct_number_of_lines : Nil\n    expected = IO::Memory.new\n\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output.overwrite \"foo\"\n    expected << \"foo\" << EOL\n\n    output.clear\n    expected << \"\\e[1A\\e[0J\"\n\n    output.overwrite \"biz\", \"baz\"\n    expected << \"biz\" << EOL << \"baz\" << EOL\n\n    @io.to_s.should eq expected.to_s\n  end\n\n  def test_overwrite : Nil\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output.puts \"Foo\"\n    output.overwrite \"Bar\"\n\n    @io.to_s.should eq \"Foo#{EOL}\\e[1A\\e[0JBar#{EOL}\"\n  end\n\n  def test_overwrite_multiple_lines : Nil\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output.puts \"Foo#{EOL}Bar#{EOL}Baz\"\n    output.overwrite \"Bar\"\n\n    @io.to_s.should eq \"Foo#{EOL}Bar#{EOL}Baz#{EOL}\\e[3A\\e[0JBar#{EOL}\"\n  end\n\n  def test_overwrite_multiple_section_output : Nil\n    sections = Array(ACON::Output::Section).new\n    output1 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n    output2 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output1.puts \"Foo\"\n    output2.puts \"Bar\"\n\n    output1.overwrite \"Baz\"\n    output2.overwrite \"Foobar\"\n\n    @io.to_s.should eq \"Foo#{EOL}Bar#{EOL}\\e[2A\\e[0JBar#{EOL}\\e[1A\\e[0JBaz#{EOL}Bar#{EOL}\\e[1A\\e[0JFoobar#{EOL}\"\n  end\n\n  def test_max_height : Nil\n    expected = IO::Memory.new\n\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n    output.max_height = 3\n\n    # Fill the section\n    output.puts({\"One\", \"Two\", \"Three\"})\n    expected << \"One\" << EOL << \"Two\" << EOL << \"Three\" << EOL\n\n    # Cause overflow that'll redraw whole section, without the first line\n    output.puts \"Four\"\n    expected << \"\\e[3A\\e[0J\"\n    expected << \"Two\" << EOL << \"Three\" << EOL << \"Four\" << EOL\n\n    # Cause overflow with multiple new lines at once\n    output.puts \"Five#{EOL}Six\"\n    expected << \"\\e[3A\\e[0J\"\n    expected << \"Four\" << EOL << \"Five\" << EOL << \"Six\" << EOL\n\n    # Reset line height that'll redraw whole section, displaying all lines\n    output.max_height = nil\n    expected << \"\\e[3A\\e[0J\"\n    expected << \"One\" << EOL << \"Two\" << EOL << \"Three\" << EOL\n    expected << \"Four\" << EOL << \"Five\" << EOL << \"Six\" << EOL\n\n    @io.to_s.should eq expected.to_s\n  end\n\n  def test_max_height_multiple_sections : Nil\n    expected = IO::Memory.new\n\n    sections = Array(ACON::Output::Section).new\n    output1 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n    output1.max_height = 3\n\n    output2 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n    output2.max_height = 3\n\n    # Fill the first section\n    output1.puts({\"One\", \"Two\", \"Three\"})\n    expected << \"One\" << EOL << \"Two\" << EOL << \"Three\" << EOL\n\n    # Fill the second section\n    output2.puts({\"One\", \"Two\", \"Three\"})\n    expected << \"One\" << EOL << \"Two\" << EOL << \"Three\" << EOL\n\n    # Cause overflow on second section that'll redraw whole section, without the first line\n    output2.puts \"Four\"\n    expected << \"\\e[3A\\e[0J\"\n    expected << \"Two\" << EOL << \"Three\" << EOL << \"Four\" << EOL\n\n    # Cause overflow on first section that'll redraw whole section, without the first line\n    output1.puts \"Four#{EOL}Five#{EOL}Six\"\n    expected << \"\\e[6A\\e[0J\"\n    expected << \"Four\" << EOL << \"Five\" << EOL << \"Six\" << EOL\n    expected << \"Two\" << EOL << \"Three\" << EOL << \"Four\" << EOL\n\n    @io.to_s.should eq expected.to_s\n  end\n\n  def test_max_height_without_new_line : Nil\n    expected = IO::Memory.new\n\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n    output.max_height = 3\n\n    # Fill the section\n    output.puts({\"One\", \"Two\"})\n    output.print \"Three\"\n    expected << \"One\" << EOL << \"Two\" << EOL << \"Three\" << EOL\n\n    # Append text to the last line\n    output.print \" and Four\"\n    expected << \"\\e[1A\\e[0J\" << \"Three and Four\" << EOL\n\n    @io.to_s.should eq expected.to_s\n  end\n\n  def test_write_without_new_line : Nil\n    sections = Array(ACON::Output::Section).new\n    output = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output.print \"Foo#{EOL}\"\n    output.print \"Bar\"\n\n    @io.to_s.should eq \"Foo#{EOL}Bar#{EOL}\"\n  end\n\n  def test_write_multiple_sections_output_without_new_lines : Nil\n    expected = IO::Memory.new\n\n    sections = Array(ACON::Output::Section).new\n    output1 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n    output2 = ACON::Output::Section.new @io, sections, :normal, true, ACON::Formatter::Output.new\n\n    output1.print \"Foo\"\n    expected << \"Foo\" << EOL\n\n    output2.puts \"Bar\"\n    expected << \"Bar\" << EOL\n\n    output1.puts \" is not foo.\"\n    expected << \"\\e[2A\\e[0JFoo is not foo.\" << EOL << \"Bar\" << EOL\n\n    output2.print \"Baz\"\n    expected << \"Baz\" << EOL\n\n    output2.print \"bar\"\n    expected << \"\\e[1A\\e[0JBazbar\" << EOL\n\n    output2.puts \"\"\n    expected << \"\\e[1A\\e[0JBazbar\" << EOL\n\n    output2.puts \"\"\n    expected << EOL\n\n    output2.puts \"Done.\"\n    expected << \"Done.\" << EOL\n\n    @io.to_s.should eq expected.to_s\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/output/io_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct IOTest < ASPEC::TestCase\n  @io : IO::Memory\n\n  def initialize\n    @io = IO::Memory.new\n  end\n\n  def tear_down : Nil\n    @io.clear\n  end\n\n  def test_do_write : Nil\n    output = ACON::Output::IO.new @io\n    output.puts \"foo\"\n    output.print \"bar\"\n    output.to_s.should eq \"foo#{EOL}bar\"\n  end\n\n  def test_do_write_var_args : Nil\n    output = ACON::Output::IO.new @io\n    output.puts \"foo\", \"bar\"\n    output.print \"biz\", \"baz\"\n    output.to_s.should eq \"foo#{EOL}bar#{EOL}bizbaz\"\n  end\n\n  def test_decorated_dumb_term : Nil\n    with_isolated_env do\n      ENV[\"TERM\"] = \"dumb\"\n      ACON::Output::IO.new(@io).decorated?.should be_false\n    end\n  end\n\n  def test_decorated_no_color : Nil\n    with_isolated_env do\n      ENV[\"NO_COLOR\"] = \"true\"\n      ENV[\"COLORTERM\"] = \"truecolor\"\n      ACON::Output::IO.new(@io).decorated?.should be_false\n    end\n  end\n\n  def test_decorated_no_color_empty : Nil\n    with_isolated_env do\n      ENV[\"NO_COLOR\"] = \"\"\n      ENV[\"COLORTERM\"] = \"truecolor\"\n      ACON::Output::IO.new(@io).decorated?.should be_true\n    end\n  end\n\n  def test_decorated_force_color : Nil\n    with_isolated_env do\n      ENV[\"FORCE_COLOR\"] = \"true\"\n      ACON::Output::IO.new(@io).decorated?.should be_true\n    end\n  end\n\n  def test_decorated_force_color_empty : Nil\n    with_isolated_env do\n      ENV[\"FORCE_COLOR\"] = \"\"\n      ACON::Output::IO.new(@io).decorated?.should be_false\n    end\n  end\n\n  def test_decorated_supported_term : Nil\n    with_isolated_env do\n      ENV[\"TERM\"] = \"xterm-256color\"\n      ACON::Output::IO.new(@io).decorated?.should be_true\n    end\n  end\n\n  def test_decorated_colorterm : Nil\n    with_isolated_env do\n      ENV[\"COLORTERM\"] = \"truecolor\"\n      ACON::Output::IO.new(@io).decorated?.should be_true\n    end\n  end\n\n  def test_decorated_ansicon : Nil\n    with_isolated_env do\n      ENV[\"ANSICON\"] = \"1\"\n      ACON::Output::IO.new(@io).decorated?.should be_true\n    end\n  end\n\n  def test_decorated_conemuansi : Nil\n    with_isolated_env do\n      ENV[\"ConEmuANSI\"] = \"ON\"\n      ACON::Output::IO.new(@io).decorated?.should be_true\n    end\n  end\n\n  def test_decorated_term_program_hyper : Nil\n    with_isolated_env do\n      ENV[\"TERM_PROGRAM\"] = \"Hyper\"\n      ACON::Output::IO.new(@io).decorated?.should be_true\n    end\n  end\n\n  def test_decorated_term_program_non_hyper : Nil\n    with_isolated_env do\n      ENV[\"TERM_PROGRAM\"] = \"WezTerm\"\n      ACON::Output::IO.new(@io).decorated?.should be_false\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/output/null_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct NullSpec < ASPEC::TestCase\n  def test_verbosity : Nil\n    ACON::Output::Null.new.verbosity.silent?.should be_true\n  end\n\n  def test_formatter : Nil\n    output = ACON::Output::Null.new\n    output.formatter.should be_a ACON::Formatter::Null\n    output.formatter = ACON::Formatter::Output.new\n    output.formatter.should be_a ACON::Formatter::Null\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/output/output_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class MockOutput < ACON::Output\n  getter output : String = \"\"\n\n  def clear : Nil\n    @output = \"\"\n  end\n\n  protected def do_write(message : String, new_line : Bool) : Nil\n    @output += message\n    @output += \"\\n\" if new_line\n  end\nend\n\nstruct OutputTest < ASPEC::TestCase\n  def test_write_verbosity_quiet : Nil\n    output = MockOutput.new :quiet\n    output.puts \"foo\"\n    output.output.should be_empty\n  end\n\n  def test_write_array_messages : Nil\n    output = MockOutput.new\n    output.puts [\"foo\", \"bar\"]\n    output.output.should eq \"foo\\nbar\\n\"\n  end\n\n  @[DataProvider(\"message_provider\")]\n  def test_write_raw_message(message : String, output_type : ACON::Output::Type, expected : String) : Nil\n    output = MockOutput.new\n    output.puts message, output_type: output_type\n    output.output.should eq expected\n  end\n\n  def message_provider : Tuple\n    {\n      {\"<info>foo</info>\", ACON::Output::Type::RAW, \"<info>foo</info>\\n\"},\n      {\"<info>foo</info>\", ACON::Output::Type::PLAIN, \"foo\\n\"},\n    }\n  end\n\n  def test_write_non_decorated : Nil\n    output = MockOutput.new\n    output.decorated = false\n    output.puts \"<info>foo</info>\"\n    output.output.should eq \"foo\\n\"\n  end\n\n  def test_write_decorated : Nil\n    foo_style = ACON::Formatter::OutputStyle.new :yellow, :red, :blink\n    output = MockOutput.new\n    output.formatter.has_style?(\"FOO\").should be_false\n    output.formatter.set_style \"FOO\", foo_style\n    output.formatter.has_style?(\"FOO\").should be_true\n    output.decorated = true\n    output.puts \"<foo>foo</foo>\"\n    output.output.should eq \"\\e[33;41;5mfoo\\e[39;49;25m\\n\"\n  end\n\n  def test_write_decorated_invalid_style : Nil\n    output = MockOutput.new\n    output.puts \"<bar>foo</bar>\"\n    output.output.should eq \"<bar>foo</bar>\\n\"\n  end\n\n  @[DataProvider(\"verbosity_provider\")]\n  def test_write_with_verbosity(verbosity : ACON::Output::Verbosity, expected : String) : Nil\n    output = MockOutput.new\n\n    output.verbosity = verbosity\n    output.print \"1\"\n    output.print \"2\", :quiet\n    output.print \"3\", :normal\n    output.print \"4\", :verbose\n    output.print \"5\", :very_verbose\n    output.print \"6\", :debug\n\n    output.output.should eq expected\n  end\n\n  def verbosity_provider : Tuple\n    {\n      {ACON::Output::Verbosity::SILENT, \"\"},\n      {ACON::Output::Verbosity::QUIET, \"2\"},\n      {ACON::Output::Verbosity::NORMAL, \"123\"},\n      {ACON::Output::Verbosity::VERBOSE, \"1234\"},\n      {ACON::Output::Verbosity::VERY_VERBOSE, \"12345\"},\n      {ACON::Output::Verbosity::DEBUG, \"123456\"},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/question/choice_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct ChoiceQuestionTest < ASPEC::TestCase\n  def test_new_empty_choices : Nil\n    expect_raises ACON::Exception::Logic, \"Choice questions must have at least 1 choice available.\" do\n      ACON::Question::Choice.new \"A question\", Array(String).new\n    end\n  end\n\n  def test_custom_validator : Nil\n    question = ACON::Question::Choice.new(\n      \"A question\",\n      [\n        \"First response\",\n        \"Second response\",\n        \"Third response\",\n        \"Fourth response\",\n      ]\n    )\n\n    question.validator do\n      \"FOO\"\n    end\n\n    {\"First response\", \"First response \", \" First response\", \" First response \"}.each do |answer|\n      if validator = question.validator\n        actual = validator.call answer\n\n        actual.should eq \"FOO\"\n      end\n    end\n  end\n\n  def test_validator_exact_match : Nil\n    question = ACON::Question::Choice.new(\n      \"A question\",\n      [\n        \"First response\",\n        \"Second response\",\n        \"Third response\",\n        \"Fourth response\",\n      ]\n    )\n\n    {\"First response\", \"First response \", \" First response\", \" First response \"}.each do |answer|\n      if validator = question.validator\n        validator.call(answer).should eq \"First response\"\n      end\n    end\n  end\n\n  def test_validator_index_match : Nil\n    question = ACON::Question::Choice.new(\n      \"A question\",\n      [\n        \"First response\",\n        \"Second response\",\n        \"Third response\",\n        \"Fourth response\",\n      ]\n    )\n\n    {\"0\"}.each do |answer|\n      if validator = question.validator\n        validator.call(answer).should eq \"First response\"\n      end\n    end\n  end\n\n  def test_non_trimmable : Nil\n    question = ACON::Question::Choice.new(\n      \"A question\",\n      [\n        \"First response \",\n        \" Second response\",\n        \"  Third response  \",\n      ]\n    )\n\n    question.trimmable = false\n\n    if validator = question.validator\n      validator.not_nil!.call(\"  Third response  \").should eq \"  Third response  \"\n    end\n  end\n\n  @[DataProvider(\"hash_choice_provider\")]\n  def test_validator_hash_choices(answer : String, expected : String) : Nil\n    question = ACON::Question::Choice.new(\n      \"A question\",\n      {\n        \"0\"   => \"First choice\",\n        \"foo\" => \"Foo\",\n        \"99\"  => \"N°99\",\n      }\n    )\n\n    if validator = question.validator\n      validator.call(answer).should eq expected\n    end\n  end\n\n  def hash_choice_provider : Hash\n    {\n      \"'0' choice by key\"            => {\"0\", \"First choice\"},\n      \"'0' choice by value\"          => {\"First choice\", \"First choice\"},\n      \"select by key\"                => {\"foo\", \"Foo\"},\n      \"select by value\"              => {\"Foo\", \"Foo\"},\n      \"select by key, numeric key\"   => {\"99\", \"N°99\"},\n      \"select by value, numeric key\" => {\"N°99\", \"N°99\"},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/question/confirmation_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct ConfirmationQuestionTest < ASPEC::TestCase\n  @[DataProvider(\"normalizer_provider\")]\n  def test_default_regex(default : Bool, answers : Array, expected : Bool) : Nil\n    question = ACON::Question::Confirmation.new \"A question\", default\n\n    answers.each do |answer|\n      normalizer = question.normalizer.not_nil!\n      actual = normalizer.call answer\n      actual.should eq expected\n    end\n  end\n\n  def normalizer_provider : Tuple\n    {\n      {\n        true,\n        [\"y\", \"Y\", \"yes\", \"YES\", \"yEs\", \"\"],\n        true,\n      },\n      {\n        true,\n        [\"n\", \"N\", \"no\", \"NO\", \"nO\", \"foo\", \"1\", \"0\"],\n        false,\n      },\n      {\n        false,\n        [\"y\", \"Y\", \"yes\", \"YES\", \"yEs\"],\n        true,\n      },\n      {\n        false,\n        [\"n\", \"N\", \"no\", \"NO\", \"nO\", \"foo\", \"1\", \"0\", \"\"],\n        false,\n      },\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/question/multiple_choice_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct MultipleChoiceQuestionTest < ASPEC::TestCase\n  def test_new_empty_choices : Nil\n    expect_raises ACON::Exception::Logic, \"Choice questions must have at least 1 choice available.\" do\n      ACON::Question::MultipleChoice.new \"A question\", Array(String).new\n    end\n  end\n\n  def test_non_trimmable : Nil\n    question = ACON::Question::MultipleChoice(String).new(\n      \"A question\",\n      [\n        \"First response \",\n        \" Second response\",\n        \"  Third response  \",\n      ]\n    )\n\n    question.trimmable = false\n\n    question.validator.not_nil!.call(\"First response , Second response\").should eq [\"First response \", \" Second response\"]\n  end\n\n  @[DataProvider(\"hash_choice_provider\")]\n  def test_validator_hash_choices(answer : String, expected : Array) : Nil\n    question = ACON::Question::MultipleChoice.new(\n      \"A question\",\n      {\n        \"0\"   => \"First choice\",\n        \"foo\" => \"Foo\",\n        \"99\"  => \"N°99\",\n      }\n    )\n\n    question.validator.not_nil!.call(answer).should eq expected\n  end\n\n  def hash_choice_provider : Hash\n    {\n      \"'0' choice by key - multiple\" => {\"0,Foo\", [\"First choice\", \"Foo\"]},\n      \"'0' choice by key- single\"    => {\"foo\", [\"Foo\"]},\n      \"select by value, numeric key\" => {\"N°99,foo,First choice\", [\"N°99\", \"Foo\", \"First choice\"]},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/question/question_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class QuestionCommand < ACON::Command\n  protected def configure : Nil\n    self\n      .name(\"question:command\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    name = self.helper(ACON::Helper::Question).ask input, output, ACON::Question(String?).new \"What is your name?\", nil\n\n    output.puts \"Your name is: #{name}\"\n\n    ACON::Command::Status::SUCCESS\n  end\nend\n\nstruct QuestionTest < ASPEC::TestCase\n  @question : ACON::Question(String?)\n\n  def initialize\n    @question = ACON::Question(String?).new \"Test Question\", nil\n  end\n\n  @[Tags(\"compiled\")]\n  def test_nil_generic_arg : Nil\n    ASPEC::Methods.assert_compile_time_error \"An ACON::Question generic argument cannot be 'Nil'. Use 'String?' instead.\", <<-CR\n      require \"../spec_helper.cr\"\n\n      ACON::Question(Nil).new \"Nil Question\", nil\n    CR\n  end\n\n  def test_default : Nil\n    @question.default.should be_nil\n    default = ACON::Question(String).new(\"Test Question\", \"FOO\").default\n    default.should eq \"FOO\"\n    typeof(default).should eq String\n  end\n\n  def test_hidden_autocompleter_callback : Nil\n    @question.autocompleter_callback do\n      [] of String\n    end\n\n    expect_raises ACON::Exception::Logic, \"A hidden question cannot use the autocompleter\" do\n      @question.hidden = true\n    end\n  end\n\n  @[DataProvider(\"autocompleter_values_provider\")]\n  def test_get_set_autocompleter_values(values : Indexable | Hash, expected : Array(String)) : Nil\n    @question.autocompleter_values = values\n\n    @question.autocompleter_values.should eq expected\n  end\n\n  def autocompleter_values_provider : Hash\n    {\n      \"tuple\" => {\n        {\"a\", \"b\", \"c\"},\n        [\"a\", \"b\", \"c\"],\n      },\n      \"array\" => {\n        [\"a\", \"b\", \"c\"],\n        [\"a\", \"b\", \"c\"],\n      },\n      \"string key hash\" => {\n        {\"a\" => \"b\", \"c\" => \"d\"},\n        [\"a\", \"c\", \"b\", \"d\"],\n      },\n      \"int key hash\" => {\n        {0 => \"b\", 1 => \"d\"},\n        [\"b\", \"d\"],\n      },\n    }\n  end\n\n  def test_custom_normalizer : Nil\n    question = ACON::Question(String).new \"A question\", \"\"\n\n    question.normalizer do |val|\n      val.upcase\n    end\n\n    if normalizer = question.normalizer\n      normalizer.call(\"foo\").should eq \"FOO\"\n    end\n  end\n\n  def test_with_inputs : Nil\n    command = QuestionCommand.new\n    command.helper_set = ACON::Helper::HelperSet.new ACON::Helper::Question.new\n    tester = ACON::Spec::CommandTester.new command\n    tester.inputs \"Jim\"\n    tester.execute\n    tester.display.should eq \"What is your name?Your name is: Jim#{EOL}\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/spec_helper.cr",
    "content": "require \"spec\"\n\nrequire \"../src/athena-console\"\nrequire \"../src/spec\"\n\nrequire \"athena-spec\"\nrequire \"athena-clock/spec\"\n\nrequire \"./fixtures/commands/io\"\nrequire \"./fixtures/**\"\n\n# Spec by default disables colorize with `TERM=dumb`.\n# Override that given there are specs based on ansi output.\nColorize.enabled = true\n\nstruct MockCommandLoader\n  include Athena::Console::Loader::Interface\n\n  def initialize(\n    *,\n    @command_or_exception : ACON::Command | ::Exception? = nil,\n    @has : Bool = true,\n    @names : Array(String) | ::Exception = [] of String,\n  )\n  end\n\n  def get(name : String) : ACON::Command\n    case v = @command_or_exception\n    in ::Exception   then raise v\n    in ACON::Command then v\n    in Nil           then raise \"BUG: no command or exception was set\"\n    end\n  end\n\n  def has?(name : String) : Bool\n    @has\n  end\n\n  def names : Array(String)\n    case v = @names\n    in ::Exception   then raise v\n    in Array(String) then v\n    end\n  end\nend\n\ndef with_isolated_env(&) : Nil\n  old_values = ENV.to_h\n\n  begin\n    ENV.clear\n\n    yield\n  ensure\n    ENV.clear\n    old_values.each do |key, old_value|\n      ENV[key] = old_value\n    end\n  end\nend\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/console/spec/style/athena_style_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct AthenaStyleTest < ASPEC::TestCase\n  def initialize\n    ENV[\"COLUMNS\"] = \"121\"\n  end\n\n  def tear_down : Nil\n    ENV.delete \"COLUMNS\"\n  end\n\n  private def assert_file_equals_string(filepath : String, string : String, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    normalized_path = File.join __DIR__, \"..\", \"fixtures\", filepath\n    string.should match(Regex.new(File.read(normalized_path).gsub EOL, \"\\n\")), file: file, line: line\n  end\n\n  def test_error_style : Nil\n    error_output = ACON::Output::IO.new io = IO::Memory.new\n    output = ACON::Output::ConsoleOutput.new\n    output.stderr = error_output\n\n    style = ACON::Style::Athena.new ACON::Input::Hash.new({} of String => String), output\n    style.error_style.puts \"foo\"\n\n    io.to_s.should eq \"foo#{EOL}\"\n  end\n\n  def test_error_style_non_console_output : Nil\n    output = ACON::Output::IO.new io = IO::Memory.new\n\n    style = ACON::Style::Athena.new ACON::Input::Hash.new({} of String => String), output\n    style.error_style.puts \"foo\"\n\n    io.to_s.should eq \"foo#{EOL}\"\n  end\n\n  @[DataProvider(\"output_provider\")]\n  def test_outputs(command_proc : ACON::Commands::Generic::Proc, file_path : String) : Nil\n    command = ACON::Commands::Generic.new \"foo\", &command_proc\n\n    tester = ACON::Spec::CommandTester.new command\n\n    tester.execute interactive: false, decorated: false\n    self.assert_file_equals_string file_path, tester.display true\n  end\n\n  def output_provider : Hash\n    {\n      \"Single blank line at start with block element\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          ACON::Style::Athena.new(input, output).caution \"Lorem ipsum dolor sit amet\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/block.txt\",\n      },\n      \"Single blank line between titles and blocks\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.title \"Title\"\n          style.warning \"Lorem ipsum dolor sit amet\"\n          style.title \"Title\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/title_block.txt\",\n      },\n      \"Single blank line between blocks\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.warning \"Warning\"\n          style.caution \"Caution\"\n          style.error \"Error\"\n          style.success \"Success\"\n          style.note \"Note\"\n          style.info \"Info\"\n          style.block \"Custom block\", \"CUSTOM\", style: \"fg=white;bg=green\", prefix: \"X \", padding: true\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/blocks.txt\",\n      },\n      \"Single blank line between titles\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.title \"First title\"\n          style.title \"Second title\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/titles.txt\",\n      },\n      \"Single blank line after any text and a title\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.print \"Lorem ipsum dolor sit amet\"\n          style.title \"First title\"\n\n          style.puts \"Lorem ipsum dolor sit amet\"\n          style.title \"Second title\"\n\n          style.print \"Lorem ipsum dolor sit amet\"\n          style.print \"\"\n          style.title \"Third title\"\n\n          # Handle edge case by appending empty strings to history\n          style.print \"Lorem ipsum dolor sit amet\"\n          style.print({\"\", \"\", \"\"})\n          style.title \"Fourth title\"\n\n          # Ensure manual control over number of blank lines\n          style.puts \"Lorem ipsum dolor sit amet\"\n          style.puts({\"\", \"\"}) # Should print 1 extra newline\n          style.title \"Fifth title\"\n\n          style.puts \"Lorem ipsum dolor sit amet\"\n          style.new_line 2 # Should print 1 extra newline\n          style.title \"Sixth title\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/titles_text.txt\",\n      },\n      \"Proper line endings before outputting a text block\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.puts \"Lorem ipsum dolor sit amet\"\n          style.listing \"Lorem ipsum dolor sit amet\", \"consectetur adipiscing elit\"\n\n          # When using print\n          style.print \"Lorem ipsum dolor sit amet\"\n          style.listing \"Lorem ipsum dolor sit amet\", \"consectetur adipiscing elit\"\n\n          style.print \"Lorem ipsum dolor sit amet\"\n          style.text({\"Lorem ipsum dolor sit amet\", \"consectetur adipiscing elit\"})\n\n          style.new_line\n\n          style.print \"Lorem ipsum dolor sit amet\"\n          style.comment({\"Lorem ipsum dolor sit amet\", \"consectetur adipiscing elit\"})\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/block_line_endings.txt\",\n      },\n      \"Proper blank line after text block with block\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.listing \"Lorem ipsum dolor sit amet\", \"consectetur adipiscing elit\"\n          style.success \"Lorem ipsum dolor sit amet\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/text_block_blank_line.txt\",\n      },\n      \"Questions do not output anything when input is non-interactive\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.title \"Title\"\n          style.ask_hidden \"Hidden question\"\n          style.choice \"Choice question with default\", {\"choice1\", \"choice2\"}, \"choice1\"\n          style.confirm \"Confirmation with yes default\", true\n          style.text \"Duis aute irure dolor in reprehenderit in voluptate velit esse\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/non_interactive_question.txt\",\n      },\n      \"non-hidden questions do not output anything when input is non-interactive\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.title \"Title\"\n          style.ask \"Hidden question\", nil\n          style.choice \"Choice question with default\", {\"choice1\", \"choice2\"}, \"choice1\"\n          style.confirm \"Confirmation with yes default\", true\n          style.text \"Duis aute irure dolor in reprehenderit in voluptate velit esse\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/non_interactive_question.txt\",\n      },\n      # TODO: Test table formatting with multiple headers + TableCell\n      \"Lines are aligned to the beginning of the first line in a multi-line block\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          ACON::Style::Athena.new(input, output).block({\"Custom block\", \"Second custom block line\"}, \"CUSTOM\", style: \"fg=white;bg=green\", prefix: \"X \", padding: true)\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/multi_line_block.txt\",\n      },\n      \"Lines are aligned to the beginning of the first line in a very long line block\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          ACON::Style::Athena.new(input, output).block(\n            \"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum\",\n            \"CUSTOM\",\n            style: \"fg=white;bg=green\",\n            prefix: \"X \",\n            padding: true\n          )\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/long_line_block.txt\",\n      },\n      \"Long lines are wrapped within a block\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          ACON::Style::Athena.new(input, output).block(\n            \"Lopadotemachoselachogaleokranioleipsanodrimhypotrimmatosilphioparaomelitokatakechymenokichlepikossyphophattoperisteralektryonoptekephalliokigklopeleiolagoiosiraiobaphetraganopterygon\",\n            \"CUSTOM\",\n            style: \"fg=white;bg=green\",\n            prefix: \" § \",\n          )\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/long_line_block_wrapping.txt\",\n      },\n      \"Lines are aligned to the first line and start with '//' in a very long line comment\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          ACON::Style::Athena.new(input, output).comment(\n            \"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum\"\n          )\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/long_line_comment.txt\",\n      },\n      \"Nested tags have no effect on the color of the '//' prefix\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          output.decorated = true\n          ACON::Style::Athena.new(input, output).comment(\n            \"Árvíztűrőtükörfúrógép 🎼 Lorem ipsum dolor sit <comment>💕 amet, consectetur adipisicing elit, sed do eiusmod tempor incididu labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</comment> Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum\"\n          )\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/long_line_comment_decorated.txt\",\n      },\n      \"Block behaves properly with a prefix and without type\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          ACON::Style::Athena.new(input, output).block(\n            \"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum\",\n            prefix: \"$ \"\n          )\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/block_prefix_no_type.txt\",\n      },\n      \"Block behaves properly with a type and without prefix\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          ACON::Style::Athena.new(input, output).block(\n            \"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum\",\n            type: \"TEST\"\n          )\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/block_no_prefix_type.txt\",\n      },\n      \"Block output is properly formatted with even padding lines\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          output.decorated = true\n          ACON::Style::Athena.new(input, output).success(\n            \"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum\",\n          )\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/block_padding.txt\",\n      },\n      \"Handles trailing backslashes\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.title \"Title ending with \\\\\"\n          style.section \"Section ending with \\\\\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/backslashes.txt\",\n      },\n      \"definition list\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style\n            .definition_list(\n              {\"foo\" => \"bar\"},\n              ACON::Helper::Table::Separator.new,\n              \"this is a title\",\n              ACON::Helper::Table::Separator.new,\n              {\"foo2\" => \"bar2\"}\n            )\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/definition_list.txt\",\n      },\n      \"horizontal table\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.horizontal_table([\"a\", \"b\", \"c\", \"d\"], [[1, 2, 3], [4, 5], [7, 8, 9]])\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/horizontal_table.txt\",\n      },\n      \"Closing tag is only applied once\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          output.decorated = true\n          style = ACON::Style::Athena.new input, output\n          style.print \"<question>do you want <comment>something</>\"\n          style.puts \"?</>\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/closing_tag.txt\",\n      },\n      # TODO: Enable this test case when multi width char support is added.\n      #   \"Emojis don't make the line longer than expected\" => {\n      #     (ACON::Commands::Generic::Proc.new do |input, output|\n      #       style = ACON::Style::Athena.new input, output\n      #       style.success \"Lorem ipsum dolor sit amet\"\n      #       style.success \"Lorem ipsum dolor sit amet with one emoji 🎉\"\n      #       style.success \"Lorem ipsum dolor sit amet with so many of them 👩‍🌾👩‍🌾👩‍🌾👩‍🌾👩‍🌾\"\n\n      #       ACON::Command::Status::SUCCESS\n      #     end),\n      #     \"style/emojis.txt\",\n      #   },\n      \"Nested tags have no effect on color of the '//' prefix\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          output.decorated = true\n\n          ACON::Style::Athena.new(input, output).block(\n            \"Árvíztűrőtükörfúrógép Lorem ipsum dolor sit <comment>amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</comment> Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum\",\n            type: \"★\",\n            prefix: \"<fg=default;bg=default> ║ </>\",\n            escape: false\n          )\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/nested_tag_prefix.txt\",\n      },\n      \"Do not prepend empty line if the buffer is empty\" => {\n        (ACON::Commands::Generic::Proc.new do |input, output|\n          style = ACON::Style::Athena.new input, output\n          style.text \"Hello\"\n\n          ACON::Command::Status::SUCCESS\n        end),\n        \"style/empty_buffer.txt\",\n      },\n    }\n  end\n\n  def test_create_table : Nil\n    command = ACON::Commands::Generic.new \"foo\" do |input, output|\n      output.decorated = true\n      style = ACON::Style::Athena.new input, output\n\n      style\n        .create_table\n        .headers([\"Foo\", \"Bar\"])\n        .rows([[\"Biz\", \"Baz\"], [12, false]])\n        .render\n\n      style.new_line\n\n      ACON::Command::Status::SUCCESS\n    end\n\n    tester = ACON::Spec::CommandTester.new command\n\n    tester.execute interactive: false, decorated: false\n    self.assert_file_equals_string \"style/table.txt\", tester.display true\n  end\n\n  def test_table : Nil\n    command = ACON::Commands::Generic.new \"foo\" do |input, output|\n      output.decorated = true\n      style = ACON::Style::Athena.new input, output\n\n      style.table [\"Foo\", \"Bar\"], [[\"Biz\", \"Baz\"], [12, false]]\n\n      ACON::Command::Status::SUCCESS\n    end\n\n    tester = ACON::Spec::CommandTester.new command\n\n    tester.execute interactive: false, decorated: false\n    self.assert_file_equals_string \"style/table.txt\", tester.display true\n  end\n\n  def test_horizontal_table : Nil\n    command = ACON::Commands::Generic.new \"foo\" do |input, output|\n      output.decorated = true\n      style = ACON::Style::Athena.new input, output\n\n      style.horizontal_table [\"Foo\", \"Bar\"], [[\"Biz\", \"Baz\"], [12, false]]\n\n      ACON::Command::Status::SUCCESS\n    end\n\n    tester = ACON::Spec::CommandTester.new command\n\n    tester.execute interactive: false, decorated: false\n    self.assert_file_equals_string \"style/table_horizontal.txt\", tester.display true\n  end\n\n  def test_vertical_table : Nil\n    command = ACON::Commands::Generic.new \"foo\" do |input, output|\n      output.decorated = true\n      style = ACON::Style::Athena.new input, output\n\n      style.vertical_table [\"Foo\", \"Bar\"], [[\"Biz\", \"Baz\"], [12, false]]\n\n      ACON::Command::Status::SUCCESS\n    end\n\n    tester = ACON::Spec::CommandTester.new command\n\n    tester.execute interactive: false, decorated: false\n    self.assert_file_equals_string \"style/table_vertical.txt\", tester.display true\n  end\n\n  def test_customize_formatter : Nil\n    output = ACON::Output::IO.new IO::Memory.new\n    style = ACON::Style::Athena.new ACON::Input::Hash.new({} of String => String), output\n\n    style.formatter = ACON::Formatter::Null.new\n  end\n\n  def test_progress_bar : Nil\n    output = ACON::Output::IO.new IO::Memory.new\n    style = ACON::Style::Athena.new ACON::Input::Hash.new({} of String => String), output\n\n    style.progress_start 123\n    bar = style.progress_bar.not_nil!\n    bar.max_steps.should eq 123\n    bar.progress.should eq 0\n\n    style.progress_advance 4\n    bar.progress.should eq 4\n\n    style.progress_finish\n\n    style.progress_iterate([1, 2, 3]) { }\n\n    output = output.to_s\n    output.should contain \"0/123\"\n    output.should contain \"123/123\"\n\n    output.should contain \"0/3\"\n    output.should contain \"3/3\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/spec/terminal_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct TerminalTest < ASPEC::TestCase\n  @col_size : Int32?\n  @line_size : Int32?\n\n  def initialize\n    @col_size = ENV[\"COLUMNS\"]?.try &.to_i?\n    @line_size = ENV[\"LINES\"]?.try &.to_i?\n  end\n\n  def tear_down : Nil\n    ENV.delete \"COLUMNS\"\n    ENV.delete \"LINES\"\n  end\n\n  def test_height_width : Nil\n    ENV[\"COLUMNS\"] = \"100\"\n    ENV[\"LINES\"] = \"50\"\n\n    terminal = ACON::Terminal.new\n    terminal.width.should eq 100\n    terminal.height.should eq 50\n\n    ENV[\"COLUMNS\"] = \"120\"\n    ENV[\"LINES\"] = \"60\"\n\n    terminal = ACON::Terminal.new\n    terminal.width.should eq 120\n    terminal.height.should eq 60\n    terminal.size.should eq({120, 60})\n  end\n\n  def test_zero_values : Nil\n    ENV[\"COLUMNS\"] = \"0\"\n    ENV[\"LINES\"] = \"0\"\n\n    terminal = ACON::Terminal.new\n    terminal.width.should eq 0\n    terminal.height.should eq 0\n    terminal.size.should eq({0, 0})\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/annotations.cr",
    "content": "module Athena::Console::Annotations\n  # Annotation containing metadata related to an `ACON::Command`.\n  # This is the preferred way of configuring a command.\n  #\n  # ```\n  # @[ACONA::AsCommand(\"add\", description: \"Sums two numbers, optionally making making the sum negative\")]\n  # class AddCommand < ACON::Command\n  #   # ...\n  # end\n  # ```\n  #\n  # ## Configuration\n  #\n  # Various fields can be used within this annotation to control various aspects of the command.\n  # All fields are optional unless otherwise noted.\n  #\n  # ### name\n  #\n  # **Type:** `String` - **required**\n  #\n  # The name of the command.\n  # May be provided as either an explicit named argument, or the first positional argument.\n  # See `ACON::Command#name`.\n  #\n  # ### description\n  #\n  # **Type:** `String`\n  #\n  # A short sentence describing the function of the command.\n  # See `ACON::Command#description`.\n  #\n  # ### hidden\n  #\n  # **Type:** `Bool`\n  #\n  # If this command should be hidden from the command list.\n  # See `ACON::Command#hidden?`.\n  #\n  # ### aliases\n  #\n  # **Type:** `Enumerable(String)`\n  #\n  # Alternate names this command may be invoked by.\n  # See `ACON::Command#aliases`.\n  annotation AsCommand; end\nend\n"
  },
  {
    "path": "src/components/console/src/application.cr",
    "content": "require \"levenshtein\"\n\n# A container for a collection of multiple `ACON::Command`, and serves as the entry point of a CLI application.\n# This class is optimized for a standard CLI environment; but it may be subclassed to provide a more specialized/customized entry point.\n#\n# ## Default Command\n#\n# The default command represents which command should be executed when no command name is provided; by default this is `ACON::Commands::List`.\n# For example, running `./console` would result in all the available commands being listed.\n# The default command can be customized via `#default_command`.\n#\n# ## Single Command Applications\n#\n# In some cases a CLI may only have one supported command in which passing the command's name each time is tedious.\n# In such a case an application may be declared as a single command application via the optional second argument to `#default_command`.\n# Passing `true` makes it so that any supplied arguments or options are passed to the default command.\n#\n# WARNING: Arguments and options passed to the default command are ignored when `#single_command?` is `false`.\n#\n# ## Custom Applications\n#\n# `ACON::Application` may also be extended in order to better fit a given application.\n# For example, it could define some [global custom styles][Athena::Console::Formatter::OutputStyleInterface--global-custom-styles],\n# override the array of default commands, or customize the default input options, etc.\nclass Athena::Console::Application\n  # Returns the version of this CLI application.\n  getter version : String\n\n  # Returns the name of this CLI application.\n  getter name : String\n\n  # By default, the application will auto [exit](https://crystal-lang.org/api/toplevel.html#exit(status=0):NoReturn-class-method) after executing a command.\n  # This method can be used to disable that functionality.\n  #\n  # If set to `false`, the `ACON::Command::Status` of the executed command is returned from `#run`.\n  # Otherwise the `#run` method never returns.\n  #\n  # ```\n  # application = ACON::Application.new \"My CLI\"\n  # application.auto_exit = false\n  # exit_status = application.run\n  # exit_status # => ACON::Command::Status::SUCCESS\n  #\n  # application.auto_exit = true\n  # exit_status = application.run\n  #\n  # # This line is never reached.\n  # exit_status\n  # ```\n  setter auto_exit : Bool = true\n\n  # By default, the application will gracefully handle exceptions raised as part of the execution of a command\n  # by formatting and outputting it; including varying levels of information depending on the `ACON::Output::Verbosity` level used.\n  #\n  # If set to `false`, that logic is bypassed and the exception is bubbled up to where `#run` was invoked from.\n  #\n  # ```\n  # application = ACON::Application.new \"My CLI\"\n  #\n  # application.register \"foo\" do |input, output, command|\n  #   output.puts %(Hello #{input.argument \"name\"}!)\n  #\n  #   # Denote that this command has finished successfully.\n  #   ACON::Command::Status::SUCCESS\n  # end.argument(\"name\", :required)\n  #\n  # application.default_command \"foo\", true\n  # application.catch_exceptions = false\n  #\n  # application.run # => Not enough arguments (missing: 'name'). (Athena::Console::Exception::Runtime)\n  # ```\n  setter catch_exceptions : Bool = true\n\n  # Allows setting the `ACON::Loader::Interface` that should be used by `self`.\n  # See the related interface for more information.\n  setter command_loader : ACON::Loader::Interface? = nil\n\n  # Returns `true` if `self` only supports a single command.\n  # See [Single Command Applications](/Console/Application/#Athena::Console::Application--single-command-applications) for more information.\n  getter? single_command : Bool = false\n\n  # When set to `true`, the application will check if `PROGRAM_NAME`'s basename matches a registered command.\n  # If it does, that command will be used and any arguments will be passed to it.\n  #\n  # This enables symlink-based command invocation, where `./command-name` (symlinked to the main binary) will automatically execute the `command-name` command.\n  # Any arguments are passed to the matched command rather than being interpreted as a command name.\n  property? use_program_name_as_command : Bool = false\n\n  # Returns/sets the `ACON::Helper::HelperSet` associated with `self`.\n  #\n  # The default helper set includes:\n  #\n  # * `ACON::Helper::Formatter`\n  # * `ACON::Helper::Question`\n  property helper_set : ACON::Helper::HelperSet { self.default_helper_set }\n\n  @commands = Hash(String, ACON::Command).new\n  @default_command : String = \"list\"\n  @definition : ACON::Input::Definition? = nil\n  @initialized : Bool = false\n  @running_command : ACON::Command? = nil\n  @terminal : ACON::Terminal\n  @wants_help : Bool = false\n\n  def initialize(@name : String, @version : String = \"UNKNOWN\")\n    @terminal = ACON::Terminal.new\n\n    # TODO: Emit events when certain signals are triggered.\n    # This will require the ability to optional set an event dispatcher on this type.\n  end\n\n  # Adds the provided *command* instance to `self`, allowing it be executed.\n  def add(command : ACON::Command) : ACON::Command?\n    self.init\n\n    command.application = self\n\n    unless command.enabled?\n      command.application = nil\n\n      return nil\n    end\n\n    if !command.is_a? ACON::Commands::Lazy\n      command.definition\n    end\n\n    @commands[command.name] = command\n\n    command.aliases.each do |a|\n      @commands[a] = command\n    end\n\n    command\n  end\n\n  # Returns if application should exit automatically after executing a command.\n  # See `#auto_exit=`.\n  def auto_exit? : Bool\n    @auto_exit\n  end\n\n  # Returns if the application should handle exceptions raised within the execution of a command.\n  # See `#catch_exceptions=`.\n  def catch_exceptions? : Bool\n    @catch_exceptions\n  end\n\n  # Returns all commands within `self`, optionally only including the ones within the provided *namespace*.\n  # The keys of the returned hash represent the full command names, while the values are the command instances.\n  def commands(namespace : String? = nil) : Hash(String, ACON::Command)\n    self.init\n\n    if namespace.nil?\n      unless command_loader = @command_loader\n        return @commands\n      end\n\n      commands = @commands.dup\n      command_loader.names.each do |name|\n        if !commands.has_key?(name) && self.has?(name)\n          commands[name] = self.get name\n        end\n      end\n\n      return commands\n    end\n\n    commands = Hash(String, ACON::Command).new\n    @commands.each do |name, command|\n      if namespace == self.extract_namespace(name, namespace.count(':') + 1)\n        commands[name] = command\n      end\n    end\n\n    if command_loader = @command_loader\n      command_loader.names.each do |name|\n        if !commands.has_key?(name) && namespace == self.extract_namespace(name, namespace.count(':') + 1) && self.has?(name)\n          commands[name] = self.get name\n        end\n      end\n    end\n\n    commands\n  end\n\n  # Sets the [default command][Athena::Console::Application--default-command] to the command with the provided *name*.\n  #\n  # For example, executing the following console script via `./console`\n  # would result in `Hello world!` being printed instead of the default list output.\n  #\n  # ```\n  # application = ACON::Application.new \"My CLI\"\n  #\n  # application.register \"foo\" do |_, output|\n  #   output.puts \"Hello world!\"\n  #   ACON::Command::Status::SUCCESS\n  # end\n  #\n  # application.default_command \"foo\"\n  #\n  # application.run\n  #\n  # ./console # => Hello world!\n  # ```\n  #\n  # For example, executing the following console script via `./console George`\n  # would result in `Hello George!` being printed. If we tried this again without setting *single_command*\n  # to `true`, it would error saying `Command 'George' is not defined.\n  #\n  # ```\n  # application = ACON::Application.new \"My CLI\"\n  #\n  # application.register \"foo\" do |input, output, command|\n  #   output.puts %(Hello #{input.argument \"name\"}!)\n  #   ACON::Command::Status::SUCCESS\n  # end.argument(\"name\", :required)\n  #\n  # application.default_command \"foo\", true\n  #\n  # application.run\n  # ```\n  def default_command(name : String, single_command : Bool = false) : self\n    @default_command = name\n\n    if single_command\n      self.find name\n\n      @single_command = true\n    end\n\n    self\n  end\n\n  # Returns the `ACON::Input::Definition` associated with `self`.\n  # See the related type for more information.\n  def definition : ACON::Input::Definition\n    @definition ||= self.default_input_definition\n\n    if self.single_command?\n      input_definition = @definition.not_nil!\n      input_definition.arguments = Array(ACON::Input::Argument).new\n\n      return input_definition\n    end\n\n    @definition.not_nil!\n  end\n\n  # Sets the *definition* that should be used by `self`.\n  # See the related type for more information.\n  def definition=(@definition : ACON::Input::Definition)\n  end\n\n  # Determines what values should be added to the possible *suggestions* based on the provided *input*.\n  #\n  # By default this handles completing commands and options, but can be overridden if needed.\n  def complete(input : ACON::Completion::Input, suggestions : ACON::Completion::Suggestions) : Nil\n    if input.completion_type.argument_value? && \"command\" == input.completion_name\n      self.commands.each do |name, command|\n        next if command.hidden? || command.name != name\n\n        suggestions.suggest_value name, command.description\n\n        command.aliases.each do |a|\n          suggestions.suggest_value a, command.description\n        end\n      end\n\n      return\n    end\n\n    if input.completion_type.option_name?\n      suggestions.suggest_options self.definition.options.values\n\n      return\n    end\n  end\n\n  # Yields each command within `self`, optionally only yields those within the provided *namespace*.\n  def each_command(namespace : String? = nil, & : ACON::Command -> Nil) : Nil\n    self.commands(namespace).each_value { |c| yield c }\n  end\n\n  # Returns the `ACON::Command` with the provided *name*, which can either be the full name, an abbreviation, or an alias.\n  # This method will attempt to find the best match given an abbreviation of a name or alias.\n  #\n  # Raises an `ACON::Exception::CommandNotFound` exception when the provided *name* is incorrect or ambiguous.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  def find(name : String) : ACON::Command\n    self.init\n\n    aliases = Hash(String, String).new\n\n    @commands.each_value do |command|\n      command.aliases.each do |a|\n        @commands[a] = command unless self.has? a\n      end\n    end\n\n    return self.get name if self.has? name\n\n    all_command_names = if command_loader = @command_loader\n                          command_loader.names + @commands.keys\n                        else\n                          @commands.keys\n                        end\n\n    expression = \"#{name.split(':').join(\"[^:]*:\", &->Regex.escape(String))}[^:]*\"\n    commands = all_command_names.select(/^#{expression}/)\n\n    if commands.empty?\n      commands = all_command_names.select(/^#{expression}/i)\n    end\n\n    if commands.empty? || commands.select(/^#{expression}$/i).size < 1\n      if pos = name.index ':'\n        # Check if a namespace exists and contains commands\n        self.find_namespace name[0...pos]\n      end\n\n      message = \"Command '#{name}' is not defined.\"\n\n      if (alternatives = self.find_alternatives name, all_command_names) && (!alternatives.empty?)\n        alternatives.select! do |n|\n          !self.get(n).hidden?\n        end\n\n        case alternatives.size\n        when 1 then message += \"\\n\\nDid you mean this?\\n    \"\n        else        message += \"\\n\\nDid you mean one of these?\\n    \"\n        end\n\n        message += alternatives.join(\"\\n    \")\n      end\n\n      raise ACON::Exception::CommandNotFound.new message, alternatives\n    end\n\n    # Filter out aliases for commands which are already on the list.\n    if commands.size > 1\n      command_list = @commands.dup\n\n      commands.select! do |name_or_alias|\n        command = if !command_list.has_key?(name_or_alias)\n                    command_list[name_or_alias] = @command_loader.not_nil!.get name_or_alias\n                  else\n                    command_list[name_or_alias]\n                  end\n\n        command_name = command.name\n\n        aliases[name_or_alias] = command_name\n\n        command_name == name_or_alias || !commands.includes? command_name\n      end.uniq!\n\n      usable_width = @terminal.width - 10\n      max_len = commands.max_of &->ACON::Helper.width(String)\n      abbreviations = commands.map do |n|\n        if command_list[n].hidden?\n          commands.delete n\n\n          next nil\n        end\n\n        abbreviation = \"#{n.rjust max_len, ' '} #{command_list[n].description}\"\n\n        ACON::Helper.width(abbreviation) > usable_width ? \"#{abbreviation[0, usable_width - 3]}...\" : abbreviation\n      end\n\n      if commands.size > 1\n        suggestions = self.abbreviation_suggestions abbreviations.compact\n\n        raise ACON::Exception::CommandNotFound.new \"Command '#{name}' is ambiguous.\\nDid you mean one of these?\\n#{suggestions}\", commands\n      end\n    end\n\n    command = self.get commands.first\n\n    raise ACON::Exception::CommandNotFound.new \"The command '#{name}' does not exist.\" if command.hidden?\n\n    command\n  end\n\n  # Returns the full name of a registered namespace with the provided *name*, which can either be the full name or an abbreviation.\n  #\n  # Raises an `ACON::Exception::NamespaceNotFound` exception when the provided *name* is incorrect or ambiguous.\n  def find_namespace(name : String) : String\n    all_namespace_names = self.namespaces\n\n    expression = \"#{name.split(':').join(\"[^:]*:\", &->Regex.escape(String))}[^:]*\"\n    namespaces = all_namespace_names.select(/^#{expression}/)\n\n    if namespaces.empty?\n      message = \"There are no commands defined in the '#{name}' namespace.\"\n\n      if (alternatives = self.find_alternatives name, all_namespace_names) && (!alternatives.empty?)\n        case alternatives.size\n        when 1 then message += \"\\n\\nDid you mean this?\\n    \"\n        else        message += \"\\n\\nDid you mean one of these?\\n    \"\n        end\n\n        message += alternatives.join(\"\\n    \")\n      end\n\n      raise ACON::Exception::NamespaceNotFound.new message, alternatives\n    end\n\n    exact = namespaces.includes? name\n\n    if namespaces.size > 1 && !exact\n      raise ACON::Exception::NamespaceNotFound.new \"The namespace '#{name}' is ambiguous.\\nDid you mean one of these?\\n#{self.abbreviation_suggestions namespaces}\", namespaces\n    end\n\n    exact ? name : namespaces.first\n  end\n\n  # Returns the `ACON::Command` with the provided *name*.\n  #\n  # Raises an `ACON::Exception::CommandNotFound` exception when a command with the provided *name* does not exist.\n  def get(name : String) : ACON::Command\n    self.init\n\n    raise ACON::Exception::CommandNotFound.new \"The command '#{name}' does not exist.\" unless self.has? name\n\n    if !@commands.has_key? name\n      raise ACON::Exception::CommandNotFound.new \"The '#{name}' command cannot be found because it is registered under multiple names. Make sure you don't set a different name via constructor or 'name='.\"\n    end\n\n    command = @commands[name]\n\n    if @wants_help\n      @wants_help = false\n\n      help_command = self.get \"help\"\n      help_command.as(ACON::Commands::Help).command = command\n\n      return help_command\n    end\n\n    command\n  end\n\n  # Returns `true` if a command with the provided *name* exists, otherwise `false`.\n  def has?(name : String) : Bool\n    self.init\n\n    return true if @commands.has_key? name\n\n    if (command_loader = @command_loader) && command_loader.has? name\n      self.add command_loader.get name\n\n      true\n    else\n      false\n    end\n  end\n\n  # By default this is the same as `#long_version`, but can be overridden\n  # to provide more in-depth help/usage instructions for `self`.\n  def help : String\n    self.long_version\n  end\n\n  # Returns all unique namespaces used by currently registered commands,\n  # excluding the global namespace.\n  def namespaces : Array(String)\n    namespaces = [] of String\n\n    self.commands.each_value do |command|\n      next if command.hidden?\n\n      namespaces.concat self.extract_all_namespaces command.name.not_nil!\n\n      command.aliases.each do |a|\n        namespaces.concat self.extract_all_namespaces a\n      end\n    end\n\n    namespaces.reject!(&.blank?).uniq!\n  end\n\n  # Runs the current application, optionally with the provided *input* and *output*.\n  #\n  # Returns the `ACON::Command::Status` of the related command execution if `#auto_exit?` is `false`.\n  # Will gracefully handle exceptions raised within the command execution unless `#catch_exceptions?` is `false`.\n  def run(input : ACON::Input::Interface = ACON::Input::ARGV.new, output : ACON::Output::Interface = ACON::Output::ConsoleOutput.new) : ACON::Command::Status | NoReturn\n    ENV[\"LINES\"] = @terminal.height.to_s\n    ENV[\"COLUMNS\"] = @terminal.width.to_s\n\n    self.configure_io input, output\n\n    begin\n      exit_code = self.do_run input, output\n    rescue ex : ::Exception\n      raise ex unless @catch_exceptions\n\n      self.render_exception ex, output\n\n      exit_code = if ex.is_a? ACON::Exception\n                    ACON::Command::Status.new ex.code\n                  else\n                    ACON::Command::Status::FAILURE\n                  end\n    end\n\n    if @auto_exit\n      exit exit_code.value\n    end\n\n    exit_code\n  end\n\n  # Creates and `#add`s an `ACON::Command` with the provided *name*; executing the block when the command is invoked.\n  def register(name : String, &block : ACON::Input::Interface, ACON::Output::Interface, ACON::Command -> ACON::Command::Status) : ACON::Command\n    self.add(ACON::Commands::Generic.new(name, &block)).not_nil!\n  end\n\n  # Returns the `#name` and `#version` of the application.\n  # Used when the `-V` or `--version` option is passed.\n  def long_version : String\n    \"#{@name} <info>#{@version}</info>\"\n  end\n\n  protected def command_name(input : ACON::Input::Interface) : String?\n    return @default_command if @single_command\n\n    if @use_program_name_as_command\n      prog_name = self.program_name\n      return prog_name if self.has?(prog_name)\n    end\n\n    input.first_argument\n  end\n\n  protected def program_name : String\n    Path.new(PROGRAM_NAME).basename\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  protected def configure_io(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil\n    if input.has_parameter? \"--ansi\", only_params: true\n      output.decorated = true\n    elsif input.has_parameter? \"--no-ansi\", only_params: true\n      output.decorated = false\n    end\n\n    if input.has_parameter? \"--no-interaction\", \"-n\", only_params: true\n      input.interactive = false\n    end\n\n    shell_verbosity = ENV[\"SHELL_VERBOSITY\"]?.try(&.to_i) || 0\n\n    case shell_verbosity\n    when -2 then output.verbosity = :silent\n    when -1 then output.verbosity = :quiet\n    when  1 then output.verbosity = :verbose\n    when  2 then output.verbosity = :very_verbose\n    when  3 then output.verbosity = :debug\n    end\n\n    if input.has_parameter? \"--silent\", only_params: true\n      output.verbosity = :silent\n      shell_verbosity = -2\n    elsif input.has_parameter? \"--quiet\", \"-q\", only_params: true\n      output.verbosity = :quiet\n      shell_verbosity = -1\n    else\n      if input.has_parameter?(\"-vvv\", \"--verbose=3\", only_params: true) || 3 == input.parameter(\"--verbose\", false, true)\n        output.verbosity = :debug\n        shell_verbosity = 3\n      elsif input.has_parameter?(\"-vv\", \"--verbose=2\", only_params: true) || 2 == input.parameter(\"--verbose\", false, true)\n        output.verbosity = :very_verbose\n        shell_verbosity = 2\n      elsif input.has_parameter?(\"-v\", \"--verbose=1\", only_params: true) || input.has_parameter?(\"--verbose\") || input.parameter(\"--verbose\", false, true)\n        output.verbosity = :verbose\n        shell_verbosity = 1\n      end\n    end\n\n    if 0 > shell_verbosity\n      input.interactive = false\n    end\n\n    ENV[\"SHELL_VERBOSITY\"] = shell_verbosity.to_s\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  protected def do_run(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    if input.has_parameter? \"--version\", \"-V\", only_params: true\n      output.puts self.long_version\n\n      return ACON::Command::Status::SUCCESS\n    end\n\n    begin\n      input.bind self.definition\n    rescue ex : ::Exception\n      # TODO: Make this part of the `rescue` after Crystal 1.13\n      # Ignore errors as full binding/validation happens later when command is known\n      raise ex unless ex.is_a? ACON::Exception\n    end\n\n    command_name = self.command_name input\n\n    if input.has_parameter? \"--help\", \"-h\", only_params: true\n      if command_name.nil?\n        command_name = \"help\"\n        input = ACON::Input::Hash.new(command_name: @default_command)\n      else\n        @wants_help = true\n      end\n    end\n\n    if command_name.nil?\n      command_name = @default_command\n      definition = self.definition\n      definition.arguments.merge!({\n        \"command\" => ACON::Input::Argument.new(\"command\", :optional, definition.argument(\"command\").description, command_name),\n      })\n    end\n\n    begin\n      @running_command = nil\n\n      command = self.find command_name\n    rescue ex : ::Exception\n      if (ex.is_a?(ACON::Exception::CommandNotFound) && !ex.is_a?(ACON::Exception::NamespaceNotFound)) &&\n         1 == (alternatives = ex.alternatives).size &&\n         input.interactive?\n        alternative = alternatives.not_nil!.first\n\n        style = ACON::Style::Athena.new input, output\n        output.puts \"\"\n        output.puts ACON::Helper::Formatter.new.format_block \"Command '#{command_name}' is not defined.\", \"error\", true\n\n        unless style.confirm \"Do you want to run '#{alternative}' instead?\", false\n          # TODO: Handle dispatching\n\n          return ACON::Command::Status::FAILURE\n        end\n\n        command = self.find alternative\n      else\n        # TODO: Handle dispatching\n\n        begin\n          if ex.is_a?(ACON::Exception::CommandNotFound) && (namespace = self.find_namespace command_name)\n            ACON::Helper::Descriptor.new.describe(\n              output.is_a?(ACON::Output::ConsoleOutputInterface) ? output.error_output : output,\n              self,\n              ACON::Descriptor::Context.new(\n                format: \"txt\",\n                raw_text: false,\n                namespace: namespace,\n                short: false\n              )\n            )\n\n            return ACON::Command::Status::SUCCESS\n          end\n\n          raise ex\n        rescue ACON::Exception::NamespaceNotFound\n          raise ex\n        end\n      end\n    end\n\n    @running_command = command\n    exit_status = self.do_run_command command, input, output\n    @running_command = nil\n\n    exit_status\n  end\n\n  protected def do_run_command(command : ACON::Command, input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    # TODO: Support input aware helpers.\n    # TODO: Handle registering signable command listeners.\n\n    command.run input, output\n\n    # TODO: Handle eventing.\n  end\n\n  protected def default_input_definition : ACON::Input::Definition\n    ACON::Input::Definition.new(\n      ACON::Input::Argument.new(\"command\", :required, \"The command to execute\"),\n      ACON::Input::Option.new(\"help\", \"h\", description: \"Display help for the given command. When no command is given display help for the <info>#{@default_command}</info> command\"),\n      ACON::Input::Option.new(\"silent\", description: \"Do not output any message\"),\n      ACON::Input::Option.new(\"quiet\", \"q\", description: \"Only errors are displayed. All other output is suppressed\"),\n      ACON::Input::Option.new(\"verbose\", \"v|vv|vvv\", description: \"Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug\"),\n      ACON::Input::Option.new(\"version\", \"V\", description: \"Display this application version\"),\n      ACON::Input::Option.new(\"ansi\", value_mode: :negatable, description: \"Force (or disable --no-ansi) ANSI output\", default: false),\n      ACON::Input::Option.new(\"no-interaction\", \"n\", description: \"Do not ask any interactive question\"),\n    )\n  end\n\n  protected def default_commands : Array(ACON::Command)\n    [\n      Athena::Console::Commands::Help.new,\n      Athena::Console::Commands::List.new,\n      Athena::Console::Commands::DumpCompletion.new,\n      Athena::Console::Commands::Complete.new,\n    ]\n  end\n\n  protected def default_helper_set : ACON::Helper::HelperSet\n    ACON::Helper::HelperSet.new(\n      ACON::Helper::Formatter.new,\n      ACON::Helper::Question.new\n    )\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  protected def do_render_exception(ex : ::Exception, output : ACON::Output::Interface) : Nil\n    loop do\n      message = (ex.message || \"\").strip\n\n      if message.empty? || ACON::Output::Verbosity::VERBOSE <= output.verbosity\n        title = \"  [#{ex.class}]  \"\n        len = ACON::Helper.width title\n      else\n        len = 0\n        title = \"\"\n      end\n\n      width = @terminal.width ? @terminal.width - 1 : Int32::MAX\n      lines = [] of Tuple(String, Int32)\n\n      message.split(/(?:\\r?\\n)/) do |line|\n        self.split_string_by_width(line, width - 4) do |l|\n          line_length = ACON::Helper.width(l) + 4\n          lines << {l, line_length}\n\n          len = Math.max line_length, len\n        end\n      end\n\n      messages = [] of String\n\n      if !ex.is_a?(ACON::Exception) || ACON::Output::Verbosity::VERBOSE <= output.verbosity\n        if trace = ex.backtrace?.try &.first\n          filename = nil\n          line = nil\n\n          if match = trace.match(/(\\w+\\.cr):(\\d+)/)\n            filename = if f = match[1]?\n                         File.basename f\n                       end\n            line = match[2]?\n          end\n\n          messages << %(<comment>#{ACON::Formatter::Output.escape \"In #{filename || \"n/a\"} line #{line || \"n/a\"}:\"}</comment>)\n        end\n      end\n\n      messages << (empty_line = \"<error>#{\" \"*len}</error>\")\n\n      if messages.empty? || ACON::Output::Verbosity::VERBOSE <= output.verbosity\n        messages << \"<error>#{title}#{\" \"*(Math.max(0, len - ACON::Helper.width(title)))}</error>\"\n      end\n\n      lines.each do |l|\n        messages << \"<error>  #{ACON::Formatter::Output.escape l[0]}  #{\" \"*(len - l[1])}</error>\"\n      end\n\n      messages << empty_line\n      messages << \"\"\n\n      messages.each do |m|\n        output.puts m, :quiet\n      end\n\n      if (ACON::Output::Verbosity::VERBOSE <= output.verbosity) && (t = ex.backtrace?)\n        output.puts \"<comment>Exception trace:</comment>\", :quiet\n\n        # TODO: Improve backtrace rendering.\n        t.each do |l|\n          output.puts \" #{l}\"\n        end\n\n        output.puts \"\", :quiet\n      end\n\n      break unless ex = ex.cause\n    end\n  end\n\n  protected def extract_namespace(name : String, limit : Int32? = nil) : String\n    # Pop off the shortcut name of the command.\n    parts = name.split(':').tap &.pop\n\n    (limit.nil? ? parts : parts[0...limit]).join ':'\n  end\n\n  protected def render_exception(ex : ::Exception, output : ACON::Output::ConsoleOutputInterface) : Nil\n    self.render_exception ex, output.error_output\n  end\n\n  protected def render_exception(ex : ::Exception, output : ACON::Output::Interface) : Nil\n    output.puts \"\", :quiet\n\n    self.do_render_exception ex, output\n\n    if running_command = @running_command\n      output.puts \"<info>#{ACON::Formatter::Output.escape running_command.synopsis}</info>\", :quiet\n      output.puts \"\", :quiet\n    end\n  end\n\n  private def abbreviation_suggestions(abbreviations : Array(String)) : String\n    %(    #{abbreviations.join(\"\\n    \")})\n  end\n\n  private def extract_all_namespaces(name : String) : Array(String)\n    # Pop off the shortcut name of the command.\n    parts = name.split(':').tap &.pop\n\n    namespaces = [] of String\n\n    parts.each do |p|\n      namespaces << if namespaces.empty?\n        p\n      else\n        \"#{namespaces.last}:#{p}\"\n      end\n    end\n\n    namespaces\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def find_alternatives(name : String, collection : Enumerable(String)) : Array(String)\n    alternatives = Hash(String, Int32).new\n    threshold = 1_000\n\n    collection_parts = Hash(String, Array(String)).new\n    collection.each do |item|\n      collection_parts[item] = item.split ':'\n    end\n\n    name.split(':').each_with_index do |sub_name, idx|\n      collection_parts.each do |collection_name, parts|\n        exists = alternatives.has_key? collection_name\n\n        if exists && parts[idx]?.nil?\n          alternatives[collection_name] += threshold\n          next\n        elsif parts[idx]?.nil?\n          next\n        end\n\n        lev = Levenshtein.distance sub_name, parts[idx]\n\n        if lev <= sub_name.size / 3 || !sub_name.empty? && parts[idx].includes? sub_name\n          alternatives[collection_name] = exists ? alternatives[collection_name] + lev : lev\n        elsif exists\n          alternatives[collection_name] += threshold\n        end\n      end\n    end\n\n    collection.each do |item|\n      lev = Levenshtein.distance name, item\n      if lev <= name.size / 3 || item.includes? name\n        alternatives[item] = (current = alternatives[item]?) ? current - lev : lev\n      end\n    end\n\n    alternatives.select! { |_, lev| lev < 2 * threshold }\n\n    alternatives.keys.sort!\n  end\n\n  private def init : Nil\n    return if @initialized\n\n    @initialized = true\n\n    self.default_commands.each do |command|\n      self.add command\n    end\n  end\n\n  private def split_string_by_width(line : String, width : Int32, & : String -> Nil) : Nil\n    if line.empty?\n      return yield line\n    end\n\n    line.each_char.each_slice(width).map(&.join).each do |set|\n      yield set\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/athena-console.cr",
    "content": "require \"ecr\"\nrequire \"semantic_version\"\n\nrequire \"athena-clock\"\n\nrequire \"./annotations\"\nrequire \"./application\"\nrequire \"./command\"\nrequire \"./cursor\"\nrequire \"./terminal\"\n\nrequire \"./commands/*\"\nrequire \"./completion/**\"\nrequire \"./descriptor/*\"\nrequire \"./exception/*\"\nrequire \"./formatter/*\"\nrequire \"./helper/*\"\nrequire \"./input/*\"\nrequire \"./loader/*\"\nrequire \"./output/*\"\nrequire \"./question/*\"\nrequire \"./style/*\"\n\n# Convenience alias to make referencing `Athena::Console` types easier.\nalias ACON = Athena::Console\n\n# Convenience alias to make referencing `ACON::Annotations` types easier.\nalias ACONA = ACON::Annotations\n\n# Allows the creation of CLI based commands\nmodule Athena::Console\n  VERSION = \"0.4.3\"\n\n  # Contains all the `Athena::Console` based annotations.\n  module Annotations; end\n\n  # Includes the commands that come bundled with `Athena::Console`.\n  module Commands; end\n\n  # Includes types related to Athena's [tab completion][Athena::Console::Input::Interface--argumentoption-value-completion] features.\n  module Completion; end\n\n  # Both acts as a namespace for exceptions related to the `Athena::Console` component, as well as a way to check for exceptions from the component.\n  # Exposes a `#code` method that represents the exit code of a command invocation.\n  module Exception\n    # Returns the exit code that should be used for this exception.\n    getter code : Int32\n  end\n\n  # Contains types related to lazily loading commands.\n  module Loader; end\nend\n"
  },
  {
    "path": "src/components/console/src/command.cr",
    "content": "# An `ACON::Command` represents a concrete command that can be invoked via the CLI.\n# All commands should inherit from this base type, but additional abstract subclasses can be used\n# to share common logic for related command classes.\n#\n# ## Creating a Command\n#\n# A command is defined by extending `ACON::Command` and implementing the `#execute` method.\n# For example:\n#\n# ```\n# @[ACONA::AsCommand(\"app:create-user\")]\n# class CreateUserCommand < ACON::Command\n#   protected def configure : Nil\n#     # ...\n#   end\n#\n#   protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#     # Implement all the business logic here.\n#\n#     # Indicates the command executed successfully.\n#     ACON::Command::Status::SUCCESS\n#   end\n# end\n# ```\n#\n# ### Command Lifecycle\n#\n# Commands have three lifecycle methods that are invoked when running the command:\n#\n# 1. `setup` (optional) - Executed before `#interact` and `#execute`. Can be used to setup state based on input data.\n# 1. `interact` (optional) - Executed after `#setup` but before `#execute`. Can be used to check if any arguments/options are missing\n# and interactively ask the user for those values. After this method, missing arguments/options will result in an error.\n# 1. `execute` (required) - Contains the business logic for the command, returning the status of the invocation via `ACON::Command::Status`.\n#\n# ```\n# @[ACONA::AsCommand(\"app:create-user\")]\n# class CreateUserCommand < ACON::Command\n#   protected def configure : Nil\n#     # ...\n#   end\n#\n#   protected def setup(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil\n#     # ...\n#   end\n#\n#   protected def interact(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil\n#     # ...\n#   end\n#\n#   protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#     # Indicates the command executed successfully.\n#     ACON::Command::Status::SUCCESS\n#   end\n# end\n# ```\n#\n# ## Configuring the Command\n#\n# In most cases, a command is going to need to be configured to better fit its purpose.\n# The `#configure` method can be used configure various aspects of the command,\n# such as its name, description, `ACON::Input`s, help message, aliases, etc.\n#\n# ```\n# protected def configure : Nil\n#   self\n#     .help(\"Creates a user...\") # Shown when running the command with the `--help` option\n#     .aliases(\"new-user\")       # Alternate names for the command\n#     .hidden                    # Hide the command from the list\n#   # ...\n# end\n# ```\n#\n# TIP: The suggested way of setting the name and description of the command is via the `ACONA::AsCommand` annotation.\n# This enables lazy command instantiation when used within the Athena framework.\n#\n# The `#configure` command is called automatically at the end of the constructor method.\n# If your command defines its own, be sure to call `super()` to also run the parent constructor.\n# `super` may also be called _after_ setting the properties if they should be used to determine how to configure the command.\n#\n# ```\n# class CreateUserCommand < ACON::Command\n#   def initialize(@require_password : Bool = false)\n#     super()\n#   end\n#\n#   protected def configure : Nil\n#     self\n#       .argument(\"password\", @require_password ? ACON::Input::Argument::Mode::REQUIRED : ACON::Input::Argument::Mode::OPTIONAL)\n#   end\n# end\n# ```\n#\n# ### Output\n#\n# The `#execute` method has access to an `ACON::Output::Interface` instance that can be used to write messages to display.\n# The `output` parameter should be used instead of `#puts` or `#print` to decouple the command from `STDOUT`.\n#\n# ```\n# protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#   # outputs multiple lines to the console (adding \"\\n\" at the end of each line)\n#   output.puts([\n#     \"User Creator\",\n#     \"============\",\n#     \"\",\n#   ])\n#\n#   # outputs a message followed by a \"\\n\"\n#   output.puts \"Whoa!\"\n#\n#   # outputs a message without adding a \"\\n\" at the end of the line\n#   output.print \"You are about to \"\n#   output.print \"create a user.\"\n#\n#   ACON::Command::Status::SUCCESS\n# end\n# ```\n#\n# See `ACON::Output::Interface` for more information.\n#\n# ### Input\n#\n# In most cases, a command is going to have some sort of input arguments/options.\n# These inputs can be setup in the `#configure` method, and accessed via the *input* parameter within `#execute`.\n#\n# ```\n# protected def configure : Nil\n#   self\n#     .argument(\"username\", :required, \"The username of the user\")\n# end\n#\n# protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#   # Retrieve the username as a String?\n#   output.puts %(Hello #{input.argument \"username\"}!)\n#\n#   ACON::Command::Status::SUCCESS\n# end\n# ```\n#\n# See `ACON::Input::Interface` for more information.\n#\n# ## Testing the Command\n#\n# `Athena::Console` also includes a way to test your console commands without needing to build and run a binary.\n# A single command can be tested via an `ACON::Spec::CommandTester` and a whole application can be tested via an `ACON::Spec::ApplicationTester`.\n#\n# See `ACON::Spec` for more information.\nabstract class Athena::Console::Command\n  # Represents the execution status of an `ACON::Command`.\n  #\n  # The value of each member is used as the exit code of the invocation.\n  #\n  # TIP: The exit code may be customized by manually instantiating the enum with it. E.g. `Status.new 126`.\n  enum Status\n    # Represents a successful invocation with no errors.\n    SUCCESS = 0\n\n    # Represents that some error happened during invocation.\n    FAILURE = 1\n\n    # Represents the command was not used correctly, such as invalid options or missing arguments.\n    INVALID = 2\n  end\n\n  private enum Synopsis\n    SHORT\n    LONG\n  end\n\n  # Returns the default name of `self`, or `nil` if it was not set.\n  def self.default_name : String?\n    {% begin %}\n      {% if ann = @type.annotation ACONA::AsCommand %}\n        {%\n          name = (ann[0] || ann[:name])\n\n          unless name\n            ann.raise \"Console command '#{@type}' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.\"\n          end\n        %}\n\n        {% if !ann[:hidden] && !ann[:aliases] %}\n          {{name}}\n        {% else %}\n          {%\n            name = name.split '|'\n            name = name + (ann[:aliases] || [] of Nil)\n\n            if ann[:hidden] && \"\" != name[0]\n              name.unshift \"\"\n            end\n          %}\n            {{name.join '|'}}\n        {% end %}\n      {% end %}\n    {% end %}\n  end\n\n  # Returns the default description of `self`, or `nil` if it was not set.\n  def self.default_description : String?\n    {% if ann = @type.annotation ACONA::AsCommand %}\n      {{ann[:description]}}\n    {% end %}\n  end\n\n  # Returns the name of `self`.\n  getter! name : String\n\n  # Returns the `description of `self`.\n  getter description : String = \"\"\n\n  # Returns/sets the help template for `self`.\n  #\n  # See `#processed_help`.\n  property help : String = \"\"\n\n  # Returns the `ACON::Application` associated with `self`, otherwise `nil`.\n  getter! application : ACON::Application\n\n  # Returns/sets the list of aliases that may also be used to execute `self` in addition to its `#name`.\n  property aliases : Array(String) = [] of String\n\n  # Returns/sets an `ACON::Helper::HelperSet` on `self`.\n  property helper_set : ACON::Helper::HelperSet? = nil\n\n  # Returns `true` if `self` is hidden from the command list, otherwise `false`.\n  getter? hidden : Bool = false\n\n  # Returns if `self` is enabled in the current environment.\n  #\n  # Can be overridden to return `false` if it cannot run under the current conditions.\n  getter? enabled : Bool = true\n\n  # Returns the list of usages for `self`.\n  #\n  # See `#usage`.\n  getter usages : Array(String) = [] of String\n\n  @definition : ACON::Input::Definition = ACON::Input::Definition.new\n  @full_definition : ACON::Input::Definition? = nil\n  @ignore_validation_errors : Bool = false\n  @synopsis = Hash(Synopsis, String).new\n  @process_title : String? = nil\n\n  def initialize(name : String? = nil)\n    if name.nil? && (n = self.class.default_name)\n      aliases = n.split '|'\n\n      if (name = aliases.shift).empty?\n        self.hidden true\n        name = aliases.shift?\n      end\n\n      self.aliases aliases\n    end\n\n    unless name.nil?\n      self.name name\n    end\n\n    if (@description.empty?) && (description = self.class.default_description)\n      self.description description\n    end\n\n    self.configure\n  end\n\n  # Sets the aliases of `self`.\n  def aliases(*aliases : String) : self\n    self.aliases aliases.to_a\n  end\n\n  # :ditto:\n  def aliases(aliases : Enumerable(String)) : self\n    aliases.each &->validate_name(String)\n\n    @aliases = aliases\n\n    self\n  end\n\n  def application=(@application : ACON::Application?) : Nil\n    if application = @application\n      @helper_set = application.helper_set\n    else\n      @helper_set = nil\n    end\n\n    @full_definition = nil\n  end\n\n  # Adds an `ACON::Input::Argument` to `self` with the provided *name*.\n  # Optionally supports setting its *mode*, *description*, *default* value, and *suggested_values*.\n  #\n  # Also checkout the [value completion][Athena::Console::Input::Interface--argumentoption-value-completion] for how argument values can be auto completed.\n  def argument(\n    name : String,\n    mode : ACON::Input::Argument::Mode = :optional,\n    description : String = \"\",\n    default = nil,\n    suggested_values : Enumerable(String)? = nil,\n  ) : self\n    @definition << ACON::Input::Argument.new name, mode, description, default, suggested_values.try &.to_a\n\n    if full_definition = @full_definition\n      full_definition << ACON::Input::Argument.new name, mode, description, default, suggested_values.try &.to_a\n    end\n\n    self\n  end\n\n  # Adds an `ACON::Input::Argument` to this command with the provided *name*.\n  # Optionally supports setting its *mode*, *description*, *default* value.\n  #\n  # Accepts a block to use to determine this argument's suggested values.\n  # Also checkout the [value completion][Athena::Console::Input::Interface--argumentoption-value-completion] for how argument values can be auto completed.\n  def argument(\n    name : String,\n    mode : ACON::Input::Argument::Mode = :optional,\n    description : String = \"\",\n    default = nil,\n    &suggested_values : ACON::Completion::Input -> Array(String)\n  ) : self\n    @definition << ACON::Input::Argument.new name, mode, description, default, suggested_values\n\n    if full_definition = @full_definition\n      full_definition << ACON::Input::Argument.new name, mode, description, default, suggested_values\n    end\n\n    self\n  end\n\n  def definition : ACON::Input::Definition\n    @full_definition || self.native_definition\n  end\n\n  # Sets the `ACON::Input::Definition` on self.\n  def definition(@definition : ACON::Input::Definition) : self\n    @full_definition = nil\n\n    self\n  end\n\n  # :ditto:\n  def definition(*definitions : ACON::Input::Argument | ACON::Input::Option) : self\n    self.definition definitions.to_a\n  end\n\n  # :ditto:\n  def definition(definition : Array(ACON::Input::Argument | ACON::Input::Option)) : self\n    @definition.definition = definition\n\n    @full_definition = nil\n\n    self\n  end\n\n  # Sets the `#description` of `self`.\n  def description(@description : String) : self\n    self\n  end\n\n  def name(name : String) : self\n    self.validate_name name\n\n    @name = name\n\n    self\n  end\n\n  # Sets the `#help` of `self`.\n  def help(@help : String) : self\n    self\n  end\n\n  # Returns an `ACON:Helper::Interface` of the provided *helper_class*.\n  #\n  # ```\n  # formatter = self.helper ACON::Helper::Formatter\n  # # ...\n  # ```\n  def helper(helper_class : T.class) : T forall T\n    unless helper_set = @helper_set\n      raise ACON::Exception::Logic.new \"Cannot retrieve helper '#{helper_class}' because there is no `ACON::Helper::HelperSet` defined. Did you forget to add your command to the application or to set the application on the command using '#application='? You can also set the HelperSet directly using '#helper_set='.\"\n    end\n\n    helper_set[helper_class].as T\n  end\n\n  # Hides `self` from the command list.\n  def hidden(@hidden : Bool = true) : self\n    self\n  end\n\n  # Adds an `ACON::Input::Option` to `self` with the provided *name*.\n  # Optionally supports setting its *shortcut*, *value_mode*, *description*, and *default* value.\n  #\n  # Also checkout the [value completion][Athena::Console::Input::Interface--argumentoption-value-completion] for how option values can be auto completed.\n  def option(\n    name : String,\n    shortcut : String? = nil,\n    value_mode : ACON::Input::Option::Value = :none,\n    description : String = \"\",\n    default = nil,\n    suggested_values : Enumerable(String)? = nil,\n  ) : self\n    @definition << ACON::Input::Option.new name, shortcut, value_mode, description, default, suggested_values.try &.to_a\n\n    if full_definition = @full_definition\n      full_definition << ACON::Input::Option.new name, shortcut, value_mode, description, default, suggested_values.try &.to_a\n    end\n\n    self\n  end\n\n  # Adds an `ACON::Input::Option` to `self` with the provided *name*.\n  # Optionally supports setting its *shortcut*, *value_mode*, *description*, and *default* value.\n  #\n  # Accepts a block to use to determine this argument's suggested values.\n  # Also checkout the [value completion][Athena::Console::Input::Interface--argumentoption-value-completion] for how option values can be auto completed.\n  def option(\n    name : String,\n    shortcut : String? = nil,\n    value_mode : ACON::Input::Option::Value = :none,\n    description : String = \"\",\n    default = nil,\n    &suggested_values : ACON::Completion::Input -> Array(String)\n  ) : self\n    @definition << ACON::Input::Option.new name, shortcut, value_mode, description, default, suggested_values\n\n    if full_definition = @full_definition\n      full_definition << ACON::Input::Option.new name, shortcut, value_mode, description, default, suggested_values\n    end\n\n    self\n  end\n\n  # Sets the process title of `self`.\n  #\n  # TODO: Implement this.\n  def process_title(title : String) : self\n    @process_title = title\n\n    self\n  end\n\n  # The `#help` message can include some template variables for the command:\n  #\n  # * `%command.name%` - Returns the `#name` of `self`. E.g. `app:create-user`\n  #\n  # This method returns the `#help` message with these variables replaced.\n  def processed_help : String\n    is_single_command = (application = @application) && application.single_command?\n    prog_name = Path.new(PROGRAM_NAME).basename\n    full_name = is_single_command ? prog_name : \"./#{prog_name} #{@name}\"\n\n    processed_help = self.help.presence || self.description\n\n    { {\"%command.name%\", @name}, {\"%command.full_name%\", full_name} }.each do |(placeholder, replacement)|\n      processed_help = processed_help.gsub placeholder, replacement\n    end\n\n    processed_help\n  end\n\n  # Returns a short synopsis of `self`, including its `#name` and expected arguments/options.\n  # For example `app:user-create [--dry-run] [--] <username>`.\n  def synopsis(short : Bool = false) : String\n    key = short ? Synopsis::SHORT : Synopsis::LONG\n\n    unless @synopsis.has_key? key\n      @synopsis[key] = \"#{@name} #{@definition.synopsis short}\".strip\n    end\n\n    @synopsis[key]\n  end\n\n  # Adds a usage string that will displayed within the `Usage` section after the auto generated entry.\n  def usage(usage : String) : self\n    unless (name = @name) && usage.starts_with? name\n      usage = \"#{name} #{usage}\"\n    end\n\n    @usages << usage\n\n    self\n  end\n\n  # Makes the command ignore any input validation errors.\n  def ignore_validation_errors : Nil\n    @ignore_validation_errors = true\n  end\n\n  # Runs the command with the provided *input* and *output*, returning the status of the invocation as an `ACON::Command::Status`.\n  def run(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    self.merge_application_definition\n\n    begin\n      input.bind self.definition\n    rescue ex : ::Exception\n      # TODO: Make this part of the `rescue` after Crystal 1.13\n      if ex.is_a?(ACON::Exception)\n        raise ex unless @ignore_validation_errors\n      end\n    end\n\n    self.setup input, output\n\n    # TODO: Allow setting process title\n\n    if input.interactive?\n      self.interact input, output\n    end\n\n    if input.has_argument?(\"command\") && input.argument(\"command\").nil?\n      input.set_argument \"command\", self.name\n    end\n\n    input.validate\n\n    self.execute input, output\n  end\n\n  # Determines what values should be added to the possible *suggestions* based on the provided *input*.\n  #\n  # By default this will fall back on completion of the related input argument/option, but can be overridden if needed.\n  def complete(input : ACON::Completion::Input, suggestions : ACON::Completion::Suggestions) : Nil\n    definition = self.definition\n\n    if input.completion_type.option_value? && (option = definition.options[input.completion_name]?)\n      option.complete input, suggestions\n    elsif input.completion_type.argument_value? && (argument = definition.arguments[input.completion_name]?)\n      argument.complete input, suggestions\n    end\n  end\n\n  protected def merge_application_definition(merge_args : Bool = true) : Nil\n    return unless application = @application\n\n    # TODO: Figure out if there is a better way to structure/store\n    # the data to remove the .values call.\n    full_definition = ACON::Input::Definition.new\n    full_definition.options = @definition.options.values\n    full_definition << application.definition.options.values\n\n    if merge_args\n      full_definition.arguments = application.definition.arguments.values\n      full_definition << @definition.arguments.values\n    else\n      full_definition.arguments = @definition.arguments.values\n    end\n\n    @full_definition = full_definition\n  end\n\n  protected def native_definition\n    @definition\n  end\n\n  # Executes the command with the provided *input* and *output*, returning the status of the invocation via `ACON::Command::Status`.\n  #\n  # This method _MUST_ be defined and implement the business logic for the command.\n  protected abstract def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n\n  # Can be overridden to configure the current command, such as setting the name, adding arguments/options, setting help information etc.\n  protected def configure : Nil\n  end\n\n  # The related `ACON::Input::Definition` is validated _after_ this method is executed.\n  # This method can be used to interactively ask the user for missing required arguments.\n  protected def interact(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil\n  end\n\n  # Called after the input has been bound, but before it has been validated.\n  # Can be used to setup state of the command based on the provided input data.\n  protected def setup(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil\n  end\n\n  private def validate_name(name : String) : Nil\n    raise ACON::Exception::InvalidArgument.new \"Command name '#{name}' is invalid.\" if name.blank? || !name.matches? /^[^:]++(:[^:]++)*$/\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/commands/complete.cr",
    "content": "require \"semantic_version\"\n\n@[Athena::Console::Annotations::AsCommand(\"|_complete\", description: \"Internal command to provide shell completion suggestions\")]\n# :nodoc:\nclass Athena::Console::Commands::Complete < Athena::Console::Command\n  API_VERSION = 2\n\n  @completion_outputs : Hash(String, ACON::Completion::Output::Interface.class)\n\n  @debug : Bool = false\n\n  def initialize(completion_outputs : Hash(String, ACON::Completion::Output::Interface.class) = Hash(String, ACON::Completion::Output::Interface.class).new)\n    @completion_outputs = completion_outputs.merge!({\n      \"bash\" => ACON::Completion::Output::Bash,\n      \"fish\" => ACON::Completion::Output::Fish,\n      \"zsh\"  => ACON::Completion::Output::Zsh,\n    } of String => ACON::Completion::Output::Interface.class)\n\n    super()\n  end\n\n  protected def configure : Nil\n    self\n      .definition(\n        ACON::Input::Option.new(\"shell\", \"s\", :required, \"The shell type ('#{@completion_outputs.keys.join \"', '\"}')\"),\n        ACON::Input::Option.new(\"input\", \"i\", ACON::Input::Option::Value[:required, :is_array], \"An array of input tokens (e.g. COMP_WORDS or argv)\"),\n        ACON::Input::Option.new(\"current\", \"c\", :required, \"The index of the 'input' array that the cursor is in (e.g. COMP_CWORD)\"),\n        ACON::Input::Option.new(\"api-version\", \"a\", :required, \"The API version of the completion script\")\n      )\n  end\n\n  protected def setup(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil\n    @debug = ENV[\"ATHENA_DEBUG_COMPLETION\"]? == \"true\"\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    if major_version = input.option(\"api-version\")\n      version = SemanticVersion.new major_version.to_i, 0, 0\n\n      if version < SemanticVersion.new(API_VERSION, 0, 0)\n        message = \"Completion script version is not supported ('#{version.major}' given, >=#{API_VERSION} required).\"\n\n        self.log message\n\n        output.puts \"#{message} Install the Athena completion script again by using the 'completion' command.\"\n\n        return ACON::Command::Status.new 126\n      end\n    end\n\n    unless shell = input.option \"shell\"\n      raise ACON::Exception::Runtime.new \"The '--shell' option must be set.\"\n    end\n\n    unless completion_output = @completion_outputs[shell]?\n      raise ACON::Exception::Runtime.new %(Shell completion is not supported for your shell: '#{shell}' (supported: '#{@completion_outputs.keys.join \"', '\"}').)\n    end\n\n    completion_input = self.create_completion_input input\n    suggestions = ACON::Completion::Suggestions.new\n\n    self.log({\n      \"\",\n      \"<comment>#{Time.local}</>\",\n      \"<info>Input:</> <comment>(\\\"|\\\" indicates the cursor position)</>\",\n      \" #{completion_input}\",\n      \"<info>Command:</>\",\n      \" #{ARGV.join \" \"}\",\n      \"<info>Messages:</>\",\n    })\n\n    command = self.find_command completion_input, output\n\n    if command.nil?\n      self.log \"  No command found, completing using the Application class.\"\n\n      self.application.complete completion_input, suggestions\n    elsif completion_input.must_suggest_argument_values_for?(\"command\") &&\n          command.name != completion_input.completion_value &&\n          !command.aliases.includes?(completion_input.completion_value)\n      self.log \"  Found command, suggesting aliases\"\n\n      # expand shortcut names (\"foo:f<TAB>\") into their full name (\"foo:foo\")\n      suggestions.suggest_values [command.name].concat(command.aliases)\n    else\n      command.merge_application_definition\n      completion_input.bind command.definition\n\n      if completion_input.completion_type.option_name?\n        self.log \"  Completing option names for the <comment>#{command.is_a?(ACON::Commands::Lazy) ? command.command.class : command.class}</> command.\"\n\n        suggestions.suggest_options command.definition.options.values\n      else\n        self.log({\n          \"  Completing using the <comment>#{command.is_a?(ACON::Commands::Lazy) ? command.command.class : command.class}</> class.\",\n          \"  Completing <comment>#{completion_input.completion_type}</> for <comment>#{completion_input.completion_name}</>\",\n        })\n\n        command.complete completion_input, suggestions\n      end\n    end\n\n    completion_output = completion_output.new\n\n    self.log \"<info>Suggestions:</>\"\n\n    if (options = suggestions.suggested_options) && !options.empty?\n      self.log %(  --#{options.map(&.name).join(\" --\")})\n    elsif (values = suggestions.suggested_values) && !values.empty?\n      self.log %(  #{values.join(\" \")})\n    else\n      self.log \"  <comment>No suggestions were provided</>\"\n    end\n\n    completion_output.write suggestions, output\n\n    ACON::Command::Status::SUCCESS\n  rescue ex : ::Exception\n    self.log({\"<error>Error!</>\", ex.to_s})\n\n    raise ex if output.verbosity.debug?\n\n    ACON::Command::Status::INVALID\n  end\n\n  private def create_completion_input(input : ACON::Input::Interface) : ACON::Completion::Input\n    current_index = input.option \"current\"\n\n    if current_index.nil? || !(index = current_index.to_i?)\n      raise ACON::Exception::Runtime.new \"The '--current' option must be set and it must be an integer.\"\n    end\n\n    completion_input = ACON::Completion::Input.from_tokens input.option(\"input\", Array(String)), index\n\n    begin\n      completion_input.bind self.application.definition\n    rescue ex : ::Exception\n      # TODO: Make this part of the `rescue` after Crystal 1.13\n      raise ex unless ex.is_a? ACON::Exception\n    end\n\n    completion_input\n  end\n\n  private def find_command(completion_input : ACON::Completion::Input, output : ACON::Output::Interface) : ACON::Command?\n    begin\n      unless input_name = completion_input.first_argument\n        return nil\n      end\n\n      return self.application.find input_name\n    rescue ACON::Exception::CommandNotFound\n      # noop\n    end\n\n    nil\n  end\n\n  private def log(messages : String | Enumerable(String)) : Nil\n    return unless @debug\n\n    messages = messages.is_a?(String) ? {messages} : messages\n\n    command_name = Path.new(PROGRAM_NAME).basename\n    File.write(\n      \"#{Dir.tempdir}/athena_#{command_name}.log\",\n      \"#{messages.join(EOL)}#{EOL}\",\n      mode: \"a\"\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/commands/dump_completion.cr",
    "content": "@[Athena::Console::Annotations::AsCommand(\"completion\", description: \"Dump the shell completion script\")]\n# Can be used to generate the [completion script](/Console#console-completion) to enable [argument/option value completion][Athena::Console::Input::Interface--argumentoption-value-completion].\n#\n# See the related docs for more information.\nclass Athena::Console::Commands::DumpCompletion < Athena::Console::Command\n  private SUPPORTED_SHELLS = {{ Athena::Console::Completion::Output::Interface.subclasses.map(&.name.split(\"::\").last.downcase) }}\n\n  protected def self.guess_shell : String\n    File.basename ENV[\"SHELL\"]? || \"\"\n  end\n\n  protected def configure : Nil\n    # `Process.executable_path` already resolves symlinks\n    full_command = Process.executable_path || \"\"\n\n    command_name = File.basename full_command\n\n    shell = self.class.guess_shell\n\n    rc_file, completion_file = case shell\n                               when \"fish\" then {\"~/.config/fish/config.fish\", \"/etc/fish/completions/#{command_name}.fish\"}\n                               when \"zsh\"  then {\"~/.zshrc\", \"$fpath[1]/_#{command_name}\"}\n                               else\n                                 {\"~/.bashrc\", \"/etc/bash_completion.d/#{command_name}\"}\n                               end\n\n    supported_shells = SUPPORTED_SHELLS.join \", \"\n\n    self\n      .argument(\"shell\", description: \"The shell type (e.g. 'bash'), the value of the '$SHELL' env var will be used if not provided\", suggested_values: SUPPORTED_SHELLS)\n      .help(<<-TEXT\nThe <info>%command.name%</> command dumps the shell completion script required\nto use shell autocompletion (currently, #{supported_shells} completion are supported).\n\n<comment>Static installation\n-------------------</>\n\nDump the script to a global completion file and restart your shell:\n\n    <info>%command.full_name% #{shell} | sudo tee #{completion_file}</>\n\nOr dump the script to a local file and source it:\n\n    <info>%command.full_name% #{shell} > completion.sh</>\n\n    <comment># source the file whenever you use the project</>\n    <info>source completion.sh</>\n\n    <comment># or add this line at the end of your \"#{rc_file}\" file:</>\n    <info>source /path/to/completion.sh</>\n\n<comment>Dynamic installation\n--------------------</>\n\nAdd this to the end of your shell configuration file (e.g. <info>\"#{rc_file}\"</>):\n\n    <info>eval \"$(#{full_command} completion #{shell})\"</>\nTEXT\n      )\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    command_name = File.basename Process.executable_path || \"\"\n\n    # Prevent generating the file for *.tmp files.\n    # It'll not only run slow, but probably not even valid bash syntax.\n    if command_name.ends_with? \".tmp\"\n      raise ACON::Exception::Runtime.new \"The shell completion file may only be generated non-temporary binaries.\\n\\nTry to `crystal build` your application first and try again.\"\n    end\n\n    shell = input.argument(\"shell\") || self.class.guess_shell\n\n    completion_script = case shell\n                        when \"bash\" then ACON::Completion::Output::Bash::Script.new command_name, ACON::Commands::Complete::API_VERSION\n                        when \"fish\" then ACON::Completion::Output::Fish::Script.new command_name, ACON::Commands::Complete::API_VERSION\n                        when \"zsh\"  then ACON::Completion::Output::Zsh::Script.new command_name, ACON::Commands::Complete::API_VERSION\n                        else\n                          if output.is_a? ACON::Output::ConsoleOutputInterface\n                            output = output.error_output\n                          end\n\n                          if shell\n                            output.puts %(<error>Detected shell '#{shell}', which is not supported by Athena shell completion (supported shells: '#{SUPPORTED_SHELLS.join(\"', '\")}'.))\n                          else\n                            output.puts %(<error>Shell not detected, Athena shell completion only supports '#{SUPPORTED_SHELLS.join(\"', '\")}'.)\n                          end\n\n                          return ACON::Command::Status::INVALID\n                        end\n\n    output.print completion_script\n\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/commands/generic.cr",
    "content": "# A generic implementation of `ACON::Command` that is instantiated with a block that will be executed as part of the `#execute` method.\n#\n# This is the command class used as part of `ACON::Application#register`.\nclass Athena::Console::Commands::Generic < Athena::Console::Command\n  alias Proc = ::Proc(ACON::Input::Interface, ACON::Output::Interface, ACON::Command, ACON::Command::Status)\n\n  def initialize(name : String, &@callback : ACON::Commands::Generic::Proc)\n    super name\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    @callback.call input, output, self\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/commands/help.cr",
    "content": "# Displays information for a given command.\n@[Athena::Console::Annotations::AsCommand(\"help\", description: \"Display help for a command\")]\nclass Athena::Console::Commands::Help < Athena::Console::Command\n  # :nodoc:\n  setter command : ACON::Command? = nil\n\n  protected def configure : Nil\n    self.ignore_validation_errors\n\n    self\n      .name(\"help\")\n      .argument(\"command_name\", description: \"The command name\", default: \"help\") { ACON::Descriptor::Application.new(self.application).commands.keys }\n      .option(\"format\", value_mode: :required, description: \"The output format (txt)\", default: \"txt\") { ACON::Helper::Descriptor.new.formats }\n      .option(\"raw\", value_mode: :none, description: \"To output raw command help\")\n      .help(\n        <<-HELP\n        The <info>%command.name%</info> command displays help for a given command:\n\n          <info>%command.full_name% list</info>\n\n        To display the list of available commands, please use the <info>list</info> command.\n        HELP\n      )\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    if @command.nil?\n      @command = self.application.find input.argument(\"command_name\", String)\n    end\n\n    ACON::Helper::Descriptor.new.describe(\n      output,\n      @command.not_nil!,\n      ACON::Descriptor::Context.new(\n        format: input.option(\"format\", String),\n        raw_text: input.option(\"raw\", Bool),\n      )\n    )\n\n    @command = nil\n\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/commands/lazy.cr",
    "content": "# :nodoc:\nclass Athena::Console::Commands::Lazy < Athena::Console::Command\n  @command : Proc(ACON::Command) | ACON::Command\n  @enabled : Bool\n\n  delegate :run,\n    :merge_application_definition,\n    :definition,\n    :native_definition,\n    :argument,\n    :option,\n    :process_title,\n    :help,\n    :processed_help,\n    :synopsis,\n    :usage,\n    :usages,\n    :helper,\n    to: self.command\n\n  def initialize(\n    name : String,\n    aliases : Enumerable(String),\n    description : String,\n    hidden : Bool,\n    @command : Proc(ACON::Command),\n    @enabled : Bool = true,\n  )\n    self\n      .name(name)\n      .aliases(aliases)\n      .hidden(hidden)\n      .description(description)\n  end\n\n  # :inherit:\n  def application=(application : ACON::Application?) : Nil\n    if (cmd = @command).is_a? ACON::Command\n      cmd.application = application\n    end\n\n    super\n  end\n\n  # :inherit:\n  def helper_set=(helper_set : ACON::Helper::HelperSet) : Nil\n    if (cmd = @command).is_a? ACON::Command\n      cmd.helper_set = helper_set\n    end\n\n    super\n  end\n\n  # :inherit:\n  def enabled? : Bool\n    @enabled || self.command.enabled?\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    raise NotImplementedError.new \"Use #run instead.\"\n  end\n\n  def command : ACON::Command\n    if (cmd = @command).is_a? ACON::Command\n      return cmd\n    end\n\n    command = @command = cmd.call\n    command.application = self.application?\n\n    if hs = self.helper_set\n      command.helper_set = hs\n    end\n\n    command\n      .name(self.name)\n      .aliases(self.aliases)\n      .hidden(self.hidden?)\n      .description(self.description)\n\n    command.definition\n\n    command\n  end\n\n  def complete(input : ACON::Completion::Input, suggestions : ACON::Completion::Suggestions) : Nil\n    self.command.complete input, suggestions\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/commands/list.cr",
    "content": "# Lists the available commands, optionally only including those in a specific namespace.\n@[Athena::Console::Annotations::AsCommand(\"list\", description: \"List available commands\")]\nclass Athena::Console::Commands::List < Athena::Console::Command\n  protected def configure : Nil\n    self\n      .argument(\"namespace\", description: \"Only list commands in this namespace\") { ACON::Descriptor::Application.new(self.application).namespaces.keys }\n      .option(\"raw\", value_mode: :none, description: \"To output raw command list\")\n      .option(\"format\", value_mode: :required, description: \"The output format (txt)\", default: \"txt\") { ACON::Helper::Descriptor.new.formats }\n      .option(\"short\", value_mode: :none, description: \"To skip describing command's arguments\")\n      .help(\n        <<-HELP\n        The <info>%command.name%</info> command lists all commands:\n\n          <info>%command.full_name%</info>\n\n        You can also display the commands for a specific namespace:\n\n          <info>%command.full_name% test</info>\n\n        It's also possible to get raw list of commands (useful for embedding command runner):\n\n          <info>%command.full_name% --raw</info>\n        HELP\n      )\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Helper::Descriptor.new.describe(\n      output,\n      self.application,\n      ACON::Descriptor::Context.new(\n        format: input.option(\"format\", String),\n        raw_text: input.option(\"raw\", Bool),\n        namespace: input.argument(\"namespace\", String?),\n        short: input.option(\"short\", Bool)\n      )\n    )\n\n    ACON::Command::Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/completion/input.cr",
    "content": "abstract class Athena::Console::Input; end\n\nrequire \"../input/argv\"\n\n# A specialization of `ACON::Input::ARGV` that allows for unfinished name/values.\n# Exposes information about the name, type, and value of the value/name being completed.\nclass Athena::Console::Completion::Input < Athena::Console::Input::ARGV\n  enum Type\n    # Nothing should be completed.\n    NONE\n\n    # Completing the value of an argument.\n    ARGUMENT_VALUE\n\n    # Completing the value of an option.\n    OPTION_VALUE\n\n    # Completing the name of an option.\n    OPTION_NAME\n  end\n\n  def self.from_string(input : String, current_index : Int32) : self\n    tokens = input.scan(/(?<=^|\\s)(['\"]?)(.+?)(?<!\\\\\\\\)\\1(?=$|\\s)/).map &.[0]\n\n    self.from_tokens tokens, current_index\n  end\n\n  def self.from_tokens(tokens : Array(String), current_index : Int32) : self\n    input = new tokens\n    input.current_index = current_index\n    input.tokens = tokens\n\n    input\n  end\n\n  # Returns which [type](/Console/Completion/Input/Type/) of completion is required.\n  getter completion_type : ACON::Completion::Input::Type = :none\n\n  # Returns the name of the argument/option when completing a value.\n  getter completion_name : String? = nil\n\n  # Returns the value typed by the user, or empty string.\n  getter completion_value : String = \"\"\n\n  protected setter current_index : Int32 = 1\n  protected setter tokens : Array(String)\n\n  # :inherit:\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  def bind(definition : ACON::Input::Definition) : Nil\n    super definition\n\n    relevant_token = self.relevant_token\n\n    if '-' == relevant_token[0]?\n      split_token = relevant_token.split('=', 2)\n      option_token, option_value = (split_token[0]? || \"\"), (split_token[1]? || \"\")\n\n      option = self.option_from_token option_token\n\n      if option.nil? && !self.free_cursor?\n        @completion_type = :option_name\n        @completion_value = relevant_token\n\n        return\n      end\n\n      if option && option.accepts_value?\n        @completion_type = :option_value\n        @completion_name = option.name\n        @completion_value = option_value.presence || (!option_token.starts_with?(\"--\") ? option_token[2..] : \"\")\n\n        return\n      end\n    end\n\n    previous_token = @tokens[@current_index - 1]? || \"\"\n\n    if '-' == previous_token[0]? && !previous_token.strip(\"-\").empty?\n      # Did the previous option accept a value?\n      previous_option = self.option_from_token previous_token\n\n      if previous_option && previous_option.accepts_value?\n        @completion_type = :option_value\n        @completion_name = previous_option.name\n        @completion_value = relevant_token\n\n        return\n      end\n    end\n\n    # Complete argument value\n    @completion_type = :argument_value\n\n    argument_name = nil\n    @definition.arguments.each do |arg_name, _|\n      argument_name = arg_name\n\n      break unless @arguments.has_key? arg_name\n\n      argument_value = @arguments[arg_name]\n      @completion_name = arg_name\n\n      @completion_value = if argument_value.is_a?(Array) || argument_value.is_a?(ACON::Input::Value::Array)\n                            argument_value.empty? ? \"\" : argument_value.last.to_s\n                          else\n                            argument_value.to_s\n                          end\n    end\n\n    if @current_index >= @tokens.size\n      if argument_name && (!@arguments.has_key?(argument_name) || @definition.argument(argument_name).is_array?)\n        @completion_name = argument_name\n        @completion_value = \"\"\n      else\n        # Reached end of data\n        @completion_type = :none\n        @completion_name = nil\n        @completion_value = \"\"\n      end\n    end\n  end\n\n  # Returns `true` if this input is able to suggest values for the provided *option_name*.\n  def must_suggest_option_values_for?(option_name : String) : Bool\n    @completion_type.option_value? && option_name == @completion_name\n  end\n\n  # Returns `true` if this input is able to suggest values for the provided *argument_name*.\n  def must_suggest_argument_values_for?(argument_name : String) : Bool\n    @completion_type.argument_value? && argument_name == @completion_name\n  end\n\n  # Returns the current token of the cursor, or last token if the cursor is at the end of the input.\n  def relevant_token : String\n    @tokens[self.free_cursor? ? @current_index - 1 : @current_index]? || \"\"\n  end\n\n  # :nodoc:\n  def to_s(io : IO) : Nil\n    i = 0\n    @tokens.each_with_index do |token, idx|\n      io << token\n      io << '|' if idx == @current_index\n      io << ' ' unless @tokens.size == (idx + 1)\n      i = idx\n    end\n\n    if @current_index > i\n      io << '|'\n    end\n  end\n\n  protected def parse_token(token : String, parse_options : Bool) : Bool\n    begin\n      return super\n    rescue ex : ACON::Exception::Runtime\n      # noop, completed input is almost never valid\n    end\n\n    parse_options\n  end\n\n  private def option_from_token(option_token : String) : ACON::Input::Option?\n    option_name = option_token.lstrip '-'\n\n    return nil if option_name.empty?\n\n    if '-' == (option_token[1]? || \" \")\n      # Long option name\n      return @definition.options[option_name]?\n    end\n\n    # Short option name\n    @definition.has_shortcut?(option_name[0]) ? @definition.option_for_shortcut(option_name[0]) : nil\n  end\n\n  private def free_cursor? : Bool\n    number_of_tokens = @tokens.size\n\n    if @current_index > number_of_tokens\n      raise RuntimeError.new \"Current index is invalid, it must be the number of input tokens.\"\n    end\n\n    @current_index >= number_of_tokens\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/completion/output/bash.cr",
    "content": "require \"./interface\"\n\n# :nodoc:\nstruct Athena::Console::Completion::Output::Bash < Athena::Console::Completion::Output::Interface\n  # :nodoc:\n  record Script, command_name : String, version : Int32 do\n    ECR.def_to_s \"#{__DIR__}/completion.bash\"\n  end\n\n  def write(suggestions : ACON::Completion::Suggestions, output : ACON::Output::Interface) : Nil\n    values = suggestions.suggested_values.map &.to_s\n\n    suggestions.suggested_options.each do |option|\n      values << \"--#{option.name}\"\n\n      if option.negatable?\n        values << \"--no-#{option.name}\"\n      end\n    end\n\n    output.puts values.join \"\\n\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/completion/output/completion.bash",
    "content": "# Adapted from https://github.com/symfony/symfony/blob/503a7b3cb62fb6de70176b07bd1c4242e3addc5b/src/Symfony/Component/Console/Resources/completion.bash\n\n_athena_<%= @command_name %>() {\n    # Use the default completion for shell redirect operators.\n    for w in '>' '>>' '&>' '<'; do\n        if [[ $w = \"${COMP_WORDS[COMP_CWORD-1]}\" ]]; then\n            compopt -o filenames\n            COMPREPLY=($(compgen -f -- \"${COMP_WORDS[COMP_CWORD]}\"))\n            return 0\n        fi\n    done\n\n    # Use newline as only separator to allow space in completion values\n    IFS=$'\\n'\n    local completion_cmd=\"${COMP_WORDS[0]}\"\n\n    # for an alias, get the real script behind it\n    completion_cmd_type=$(type -t $completion_cmd)\n    if [[ $completion_cmd_type == \"alias\" ]]; then\n        completion_cmd=$(alias $completion_cmd | sed -E \"s/alias $completion_cmd='(.*)'/\\1/\")\n    elif [[ $completion_cmd_type == \"file\" ]]; then\n        completion_cmd=$(type -p $completion_cmd)\n    fi\n\n    if [[ $completion_cmd_type != \"function\" && ! -x $completion_cmd ]]; then\n        return 1\n    fi\n\n    local cur prev words cword\n    _get_comp_words_by_ref -n := cur prev words cword\n\n    # Crystal doesn\\'t get the script as the first arg, so remove it and decrement cword by 1 to compensate\n    cword=$(expr $cword - 1)\n    words=(\"${words[@]:1}\")\n\n    local completecmd=(\"$completion_cmd\" \"_complete\" \"--no-interaction\" \"-sbash\" \"-c$cword\" \"-a<%= @version %>\")\n    for w in ${words[@]}; do\n        w=$(printf -- '%b' \"$w\")\n        # remove quotes from typed values\n        quote=\"${w:0:1}\"\n        if [ \"$quote\" == \\' ]; then\n            w=\"${w%\\'}\"\n            w=\"${w#\\'}\"\n        elif [ \"$quote\" == \\\" ]; then\n            w=\"${w%\\\"}\"\n            w=\"${w#\\\"}\"\n        fi\n        # empty values are ignored\n        if [ ! -z \"$w\" ]; then\n            completecmd+=(\"-i$w\")\n        fi\n    done\n\n    local sfcomplete\n    if sfcomplete=$(${completecmd[@]} 2>&1); then\n        local quote suggestions\n        quote=${cur:0:1}\n\n        # Use single quotes by default if suggestions contains backslash (FQCN)\n        if [ \"$quote\" == '' ] && [[ \"$sfcomplete\" =~ \\\\ ]]; then\n            quote=\\'\n        fi\n\n        if [ \"$quote\" == \\' ]; then\n            # single quotes: no additional escaping (does not accept ' in values)\n            suggestions=$(for s in $sfcomplete; do printf $'%q%q%q\\n' \"$quote\" \"$s\" \"$quote\"; done)\n        elif [ \"$quote\" == \\\" ]; then\n            # double quotes: double escaping for \\ $ ` \"\n            suggestions=$(for s in $sfcomplete; do\n                s=${s//\\\\/\\\\\\\\}\n                s=${s//\\$/\\\\\\$}\n                s=${s//\\`/\\\\\\`}\n                s=${s//\\\"/\\\\\\\"}\n                printf $'%q%q%q\\n' \"$quote\" \"$s\" \"$quote\";\n            done)\n        else\n            # no quotes: double escaping\n            suggestions=$(for s in $sfcomplete; do printf $'%q\\n' $(printf '%q' \"$s\"); done)\n        fi\n        COMPREPLY=($(IFS=$'\\n' compgen -W \"$suggestions\" -- $(printf -- \"%q\" \"$cur\")))\n        __ltrim_colon_completions \"$cur\"\n    else\n        if [[ \"$sfcomplete\" != *\"Command \\\"_complete\\\" is not defined.\"* ]]; then\n            >&2 echo\n            >&2 echo $sfcomplete\n        fi\n\n        return 1\n    fi\n}\n\ncomplete -F _athena_<%= @command_name %> <%= @command_name %> ./<%= @command_name %> ./bin/<%= @command_name %>\n"
  },
  {
    "path": "src/components/console/src/completion/output/completion.fish",
    "content": "# Adapted from https://github.com/symfony/symfony/blob/503a7b3cb62fb6de70176b07bd1c4242e3addc5b/src/Symfony/Component/Console/Resources/completion.fish\n# Crystal doesn\\'t get the script as the first arg, so remove it and decrement c by 1 to compensate\n\nfunction _athena_<%= @command_name %>\n    set athena_cmd (commandline -o)\n    set c (math (count (commandline -oc)) - 1)\n\n    set completecmd \"$athena_cmd[1]\" \"_complete\" \"--no-interaction\" \"-sfish\" \"-a<%= @version %>\"\n\n    for i in $athena_cmd[2..]\n        if [ $i != \"\" ]\n            set completecmd $completecmd \"-i$i\"\n        end\n    end\n\n    set completecmd $completecmd \"-c$c\"\n\n    set sfcomplete ($completecmd)\n\n    for i in $sfcomplete\n        echo $i\n    end\nend\n\ncomplete -c '<%= @command_name %>' -a '(_athena_<%= @command_name %>)' -f\n"
  },
  {
    "path": "src/components/console/src/completion/output/completion.zsh",
    "content": "#compdef <%= @command_name %>\n\n#\n# zsh completions for <%= @command_name %>\n#\n# References:\n#   - https://github.com/symfony/symfony/blob/503a7b3cb62fb6de70176b07bd1c4242e3addc5b/src/Symfony/Component/Console/Resources/completion.zsh\n\n_athena_<%= @command_name %>() {\n    local lastParam flagPrefix requestComp out comp\n    local -a completions\n\n    # The user could have moved the cursor backwards on the command-line.\n    # We need to trigger completion from the $CURRENT location, so we need\n    # to truncate the command-line ($words) up to the $CURRENT location.\n    # (We cannot use $CURSOR as its value does not work when a command is an alias.)\n    words=(\"${=words[1,CURRENT]}\") lastParam=${words[-1]}\n\n    # For zsh, when completing a flag with an = (e.g., <%= @command_name %> -n=<TAB>)\n    # completions must be prefixed with the flag\n    setopt local_options BASH_REMATCH\n    if [[ \"${lastParam}\" =~ '-.*=' ]]; then\n        # We are dealing with a flag with an =\n        flagPrefix=\"-P ${BASH_REMATCH}\"\n    fi\n\n    # Prepare the command to obtain completions\n    # Crystal doesn\\'t get the script as the first arg, so skip it when iterating over `words` and decrement CURRENT by 2 instead of 1 to compensate\n    requestComp=\"${words[0]} ${words[1]} _complete --no-interaction -szsh -a<%= @version %> -c$((CURRENT-2))\" i=\"\"\n    for w in ${words[@]:1}; do\n        w=$(printf -- '%b' \"$w\")\n        # remove quotes from typed values\n        quote=\"${w:0:1}\"\n        if [ \"$quote\" = \\' ]; then\n            w=\"${w%\\'}\"\n            w=\"${w#\\'}\"\n        elif [ \"$quote\" = \\\" ]; then\n            w=\"${w%\\\"}\"\n            w=\"${w#\\\"}\"\n        fi\n        # empty values are ignored\n        if [ ! -z \"$w\" ]; then\n            i=\"${i}-i${w} \"\n        fi\n    done\n\n    # Ensure at least 1 input\n    if [ \"${i}\" = \"\" ]; then\n        requestComp=\"${requestComp} -i\\\" \\\"\"\n    else\n        requestComp=\"${requestComp} ${i}\"\n    fi\n\n    # Use eval to handle any environment variables and such\n    out=$(eval ${requestComp} 2>/dev/null)\n\n    while IFS='\\n' read -r comp; do\n        if [ -n \"$comp\" ]; then\n            # If requested, completions are returned with a description.\n            # The description is preceded by a TAB character.\n            # For zsh\\'s _describe, we need to use a : instead of a TAB.\n            # We first need to escape any : as part of the completion itself.\n            comp=${comp//:/\\\\:}\n            local tab=$(printf '\\t')\n            comp=${comp//$tab/:}\n            completions+=${comp}\n        fi\n    done < <(printf \"%s\\n\" \"${out[@]}\")\n\n    # Let inbuilt _describe handle completions\n    eval _describe \"completions\" completions $flagPrefix\n    return $?\n}\n\ncompdef _athena_<%= @command_name %> <%= @command_name %> ./<%= @command_name %>\n"
  },
  {
    "path": "src/components/console/src/completion/output/fish.cr",
    "content": "require \"./interface\"\n\n# :nodoc:\nstruct Athena::Console::Completion::Output::Fish < Athena::Console::Completion::Output::Interface\n  # :nodoc:\n  record Script, command_name : String, version : Int32 do\n    ECR.def_to_s \"#{__DIR__}/completion.fish\"\n  end\n\n  def write(suggestions : ACON::Completion::Suggestions, output : ACON::Output::Interface) : Nil\n    values = suggestions.suggested_values.map &.to_s\n\n    suggestions.suggested_options.each do |option|\n      values << \"--#{option.name}\"\n\n      if option.negatable?\n        values << \"--no-#{option.name}\"\n      end\n    end\n\n    output.print values.join \"\\n\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/completion/output/interface.cr",
    "content": "require \"../suggestions\"\n\n# :nodoc:\nmodule Athena::Console::Completion::Output\n  abstract struct Interface\n    # Returns a string representation of the args passed to the command.\n    abstract def write(suggestions : ACON::Completion::Suggestions, output : ACON::Output::Interface) : Nil\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/completion/output/zsh.cr",
    "content": "# :nodoc:\nstruct Athena::Console::Completion::Output::Zsh < Athena::Console::Completion::Output::Interface\n  # :nodoc:\n  record Script, command_name : String, version : Int32 do\n    ECR.def_to_s \"#{__DIR__}/completion.zsh\"\n  end\n\n  def write(suggestions : ACON::Completion::Suggestions, output : ACON::Output::Interface) : Nil\n    values = suggestions.suggested_values.map do |v|\n      \"#{v.value}#{(desc = v.description.presence) ? \"\\t#{desc}\" : \"\"}\"\n    end\n\n    suggestions.suggested_options.each do |option|\n      values << \"--#{option.name}#{(desc = option.description.presence) ? \"\\t#{desc}\" : \"\"}\"\n\n      if option.negatable?\n        values << \"--no-#{option.name}#{(desc = option.description.presence) ? \"\\t#{desc}\" : \"\"}\"\n      end\n    end\n\n    output.print \"#{values.join \"\\n\"}\\n\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/completion/suggestions.cr",
    "content": "# Stores all the suggested values/options for the current `ACON::Completion::Input`.\nclass Athena::Console::Completion::Suggestions\n  # Represents a single suggested values, plus optional description.\n  record SuggestedValue, value : String, description : String = \"\" do\n    def to_s(io : IO) : Nil\n      @value.to_s io\n    end\n  end\n\n  # Returns an array of the suggested `ACON::Input::Option`s.\n  getter suggested_options = [] of ACON::Input::Option\n\n  # Returns an array of the `ACON::Completion::Suggestions::SuggestedValue`s.\n  getter suggested_values = [] of ACON::Completion::Suggestions::SuggestedValue\n\n  # Adds each of the provided *values* to `#suggested_values`.\n  def suggest_values(*values : String) : self\n    self.suggest_values values\n  end\n\n  # Adds each of the provided *values* to `#suggested_values`.\n  def suggest_values(values : Enumerable(String)) : self\n    values.each do |option|\n      self.suggest_value option\n    end\n\n    self\n  end\n\n  # Adds the provided *value*, and optional *description* to `#suggested_values`.\n  def suggest_value(value : String, description : String = \"\") : self\n    self.suggest_value SuggestedValue.new value, description\n  end\n\n  # Adds the provided *value* to `#suggested_values`.\n  def suggest_value(value : ACON::Completion::Suggestions::SuggestedValue) : self\n    @suggested_values << value\n\n    self\n  end\n\n  # Adds each of the provided *options* to `#suggested_options`.\n  def suggest_options(options : ::Enumerable(ACON::Input::Option)) : self\n    options.each do |option|\n      self.suggest_option option\n    end\n\n    self\n  end\n\n  # Adds the provided *option* to `#suggested_options`.\n  def suggest_option(option : ACON::Input::Option) : self\n    @suggested_options << option\n\n    self\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/cursor.cr",
    "content": "# Provides an OO way to interact with the console window,\n# allows writing on any position of the output.\n#\n# ```\n# @[ACONA::AsCommand(\"cursor\")]\n# class CursorCommand < ACON::Command\n#   protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#     cursor = ACON::Cursor.new output\n#\n#     # Move the cursor to a specific column, row position.\n#     cursor.move_to_position 50, 3\n#\n#     # Write text at that location.\n#     output.puts \"Hello!\"\n#\n#     # Clear the current line.\n#     cursor.clear_line\n#\n#     ACON::Command::Status::SUCCESS\n#   end\n# end\n# ```\nstruct Athena::Console::Cursor\n  @output : ACON::Output::Interface\n  @input : IO\n\n  def initialize(@output : ACON::Output::Interface, @input : IO = STDIN); end\n\n  # Moves the cursor up *lines* lines.\n  def move_up(lines : Int32 = 1) : self\n    @output.print \"\\x1b[#{lines}A\"\n\n    self\n  end\n\n  # Moves the cursor down *lines* lines.\n  def move_down(lines : Int32 = 1) : self\n    @output.print \"\\x1b[#{lines}B\"\n\n    self\n  end\n\n  # Moves the cursor right *lines* lines.\n  def move_right(lines : Int32 = 1) : self\n    @output.print \"\\x1b[#{lines}C\"\n\n    self\n  end\n\n  # Moves the cursor left *lines* lines.\n  def move_left(lines : Int32 = 1) : self\n    @output.print \"\\x1b[#{lines}D\"\n\n    self\n  end\n\n  # Moves the cursor to the provided *column*.\n  def move_to_column(column : Int32) : self\n    @output.print \"\\x1b[#{column}G\"\n\n    self\n  end\n\n  # Moves the cursor to the provided *column*, *row* position.\n  def move_to_position(column : Int32, row : Int32) : self\n    @output.print \"\\x1b[#{row + 1};#{column}H\"\n\n    self\n  end\n\n  # Saves the current position such that it could be restored via `#restore_position`.\n  def save_position : self\n    @output.print \"\\x1b7\"\n\n    self\n  end\n\n  # Restores the position set via `#save_position`.\n  def restore_position : self\n    @output.print \"\\x1b8\"\n\n    self\n  end\n\n  # Hides the cursor.\n  def hide : self\n    @output.print \"\\x1b[?25l\"\n\n    self\n  end\n\n  # Shows the cursor.\n  def show : self\n    @output.print \"\\x1b[?25h\\x1b[?0c\"\n\n    self\n  end\n\n  # Clears the current line.\n  def clear_line : self\n    @output.print \"\\x1b[2K\"\n\n    self\n  end\n\n  # Clears the current line after the cursor's current position.\n  def clear_line_after : self\n    @output.print \"\\x1b[K\"\n\n    self\n  end\n\n  # Clears the output from the cursors' current position to the end of the screen.\n  def clear_output : self\n    @output.print \"\\x1b[0J\"\n\n    self\n  end\n\n  # Clears the entire screen.\n  def clear_screen : self\n    @output.print \"\\x1b[2J\"\n\n    self\n  end\n\n  # Returns the current column, row position of the cursor.\n  def current_position : {Int32, Int32}\n    {% if flag? :win32 %}\n      return {1, 1} unless @input.tty?\n\n      LibC.GetConsoleScreenBufferInfo(LibC.GetStdHandle(LibC::STDOUT_HANDLE), out csbi)\n      {csbi.dwCursorPosition.x.to_i32, csbi.dwCursorPosition.y.to_i32}\n    {% else %}\n      return {1, 1} unless @input.tty?\n\n      stty_mode = `stty -g`\n      system \"stty -icanon -echo\"\n\n      @input.print \"\\033[6n\"\n\n      bytes = @input.peek\n\n      system \"stty #{stty_mode}\"\n\n      String.new(bytes.not_nil!).match /\\e\\[(\\d+);(\\d+)R/\n\n      {$2.to_i, $1.to_i}\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/descriptor/application.cr",
    "content": "abstract class Athena::Console::Descriptor; end\n\n# :nodoc:\nrecord Athena::Console::Descriptor::Application, application : ACON::Application, namespace : String? = nil, show_hidden : Bool = false do\n  GLOBAL_NAMESPACE = \"_global\"\n\n  @commands : Hash(String, ACON::Command)? = nil\n  @namespaces : Hash(String, NamedTuple(id: String, commands: Array(String)))? = nil\n  @aliases : Hash(String, ACON::Command)? = nil\n\n  def commands : Hash(String, ACON::Command)\n    if @commands.nil?\n      self.inspect_application\n    end\n\n    @commands.not_nil!\n  end\n\n  def command(name : String) : ACON::Command\n    if !@commands.not_nil!.has_key?(name) && !@aliases.not_nil!.has_key?(name)\n      raise ACON::Exception::CommandNotFound.new \"Command '#{name}' does not exist.\"\n    end\n\n    @commands.not_nil![name]? || @aliases.not_nil![name]\n  end\n\n  def namespaces : Hash(String, NamedTuple(id: String, commands: Array(String)))\n    if @namespaces.nil?\n      self.inspect_application\n    end\n\n    @namespaces.not_nil!\n  end\n\n  private def inspect_application : Nil\n    commands = Hash(String, ACON::Command).new\n    namespaces = Hash(String, NamedTuple(id: String, commands: Array(String))).new\n    aliases = Hash(String, ACON::Command).new\n\n    all_commands = @application.commands((namespace = @namespace) ? @application.find_namespace(namespace) : nil)\n\n    self.sort_commands(all_commands).each do |namespace, command_hash|\n      names = Array(String).new\n\n      command_hash.each do |name, command|\n        next if command.name.nil? || (!@show_hidden && command.hidden?)\n\n        if name == command.name\n          commands[name] = command\n        else\n          aliases[name] = command\n        end\n\n        names << name\n      end\n\n      namespaces[namespace] = {id: namespace, commands: names}\n    end\n\n    @commands = commands\n    @namespaces = namespaces\n    @aliases = aliases\n  end\n\n  private def sort_commands(commands : Hash(String, ACON::Command)) : Hash(String, Hash(String, ACON::Command))\n    namespaced_commands = Hash(String, Hash(String, ACON::Command)).new\n    global_commands = Hash(String, ACON::Command).new\n    sorted_commands = Hash(String, Hash(String, ACON::Command)).new\n\n    commands.each do |name, command|\n      key = @application.extract_namespace name, 1\n      if key.in? \"\", GLOBAL_NAMESPACE\n        global_commands[name] = command\n      else\n        (namespaced_commands[key] ||= Hash(String, ACON::Command).new)[name] = command\n      end\n    end\n\n    unless global_commands.empty?\n      sorted_commands[GLOBAL_NAMESPACE] = self.sort_hash global_commands\n    end\n\n    unless namespaced_commands.empty?\n      namespaced_commands = self.sort_hash namespaced_commands\n      namespaced_commands.keys.sort!.each do |key|\n        sorted_commands[key] = self.sort_hash namespaced_commands[key]\n      end\n    end\n\n    sorted_commands\n  end\n\n  private def sort_hash(hash : Hash(String, Hash(String, Athena::Console::Command))) : Hash(String, Hash(String, Athena::Console::Command))\n    sorted_hash = Hash(String, Hash(String, Athena::Console::Command)).new\n\n    hash.keys.sort!.each do |k|\n      sorted_hash[k] = self.sort_hash hash[k]\n    end\n\n    sorted_hash\n  end\n\n  private def sort_hash(hash : Hash(String, ACON::Command)) : Hash(String, ACON::Command)\n    sorted_hash = Hash(String, ACON::Command).new\n\n    hash.keys.sort!.each do |k|\n      sorted_hash[k] = hash[k]\n    end\n\n    sorted_hash\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/descriptor/context.cr",
    "content": "class Athena::Console::Descriptor::Context\n  property format : String\n  property? raw_text : Bool\n  property? raw_output : Bool?\n  property namespace : String?\n  property total_width : Int32?\n  property? short : Bool\n\n  def initialize(\n    @format : String = \"txt\",\n    @raw_text : Bool = false,\n    @raw_output : Bool? = nil,\n    @namespace : String? = nil,\n    @total_width : Int32? = nil,\n    @short : Bool = false,\n  )\n  end\n\n  def_clone\nend\n"
  },
  {
    "path": "src/components/console/src/descriptor/descriptor.cr",
    "content": "require \"./interface\"\n\n# :nodoc:\nabstract class Athena::Console::Descriptor\n  include Athena::Console::Descriptor::Interface\n\n  getter! output : ACON::Output::Interface\n\n  def describe(output : ACON::Output::Interface, object : _, context : ACON::Descriptor::Context) : Nil\n    @output = output\n\n    self.describe object, context\n  end\n\n  protected abstract def describe(application : ACON::Application, context : ACON::Descriptor::Context) : Nil\n  protected abstract def describe(command : ACON::Command, context : ACON::Descriptor::Context) : Nil\n  protected abstract def describe(definition : ACON::Input::Definition, context : ACON::Descriptor::Context) : Nil\n  protected abstract def describe(argument : ACON::Input::Argument, context : ACON::Descriptor::Context) : Nil\n  protected abstract def describe(option : ACON::Input::Option, context : ACON::Descriptor::Context) : Nil\n\n  protected def describe(obj : _, context : ACON::Descriptor::Context) : Nil\n    raise \"BUG: Failed to describe #{obj}\"\n  end\n\n  protected def write(content : String, decorated : Bool = false) : Nil\n    self.output.print content, output_type: decorated ? Athena::Console::Output::Type::NORMAL : Athena::Console::Output::Type::RAW\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/descriptor/interface.cr",
    "content": "module Athena::Console::Descriptor::Interface\n  abstract def describe(output : ACON::Output::Interface, object : _, context : ACON::Descriptor::Context) : Nil\nend\n"
  },
  {
    "path": "src/components/console/src/descriptor/text.cr",
    "content": "# :nodoc:\n#\n# TODO: Should/can this be implemented via `to_s(io)` on each type?\nclass Athena::Console::Descriptor::Text < Athena::Console::Descriptor\n  protected def describe(application : ACON::Application, context : ACON::Descriptor::Context) : Nil\n    described_namespace = context.namespace\n    description = ACON::Descriptor::Application.new application, context.namespace\n\n    commands = description.commands.values\n\n    if context.raw_text?\n      width = self.width commands\n\n      commands.each do |command|\n        self.write_text sprintf(\"%-#{width}s %s\", command.name, command.description), context\n        self.write_text \"\\n\"\n      end\n\n      return\n    end\n\n    self.write_text \"#{application.help}\\n\\n\", context\n\n    self.write_text \"<comment>Usage:</comment>\\n\", context\n    self.write_text \"  command [options] [arguments]\\n\\n\", context\n\n    self.describe ACON::Input::Definition.new(application.definition.options), context\n\n    self.write_text \"\\n\"\n    self.write_text \"\\n\"\n\n    commands = description.commands\n    namespaces = description.namespaces\n\n    if described_namespace && !namespaces.empty?\n      namespaces.values.first[:commands].each do |n|\n        commands[n] = description.command n\n      end\n    end\n\n    width = self.width(\n      namespaces.values.flat_map do |n|\n        commands.keys & n[:commands]\n      end.uniq!\n    )\n\n    if described_namespace\n      self.write_text %(<comment>Available commands for the '#{described_namespace}' namespace:</comment>), context\n    else\n      self.write_text \"<comment>Available commands:</comment>\", context\n    end\n\n    namespaces.each_value do |namespace|\n      namespace[:commands].select! { |c| commands.has_key? c }\n\n      next if namespace[:commands].empty?\n\n      if !described_namespace && namespace[:id] != ACON::Descriptor::Application::GLOBAL_NAMESPACE\n        self.write_text \"\\n\"\n        self.write_text \" <comment>#{namespace[:id]}</comment>\", context\n      end\n\n      namespace[:commands].each do |name|\n        self.write_text \"\\n\"\n        spacing_width = width - ACON::Helper.width name\n        command = commands[name]\n        command_aliases = name === command.name ? self.command_aliases_text command : \"\"\n\n        self.write_text \"  <info>#{name}</info>#{\" \" * spacing_width}#{command_aliases}#{command.description}\", context\n      end\n    end\n\n    self.write_text \"\\n\"\n  end\n\n  protected def describe(argument : ACON::Input::Argument, context : ACON::Descriptor::Context) : Nil\n    default = if !argument.default.nil? && !argument.default.is_a?(Array)\n                %(<comment> [default: #{self.format_default_value argument.default}]</comment>)\n              else\n                \"\"\n              end\n\n    total_width = context.total_width || ACON::Helper.width argument.name\n    spacing_width = total_width - argument.name.size\n\n    self.write_text(\n      sprintf(\n        \"  <info>%s</info>  %s%s%s\",\n        argument.name,\n        \" \" * spacing_width,\n        argument.description.gsub(/\\s*[\\r\\n]\\s*/, \"\\n#{\" \" * (total_width + 4)}\"),\n        default\n      ),\n      context\n    )\n  end\n\n  protected def describe(command : ACON::Command, context : ACON::Descriptor::Context) : Nil\n    command.merge_application_definition false\n\n    if description = command.description.presence\n      self.write_text \"<comment>Description:</comment>\", context\n      self.write_text \"\\n\"\n      self.write_text \"  #{description}\"\n      self.write_text \"\\n\\n\"\n    end\n\n    self.write_text \"<comment>Usage:</comment>\", context\n\n    ([command.synopsis(true)] + command.aliases + command.usages).each do |usage|\n      self.write_text \"\\n\"\n      self.write_text \"  #{ACON::Formatter::Output.escape usage}\", context\n    end\n\n    self.write_text \"\\n\"\n\n    definition = command.definition\n\n    if !definition.options.empty? || !definition.arguments.empty?\n      self.write_text \"\\n\"\n      self.describe definition, context\n      self.write_text \"\\n\"\n    end\n\n    if (help = command.processed_help).presence && help != description\n      self.write_text \"\\n\"\n      self.write_text \"<comment>Help:</comment>\", context\n      self.write_text \"\\n\"\n      self.write_text \"  #{help.gsub(\"\\n\", \"\\n  \")}\", context\n      self.write_text \"\\n\"\n    end\n  end\n\n  protected def describe(definition : ACON::Input::Definition, context : ACON::Descriptor::Context) : Nil\n    total_width = self.calculate_total_width_for_options definition.options\n\n    definition.arguments.each_value do |arg|\n      total_width = Math.max total_width, ACON::Helper.width(arg.name)\n    end\n\n    unless definition.arguments.empty?\n      self.write_text \"<comment>Arguments:</comment>\", context\n      self.write_text \"\\n\"\n\n      new_context = context.clone\n      new_context.total_width = total_width\n\n      definition.arguments.each_value do |arg|\n        self.describe arg, new_context\n        self.write_text \"\\n\"\n      end\n    end\n\n    if !definition.arguments.empty? && !definition.options.empty?\n      self.write_text \"\\n\"\n    end\n\n    unless definition.options.empty?\n      later_options = [] of ACON::Input::Option\n\n      self.write_text \"<comment>Options:</comment>\", context\n\n      definition.options.each_value do |option|\n        if (option.shortcut || \"\").size > 1\n          later_options << option\n          next\n        end\n\n        new_context = context.clone\n        new_context.total_width = total_width\n\n        self.write_text \"\\n\"\n        self.describe option, new_context\n      end\n\n      later_options.each do |option|\n        self.write_text \"\\n\"\n\n        new_context = context.clone\n        new_context.total_width = total_width\n\n        self.describe option, new_context\n      end\n    end\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  protected def describe(option : ACON::Input::Option, context : ACON::Descriptor::Context) : Nil\n    if option.accepts_value? && !option.default.nil? && (!option.default.is_a?(Array) || !option.default.as(Array).empty?)\n      default = %(<comment> [default: #{self.format_default_value option.default}]</comment>)\n    else\n      default = \"\"\n    end\n\n    value = \"\"\n    if option.accepts_value?\n      value = \"=#{option.name.upcase}\"\n\n      if option.value_optional?\n        value = \"[#{value}]\"\n      end\n    end\n\n    total_width = context.total_width || self.calculate_total_width_for_options [option]\n    synopsis = sprintf(\n      \"%s%s\",\n      (s = option.shortcut) ? sprintf(\"-%s, \", s) : \"    \",\n      (option.negatable? ? \"--%<name>s|--no-%<name>s\" : \"--%<name>s%<value>s\") % {name: option.name, value: value}\n    )\n\n    spacing_width = total_width - ACON::Helper.width synopsis\n\n    self.write_text(\n      sprintf(\n        \"  <info>%s</info>  %s%s%s%s\",\n        synopsis,\n        \" \" * spacing_width,\n        option.description.gsub(/\\s*[\\r\\n]\\s*/, \"\\n#{\" \" * (total_width + 4)}\"),\n        default,\n        option.is_array? ? \"<comment> (multiple values allowed)</comment>\" : \"\"\n      ),\n      context\n    )\n  end\n\n  private def calculate_total_width_for_options(options : Hash(String, ACON::Input::Option)) : Int32\n    self.calculate_total_width_for_options options.values\n  end\n\n  private def calculate_total_width_for_options(options : Array(ACON::Input::Option)) : Int32\n    return 0 if options.empty?\n\n    options.max_of do |o|\n      name_length = 1 + Math.max(ACON::Helper.width(o.shortcut || \"\"), 1) + 4 + ACON::Helper.width(o.name)\n\n      if o.negatable?\n        name_length += 6 + ACON::Helper.width(o.name)\n      elsif o.accepts_value?\n        name_length += 1 + ACON::Helper.width(o.name) + (o.value_optional? ? 2 : 0)\n      end\n\n      name_length\n    end\n  end\n\n  private def command_aliases_text(command : ACON::Command) : String\n    String.build do |io|\n      unless (aliases = command.aliases).empty?\n        io << '['\n        aliases.join io, '|'\n        io << ']' << ' '\n      end\n    end\n  end\n\n  private def format_default_value(default)\n    case default\n    when String\n      %(\"#{ACON::Formatter::Output.escape default}\")\n    when Enumerable\n      %([#{default.map { |item| %|\"#{ACON::Formatter::Output.escape item.to_s}\"| }.join \",\"}])\n    else\n      default\n    end\n  end\n\n  private def width(commands : Array(ACON::Command) | Array(String)) : Int32\n    widths = Array(Int32).new\n\n    commands.each do |command|\n      case command\n      in ACON::Command\n        widths << ACON::Helper.width command.name.not_nil!\n\n        command.aliases.each do |a|\n          widths << ACON::Helper.width a\n        end\n      in String\n        widths << ACON::Helper.width command\n      end\n    end\n\n    widths.empty? ? 0 : widths.max + 2\n  end\n\n  private def write_text(content : String, context : ACON::Descriptor::Context? = nil) : Nil\n    unless ctx = context\n      return self.write content, true\n    end\n\n    raw_output = true\n\n    ctx.raw_output?.try do |ro|\n      raw_output = !ro\n    end\n\n    if ctx.raw_text?\n      content = content.gsub(/(?:<\\/?[^>]*>)|(?:<!--(.*?)-->[\\n]?)/, \"\") # TODO: Use a more robust strip_tags implementation.\n    end\n\n    self.write(\n      content,\n      raw_output\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/exception/command_not_found.cr",
    "content": "class Athena::Console::Exception::CommandNotFound < ArgumentError\n  include Athena::Console::Exception\n\n  getter alternatives : Array(String)\n\n  def initialize(message : String, @alternatives : Array(String) = [] of String, @code : Int32 = 0)\n    super message\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/exception/invalid_argument.cr",
    "content": "class Athena::Console::Exception::InvalidArgument < ArgumentError\n  include Athena::Console::Exception\n\n  def initialize(message : String, @code : Int32 = 0)\n    super message\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/exception/invalid_option.cr",
    "content": "class Athena::Console::Exception::InvalidOption < ArgumentError\n  include Athena::Console::Exception\n\n  def initialize(message : String, @code : Int32 = 0)\n    super message\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/exception/logic.cr",
    "content": "# Represents a code logic error that should lead directly to a fix in your code.\nclass Athena::Console::Exception::Logic < ::Exception\n  include Athena::Console::Exception\n\n  def initialize(message : String, @code : Int32 = 0, cause : ::Exception? = nil)\n    super message, cause\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/exception/missing_input.cr",
    "content": "require \"./runtime\"\n\nclass Athena::Console::Exception::MissingInput < Athena::Console::Exception::Runtime\nend\n"
  },
  {
    "path": "src/components/console/src/exception/namespace_not_found.cr",
    "content": "class Athena::Console::Exception::NamespaceNotFound < Athena::Console::Exception::CommandNotFound\nend\n"
  },
  {
    "path": "src/components/console/src/exception/runtime.cr",
    "content": "class Athena::Console::Exception::Runtime < RuntimeError\n  include Athena::Console::Exception\n\n  def initialize(message : String, @code : Int32 = 0, cause : ::Exception? = nil)\n    super message, cause\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/ext/terminal.cr",
    "content": "{% if flag?(:win32) %}\n  lib LibC\n    STDOUT_HANDLE = 0xFFFFFFF5\n\n    struct Point\n      x : UInt16\n      y : UInt16\n    end\n\n    struct SmallRect\n      left : UInt16\n      top : UInt16\n      right : UInt16\n      bottom : UInt16\n    end\n\n    struct ScreenBufferInfo\n      dwSize : Point\n      dwCursorPosition : Point\n      wAttributes : UInt16\n      srWindow : SmallRect\n      dwMaximumWindowSize : Point\n    end\n\n    alias Handle = Void*\n    alias ScreenBufferInfoPtr = ScreenBufferInfo*\n\n    fun GetConsoleScreenBufferInfo(handle : Handle, info : ScreenBufferInfoPtr) : Bool\n    fun GetStdHandle(handle : UInt32) : Handle\n  end\n{% else %}\n  lib LibC\n    struct Winsize\n      ws_row : UShort\n      ws_col : UShort\n      ws_xpixel : UShort\n      ws_ypixel : UShort\n    end\n\n    # TIOCGWINSZ is a platform dependent magic number passed to ioctl that requests the current terminal window size.\n    # Values lifted from https://github.com/crystal-term/screen/blob/ea51ee8d1f6c286573c41a7e784d31c80af7b9bb/src/term-screen.cr#L86-L88.\n    {% begin %}\n      {% if flag?(:darwin) || flag?(:bsd) %}\n        TIOCGWINSZ = 0x40087468\n      {% elsif flag?(:unix) %}\n        TIOCGWINSZ = 0x5413\n      {% else %} # Solaris\n        TIOCGWINSZ = 0x5468\n      {% end %}\n    {% end %}\n\n    fun ioctl(fd : Int, request : ULong, ...) : Int\n  end\n{% end %}\n"
  },
  {
    "path": "src/components/console/src/formatter/interface.cr",
    "content": "require \"./output_style_interface\"\n\n# A container that stores and applies `ACON::Formatter::OutputStyleInterface`.\n# Is responsible for formatting outputted messages as per their styles.\nmodule Athena::Console::Formatter::Interface\n  # Sets if output messages should be decorated.\n  abstract def decorated=(@decorated : Bool)\n\n  # Returns `true` if output messages will be decorated, otherwise `false`.\n  abstract def decorated? : Bool\n\n  # Assigns the provided *style* to the provided *name*.\n  abstract def set_style(name : String, style : ACON::Formatter::OutputStyleInterface) : Nil\n\n  # Returns `true` if `self` has a style with the provided *name*, otherwise `false`.\n  abstract def has_style?(name : String) : Bool\n\n  # Returns an `ACON::Formatter::OutputStyleInterface` with the provided *name*.\n  abstract def style(name : String) : ACON::Formatter::OutputStyleInterface\n\n  # Formats the provided *message* according to the stored styles.\n  abstract def format(message : String?) : String\nend\n"
  },
  {
    "path": "src/components/console/src/formatter/null.cr",
    "content": "require \"./interface\"\n\n# :nodoc:\nclass Athena::Console::Formatter::Null\n  include Athena::Console::Formatter::Interface\n\n  @style : ACON::Formatter::NullStyle? = nil\n\n  def decorated=(@decorated : Bool)\n  end\n\n  def decorated? : Bool\n    false\n  end\n\n  def set_style(name : String, style : ACON::Formatter::OutputStyleInterface) : Nil\n  end\n\n  def has_style?(name : String) : Bool\n    false\n  end\n\n  def style(name : String) : ACON::Formatter::OutputStyleInterface\n    @style ||= ACON::Formatter::NullStyle.new\n  end\n\n  def format(message : String?) : String\n    message\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/formatter/null_style.cr",
    "content": "# :nodoc:\nclass Athena::Console::Formatter::NullStyle\n  include Athena::Console::Formatter::OutputStyleInterface\n\n  # :inherit:\n  def foreground=(foreground : Colorize::Color)\n  end\n\n  # :inherit:\n  def background=(background : Colorize::Color)\n  end\n\n  # :inherit:\n  def add_option(option : Colorize::Mode) : Nil\n  end\n\n  # :inherit:\n  def remove_option(option : Colorize::Mode) : Nil\n  end\n\n  # :inherit:\n  def apply(text : String) : String\n    text\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/formatter/output.cr",
    "content": "require \"./wrappable_interface\"\n\n# Default implementation of `ACON::Formatter::WrappableInterface`.\nclass Athena::Console::Formatter::Output\n  include Athena::Console::Formatter::WrappableInterface\n\n  # Returns a new string where the special `<` characters in the provided *text* are escaped.\n  def self.escape(text : String) : String\n    text = text.gsub /([^\\\\\\\\]?)</, \"\\\\1\\\\<\"\n\n    self.escape_trailing_backslash text\n  end\n\n  # Returns a new string where trailing `\\` in the provided *text* is escaped.\n  def self.escape_trailing_backslash(text : String) : String\n    if text.ends_with? '\\\\'\n      len = text.size\n      text = text.rstrip '\\\\'\n      text = text.gsub \"\\0\", \"\"\n      text += \"\\0\" * (len - text.size)\n    end\n\n    text\n  end\n\n  # :nodoc:\n  getter style_stack : ACON::Formatter::OutputStyleStack = ACON::Formatter::OutputStyleStack.new\n\n  # :inherit:\n  property? decorated : Bool\n\n  @styles = Hash(String, ACON::Formatter::OutputStyleInterface).new\n  @current_line_length = 0\n\n  def initialize(@decorated : Bool = false, styles : Colorize::Mode? = nil)\n    self.set_style \"error\", ACON::Formatter::OutputStyle.new(:white, :red)\n    self.set_style \"info\", ACON::Formatter::OutputStyle.new(:green)\n    self.set_style \"comment\", ACON::Formatter::OutputStyle.new(:yellow)\n    self.set_style \"question\", ACON::Formatter::OutputStyle.new(:black, :cyan)\n  end\n\n  # :inherit:\n  def set_style(name : String, style : ACON::Formatter::OutputStyleInterface) : Nil\n    @styles[name.downcase] = style\n  end\n\n  # :inherit:\n  def has_style?(name : String) : Bool\n    @styles.has_key? name.downcase\n  end\n\n  # :inherit:\n  def style(name : String) : ACON::Formatter::OutputStyleInterface\n    @styles[name.downcase]? || raise ACON::Exception::InvalidArgument.new \"Undefined style: '#{name}'.\"\n  end\n\n  # :inherit:\n  def format(message : String?) : String\n    self.format_and_wrap message, 0\n  end\n\n  # :inherit:\n  # ameba:disable Metrics/CyclomaticComplexity\n  def format_and_wrap(message : String?, width : Int32) : String\n    return \"\" if message.nil?\n\n    offset = 0\n    output = \"\"\n\n    @current_line_length = 0\n\n    message.scan(/<(([a-z](?:[^\\\\<>]*+ | \\\\.)*) | \\/([a-z][^<>]*+)?)>/ix) do |match|\n      pos = match.begin.not_nil!\n      text = match[0]\n\n      next if pos != 0 && '\\\\' == message[pos - 1]\n\n      # Add text up to next tag.\n      output += self.apply_current_style message[offset, pos - offset], output, width\n      offset = pos + text.size\n\n      tag = if open = '/' != text.char_at(1)\n              match[2]\n            else\n              match[3]? || \"\"\n            end\n\n      if !open && !tag.presence\n        # </>\n        @style_stack.pop\n      elsif (style = self.create_style_from_string(tag)).nil?\n        output += self.apply_current_style text, output, width\n      elsif open\n        @style_stack << style\n      else\n        @style_stack.pop style\n      end\n    end\n\n    output += self.apply_current_style message[offset...], output, width\n\n    if output.includes? '\\0'\n      return output\n        .gsub(\"\\0\", '\\\\')\n        .gsub(\"\\\\<\", '<')\n    end\n\n    output.gsub /\\\\</, \"<\"\n  end\n\n  protected def create_style_from_string(string : String) : ACON::Formatter::OutputStyleInterface?\n    if style = @styles[string]?\n      return style\n    end\n\n    matches = string.scan /([^=]+)=([^;]+)(;|$)/\n\n    return nil if matches.empty?\n\n    style = ACON::Formatter::OutputStyle.new\n    matches.each do |match|\n      case match[1].downcase\n      when \"fg\"   then style.foreground = match[2]\n      when \"bg\"   then style.background = match[2]\n      when \"href\" then style.href = match[2]\n      when \"options\"\n        match[2].downcase.scan /([^,;]+)/ do |option|\n          style.add_option option[1]\n        end\n      else\n        return nil\n      end\n    end\n\n    style\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def apply_current_style(text : String, current : String, width : Int32)\n    if text.empty?\n      return \"\"\n    end\n\n    if width.zero?\n      return self.decorated? ? @style_stack.current.apply(text) : text\n    end\n\n    if @current_line_length.zero? && !current.empty?\n      text = text.lstrip\n    end\n\n    if !@current_line_length.zero?\n      i = width - @current_line_length\n      prefix = \"#{text[0, i]}\\n\"\n      text = text[i...]? || \"\"\n    else\n      prefix = \"\"\n    end\n\n    # TODO: Something about matching `~(\\\\n)$~`.\n    text = \"#{prefix}#{text.gsub(/([^\\n]{#{width}})\\ */, \"\\\\1\\n\")}\"\n    text = text.chomp\n\n    if @current_line_length.zero? && !current.empty? && !current.ends_with? \"\\n\"\n      text = \"\\n#{text}\"\n    end\n\n    lines = text.split \"\\n\"\n\n    lines.each do |line|\n      @current_line_length += line.size\n\n      @current_line_length = 0 if width <= @current_line_length\n    end\n\n    if self.decorated?\n      lines.map! do |line|\n        @style_stack.current.apply line\n      end\n    end\n\n    lines.join \"\\n\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/formatter/output_formatter_style_stack.cr",
    "content": "# :nodoc:\nstruct Athena::Console::Formatter::OutputStyleStack\n  property empty_style : ACON::Formatter::OutputStyleInterface\n\n  @styles = Array(ACON::Formatter::OutputStyleInterface).new\n\n  def initialize(@empty_style : ACON::Formatter::OutputStyleInterface = ACON::Formatter::OutputStyle.new)\n    self.reset\n  end\n\n  def reset : Nil\n    @styles.clear\n  end\n\n  def <<(style : ACON::Formatter::OutputStyleInterface) : Nil\n    @styles << style\n  end\n\n  def pop(style : ACON::Formatter::OutputStyleInterface? = nil) : ACON::Formatter::OutputStyleInterface\n    return @empty_style if @styles.empty?\n\n    return @styles.pop if style.nil?\n\n    if match_index = @styles.rindex { |stacked_style| style.apply(\"\") == stacked_style.apply(\"\") }\n      matched_style = @styles[match_index]\n\n      @styles = @styles[0...match_index]\n\n      return matched_style\n    end\n\n    raise ACON::Exception::InvalidArgument.new \"Provided style is not present in the stack.\"\n  end\n\n  def current : ACON::Formatter::OutputStyleInterface\n    @styles.last? || @empty_style\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/formatter/output_style.cr",
    "content": "require \"colorize\"\nrequire \"./output_style_interface\"\n\n# Default implementation of `ACON::Formatter::OutputStyleInterface`.\nstruct Athena::Console::Formatter::OutputStyle\n  include Athena::Console::Formatter::OutputStyleInterface\n\n  # :inherit:\n  setter foreground : Colorize::Color = :default\n\n  # :inherit:\n  setter background : Colorize::Color = :default\n\n  # :inherit:\n  setter options : Colorize::Mode = :none\n\n  # Sets the `href` that `self` should link to.\n  setter href : String? = nil\n\n  # :nodoc:\n  getter? handles_href_gracefully : Bool do\n    \"JetBrains-JediTerm\" != ENV[\"TERMINAL_EMULATOR\"]? && (!ENV.has_key?(\"KONSOLE_VERSION\") || ENV[\"KONSOLE_VERSION\"].to_i > 201_100)\n  end\n\n  def initialize(foreground : Colorize::Color | String = :default, background : Colorize::Color | String = :default, @options : Colorize::Mode = :none)\n    self.foreground = foreground\n    self.background = background\n  end\n\n  # :inherit:\n  def add_option(option : Colorize::Mode) : Nil\n    @options |= option\n  end\n\n  # :ditto:\n  def add_option(option : String) : Nil\n    self.add_option Colorize::Mode.parse option\n  end\n\n  # :inherit:\n  def background=(color : String)\n    if hex_value = color.lchop? '#'\n      r, g, b = hex_value.hexbytes\n      return @background = Colorize::ColorRGB.new r, g, b\n    end\n\n    @background = Colorize::ColorANSI.parse color\n  end\n\n  # :inherit:\n  def foreground=(foreground : String)\n    if hex_value = foreground.lchop? '#'\n      r, g, b = hex_value.hexbytes\n      return @foreground = Colorize::ColorRGB.new r, g, b\n    end\n\n    @foreground = Colorize::ColorANSI.parse foreground\n  end\n\n  # :inherit:\n  def remove_option(option : Colorize::Mode) : Nil\n    @options ^= option\n  end\n\n  # :ditto:\n  def remove_option(option : String) : Nil\n    self.remove_option Colorize::Mode.parse option\n  end\n\n  # :inherit:\n  def apply(text : String) : String\n    if (href = @href) && self.handles_href_gracefully?\n      text = \"\\e]8;;#{href}\\e\\\\#{text}\\e]8;;\\e\\\\\"\n    end\n\n    return text if self.default?\n\n    apply_color text\n  end\n\n  # TODO: Remove methods below when/if https://github.com/crystal-lang/crystal/pull/16052 is merged/released.\n  # Should then bump min crystal version.\n\n  private def apply_color(text : String) : String\n    String.build do |io|\n      printed = false\n\n      io << \"\\e[\"\n\n      unless @foreground == Colorize::ColorANSI::Default\n        @foreground.fore io\n        printed = true\n      end\n\n      unless @background == Colorize::ColorANSI::Default\n        io << ';' if printed\n        @background.back io\n        printed = true\n      end\n\n      each_code(@options) do |flag|\n        io << ';' if printed\n        io << flag\n        printed = true\n      end\n\n      io << 'm'\n\n      io << text\n\n      printed = false\n\n      io << \"\\e[\"\n\n      unless @foreground == Colorize::ColorANSI::Default\n        io << ';' if printed\n        io << 39\n        printed = true\n      end\n\n      unless @background == Colorize::ColorANSI::Default\n        io << ';' if printed\n        io << 49\n        printed = true\n      end\n\n      each_code(@options, true) do |flag|\n        io << ';' if printed\n        io << flag\n        printed = true\n      end\n\n      io << 'm'\n    end\n  end\n\n  private def default? : Bool\n    @foreground == Colorize::ColorANSI::Default && @background == Colorize::ColorANSI::Default && @options.none?\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def each_code(mode : Colorize::Mode, unset : Bool = false, &)\n    yield (unset ? \"22\" : \"1\") if mode.bold?\n    yield (unset ? \"22\" : \"2\") if mode.dim?\n    yield (unset ? \"23\" : \"3\") if mode.italic?\n    yield (unset ? \"24\" : \"4\") if mode.underline?\n    yield (unset ? \"25\" : \"5\") if mode.blink?\n    yield (unset ? \"26\" : \"6\") if mode.blink_fast?\n    yield (unset ? \"27\" : \"7\") if mode.reverse?\n    yield (unset ? \"28\" : \"8\") if mode.hidden?\n    yield (unset ? \"29\" : \"9\") if mode.strikethrough?\n    yield (unset ? \"24\" : \"21\") if mode.double_underline?\n    yield (unset ? \"55\" : \"53\") if mode.overline?\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/formatter/output_style_interface.cr",
    "content": "require \"colorize\"\n\n# Output styles represent reusable formatting information that can be used when formatting output messages.\n# `Athena::Console` comes bundled with a few common styles including:\n#\n# * error\n# * info\n# * comment\n# * question\n#\n# Whenever you output text via an `ACON::Output::Interface`, you can surround the text with tags to color its output. For example:\n#\n# ```\n# # Green text\n# output.puts \"<info>foo</info>\"\n#\n# # Yellow text\n# output.puts \"<comment>foo</comment>\"\n#\n# # Black text on a cyan background\n# output.puts \"<question>foo</question>\"\n#\n# # White text on a red background\n# output.puts \"<error>foo</error>\"\n# ```\n#\n# ## Custom Styles\n#\n# Custom styles can also be defined/used:\n#\n# ```\n# my_style = ACON::Formatter::OutputStyle.new :red, \"#f87b05\", Colorize::Mode[:bold, :underline]\n# output.formatter.set_style \"fire\", my_style\n#\n# output.puts \"<fire>foo</>\"\n# ```\n#\n# ### Global Custom Styles\n#\n# You can also make your style global by extending `ACON::Application` and adding it within the `#configure_io` method:\n#\n# ```\n# class MyCustomApplication < ACON::Application\n#   protected def configure_io(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil\n#     super\n#\n#     my_style = ACON::Formatter::OutputStyle.new :red, \"#f87b05\", Colorize::Mode[:bold, :underline]\n#     output.formatter.set_style \"fire\", my_style\n#   end\n# end\n# ```\n#\n# ## Inline Styles\n#\n# Styles can also be defined inline when printing a message:\n#\n# ```\n# # Using named colors\n# output.puts \"<fg=green>foo</>\"\n#\n# # Using hexadecimal colors\n# output.puts \"<fg=#c0392b>foo</>\"\n#\n# # Black text on a cyan background\n# output.puts \"<fg=black;bg=cyan>foo</>\"\n#\n# # Bold text on a yellow background\n# output.puts \"<bg=yellow;options=bold>foo</>\"\n#\n# # Bold text with underline.\n# output.puts \"<options=bold,underline>foo</>\"\n# ```\n#\n# ## Clickable Links\n#\n# Commands can use the special `href` tag to display links within the console.\n#\n# ```\n# output.puts \"<href=https://athenaframework.org>Athena</>\"\n# ```\n#\n# If your terminal [supports](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) it, you would be able to click\n# the text and have it open in your default browser. Otherwise, you will see it as regular text.\nmodule Athena::Console::Formatter::OutputStyleInterface\n  # Sets the foreground color of `self`.\n  abstract def foreground=(foreground : Colorize::Color)\n\n  # Sets the background color of `self`.\n  abstract def background=(background : Colorize::Color)\n\n  # Adds a text mode to `self`.\n  abstract def add_option(option : Colorize::Mode) : Nil\n\n  # Removes a text mode to `self`.\n  abstract def remove_option(option : Colorize::Mode) : Nil\n\n  # Applies `self` to the provided *text*.\n  abstract def apply(text : String) : String\nend\n"
  },
  {
    "path": "src/components/console/src/formatter/wrappable_interface.cr",
    "content": "require \"./interface\"\n\n# Extension of `ACON::Formatter::Interface` that supports word wrapping.\nmodule Athena::Console::Formatter::WrappableInterface\n  include Athena::Console::Formatter::Interface\n\n  # Formats the provided *message* according to the defined styles, wrapping it at the provided *width*.\n  # A width of `0` means no wrapping.\n  abstract def format_and_wrap(message : String?, width : Int32) : String\nend\n"
  },
  {
    "path": "src/components/console/src/helper/athena_question.cr",
    "content": "abstract class Athena::Console::Helper; end\n\nrequire \"./question\"\n\n# Extension of `ACON::Helper::Question` that provides more structured output.\n#\n# See `ACON::Style::Athena`.\nclass Athena::Console::Helper::AthenaQuestion < Athena::Console::Helper::Question\n  protected def write_error(output : ACON::Output::Interface, error : ::Exception) : Nil\n    if output.is_a? ACON::Style::Athena\n      output.new_line\n      output.error error.message || \"\"\n\n      return\n    end\n\n    super\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  protected def write_prompt(output : ACON::Output::Interface, question : ACON::Question::Base) : Nil\n    text = ACON::Formatter::Output.escape_trailing_backslash question.question\n    default = question.default\n\n    if question.multi_line?\n      text = \"#{text} (press #{self.eof_shortcut} to continue)\"\n    end\n\n    text = if default.nil?\n             \" <info>#{text}</info>:\"\n           elsif question.is_a? ACON::Question::Confirmation\n             %( <info>#{text} (yes/no)</info> [<comment>#{default ? \"yes\" : \"no\"}</comment>]:)\n           elsif question.is_a? ACON::Question::MultipleChoice\n             choices = question.choices\n             default = case default\n                       when String then default.split(',').map! do |item|\n                         if idx = item.to_i?\n                           item = idx\n                         end\n\n                         choices[item]? || item.to_s\n                       end\n                       else\n                         [default]\n                       end\n\n             %( <info>#{text}</info> [<comment>#{ACON::Formatter::Output.escape default.join(\", \")}</comment>]:)\n           elsif question.is_a? ACON::Question::Choice\n             choices = question.choices\n\n             \" <info>#{text}</info> [<comment>#{ACON::Formatter::Output.escape default.to_s}</comment>]:\"\n           else\n             \" <info>#{text}</info> [<comment>#{ACON::Formatter::Output.escape default.to_s}</comment>]:\"\n           end\n\n    output.puts text\n\n    prompt = \" > \"\n\n    if question.is_a? ACON::Question::AbstractChoice\n      output.puts self.format_choice_question_choices question, \"comment\"\n\n      prompt = question.prompt\n    end\n\n    output.print prompt\n  end\n\n  private def eof_shortcut : String\n    # TODO: Windows uses Ctrl+Z + Enter\n\n    \"<comment>Ctrl+D</comment>\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/descriptor_helper.cr",
    "content": "# :nodoc:\nclass Athena::Console::Helper::Descriptor < Athena::Console::Helper\n  @descriptors = Hash(String, ACON::Descriptor::Interface).new\n\n  def initialize\n    self.register \"txt\", ACON::Descriptor::Text.new\n  end\n\n  def describe(output : ACON::Output::Interface, object : _, context : ACON::Descriptor::Context) : Nil\n    raise ACON::Exception::InvalidArgument.new \"Unsupported format #{context.format}.\" unless descriptor = @descriptors[context.format]?\n\n    descriptor.describe output, object, context\n  end\n\n  def register(format : String, descriptor : ACON::Descriptor::Interface) : self\n    @descriptors[format] = descriptor\n\n    self\n  end\n\n  def formats : Array(String)\n    @descriptors.keys\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/formatter.cr",
    "content": "# Provides additional ways to format output messages than `ACON::Formatter::OutputStyle` can do alone, such as:\n#\n# * Printing messages in a section\n# * Printing messages in a block\n# * Print truncated messages.\n#\n# The provided methods return a `String` which could then be passed to `ACON::Output::Interface#print` or `ACON::Output::Interface#puts`.\nclass Athena::Console::Helper::Formatter < Athena::Console::Helper\n  # Prints the provided *message* in the provided *section*.\n  # Optionally allows setting the *style* of the section.\n  #\n  # ```text\n  # [SomeSection] Here is some message related to that section\n  # ```\n  #\n  # ```\n  # output.puts formatter.format_section \"SomeSection\", \"Here is some message related to that section\"\n  # ```\n  def format_section(section : String, message : String, style : String = \"info\") : String\n    \"<#{style}>[#{section}]</#{style}> #{message}\"\n  end\n\n  # Prints the provided *messages* in a block formatted according to the provided *style*, with a total width a bit more than the longest line.\n  #\n  # The *large* options adds additional padding, one blank line above and below the messages, and 2 more spaces on the left and right.\n  #\n  # ```\n  # output.puts formatter.format_block({\"Error!\", \"Something went wrong\"}, \"error\", true)\n  # ```\n  def format_block(messages : String | Enumerable(String), style : String, large : Bool = false)\n    messages = messages.is_a?(String) ? {messages} : messages\n\n    len = 0\n    lines = [] of String\n\n    messages.each do |message|\n      message = ACON::Formatter::Output.escape message\n      lines << (large ? \"  #{message}  \" : \" #{message} \")\n      len = Math.max (message.size + (large ? 4 : 2)), len\n    end\n\n    messages = large ? [\" \" * len] : [] of String\n\n    lines.each do |line|\n      messages << %(#{line}#{\" \" * (len - line.delete('\\\\').size)})\n    end\n\n    if large\n      messages << \" \" * len\n    end\n\n    messages.each_with_index do |line, idx|\n      messages[idx] = \"<#{style}>#{line}</#{style}>\"\n    end\n\n    messages.join '\\n'\n  end\n\n  # Truncates the provided *message* to be at most *length* characters long,\n  # with the optional *suffix* appended to the end.\n  #\n  # ```\n  # message = \"This is a very long message, which should be truncated\"\n  # truncated_message = formatter.truncate message, 7\n  # output.puts truncated_message # => This is...\n  # ```\n  #\n  # If *length* is negative, it will start truncating from the end.\n  #\n  # ```\n  # message = \"This is a very long message, which should be truncated\"\n  # truncated_message = formatter.truncate message, -4\n  # output.puts truncated_message # => This is a very long message, which should be trunc...\n  # ```\n  def truncate(message : String, length : Int, suffix : String = \"...\") : String\n    computed_length = length - self.class.width suffix\n\n    if computed_length > self.class.width message\n      return message\n    end\n\n    \"#{message[0...length]}#{suffix}\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/helper.cr",
    "content": "require \"./interface\"\n\n# Contains `ACON::Helper::Interface` implementations that can be used to help with various tasks.\n# Such as asking questions, customizing the output format, or generating tables.\n#\n# This class also acts as a base type that implements common functionality between each helper.\nabstract class Athena::Console::Helper\n  include Athena::Console::Helper::Interface\n\n  private TIME_FORMATS = {\n    {0, \"< 1 sec\", nil},\n    {1, \"1 sec\", nil},\n    {2, \"secs\", 1},\n    {60, \"1 min\", nil},\n    {120, \"mins\", 60},\n    {3_600, \"1 hr\", nil},\n    {7_200, \"hrs\", 3_600},\n    {86_400, \"1 day\", nil},\n    {172_800, \"days\", 86_400},\n  }\n\n  # Formats the provided *span* of time as a human readable string.\n  #\n  # ```\n  # ACON::Helper.format_time 10.seconds # => \"10 secs\"\n  # ACON::Helper.format_time 4.minutes  # => \"4 mins\"\n  # ACON::Helper.format_time 74.minutes # => \"1 hr\"\n  # ```\n  def self.format_time(span : Time::Span) : String\n    self.format_time span.total_seconds\n  end\n\n  # Formats the provided *seconds* as a human readable string.\n  #\n  # ```\n  # ACON::Helper.format_time 10   # => \"10 secs\"\n  # ACON::Helper.format_time 240  # => \"4 mins\"\n  # ACON::Helper.format_time 4400 # => \"1 hr\"\n  # ```\n  def self.format_time(seconds : Number) : String\n    TIME_FORMATS.each_with_index do |format, idx|\n      min_seconds, label, max_seconds = format\n\n      next unless seconds >= min_seconds\n\n      if ((next_format = TIME_FORMATS[idx + 1]?) && (seconds < next_format[0])) || idx == TIME_FORMATS.size - 1\n        return label if max_seconds.nil?\n\n        return \"#{(seconds // max_seconds).to_i} #{label}\"\n      end\n    end\n\n    raise \"BUG: Unable to format time: #{seconds}.\"\n  end\n\n  # Returns a new string with all of its ANSI formatting removed.\n  def self.remove_decoration(formatter : ACON::Formatter::Interface, string : String) : String\n    is_decorated = formatter.decorated?\n    formatter.decorated = false\n\n    # Remove <...> formatting\n    string = formatter.format string\n\n    # Remove already formatted characters\n    string = string.gsub /\\033\\[[^m]*m/, \"\"\n\n    # Remove terminal hyperlinks\n    string = string.gsub /\\033]8;[^;]*;[^\\033]*\\033\\\\/, \"\"\n\n    formatter.decorated = is_decorated\n\n    string\n  end\n\n  # Returns the width of a string; where the width is how many character positions the string will use.\n  #\n  # TODO: Support double width chars.\n  def self.width(string : String) : Int32\n    string.size\n  end\n\n  property helper_set : ACON::Helper::HelperSet? = nil\nend\n"
  },
  {
    "path": "src/components/console/src/helper/helper_set.cr",
    "content": "# The container that stores various `ACON::Helper::Interface` implementations, keyed by their class.\n#\n# Each application includes a default helper set, but additional ones may be added.\n# See `ACON::Application#helper_set`.\n#\n# These helpers can be accessed from within a command via the `ACON::Command#helper` method.\nclass Athena::Console::Helper::HelperSet\n  @helpers = Hash(ACON::Helper.class, ACON::Helper::Interface).new\n\n  def self.new(*helpers : ACON::Helper::Interface) : self\n    helper_set = new\n    helpers.each do |helper|\n      helper_set << helper\n    end\n    helper_set\n  end\n\n  def initialize(@helpers : Hash(ACON::Helper.class, ACON::Helper::Interface) = Hash(ACON::Helper.class, ACON::Helper::Interface).new); end\n\n  # Adds the provided *helper* to `self`.\n  def <<(helper : ACON::Helper::Interface) : Nil\n    @helpers[helper.class] = helper\n\n    helper.helper_set = self\n  end\n\n  # Returns `true` if `self` has a helper for the provided *helper_class*, otherwise `false`.\n  def has?(helper_class : ACON::Helper.class) : Bool\n    @helpers.has_key? helper_class\n  end\n\n  # Returns the helper of the provided *helper_class*, or `nil` if it is not defined.\n  def []?(helper_class : T.class) : T? forall T\n    {%\n      unless T <= ACON::Helper::Interface\n        T.raise \"Helper class type '#{T}' is not an 'ACON::Helper::Interface'.\"\n      end\n    %}\n\n    @helpers[helper_class]?.as? T\n  end\n\n  # Returns the helper of the provided *helper_class*, or raises if it is not defined.\n  def [](helper_class : T.class) : T forall T\n    self.[helper_class]? || raise ACON::Exception::InvalidArgument.new \"The helper '#{helper_class}' is not defined.\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/interface.cr",
    "content": "module Athena::Console::Helper::Interface\n  # Sets the `ACON::Helper::HelperSet` related to `self`.\n  abstract def helper_set=(helper_set : ACON::Helper::HelperSet?)\n\n  # Returns the `ACON::Helper::HelperSet` related to `self`, if any.\n  abstract def helper_set : ACON::Helper::HelperSet?\nend\n"
  },
  {
    "path": "src/components/console/src/helper/output_wrapper.cr",
    "content": "# :nodoc:\n#\n# Adapted from https://github.com/symfony/symfony/blob/fbf6f56ca7321e28d9a4368e18b9da683c296046/src/Symfony/Component/Console/Helper/OutputWrapper.php\nstruct Athena::Console::Helper::OutputWrapper\n  def initialize(@allow_cut_urls : Bool = false); end\n\n  def wrap(text : String, width : Int32, separator : String = \"\\n\") : String\n    return text if width.zero?\n\n    row_pattern = if @allow_cut_urls\n                    %r((?:<(?:(?:[a-z](?:[^\\\\<>]*+ | \\\\.)*)|/(?:[a-z][^<>]*+)?)>|.){1,#{width}})\n                  else\n                    %r((?:<(?:(?:[a-z](?:[^\\\\<>]*+ | \\\\.)*)|/(?:[a-z][^<>]*+)?)>|.|https?://\\S+){1,#{width}})\n                  end\n\n    pattern = %r((?:((?>(#{row_pattern.source})((?<=[^\\S\\r\\n])[^\\S\\r\\n]?|(?=\\r?\\n)|$|[^\\S\\r\\n]))|(#{row_pattern.source}))(?:\\r?\\n)?|(?:\\r?\\n|$)))imx\n\n    text\n      .gsub(pattern, \"\\\\0#{separator}\")\n      .rstrip(separator)\n      .gsub \" #{separator}\", separator\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/progress_bar.cr",
    "content": "abstract class Athena::Console::Output; end\n\nrequire \"../output/interface\"\n\n# When executing longer-running commands, it can be helpful to show progress information that updates as the command runs:\n#\n# ![Progress Bar](/img/progress_bar.gif)\n#\n# TIP: Consider using `ACON::Style::Athena` to display a progress bar.\n#\n# The ProgressBar helper can be used to progress information to any `ACON::Output::Interface`:\n#\n# ```\n# # Create a new progress bar with 50 required units for completion.\n# progress_bar = ACON::Helper::ProgressBar.new output, 50\n#\n# # Start and display the progress bar.\n# progress_bar.start\n#\n# 50.times do\n#   # Do work\n#\n#   # Advance the progress bar by 1 unit.\n#   progress_bar.advance\n#\n#   # Or advance by more than a single unit.\n#   # progress_bar.advance 3\n# end\n#\n# # Ensure progress bar is at 100%.\n# progress_bar.finish\n# ```\n#\n# A progress bar can also be created without a required number of units, in which case it will just act as a [throbber](https://en.wikipedia.org/wiki/Throbber).\n# However, `#max_steps=` can be called at any point to either set, or increase the required number of units.\n# E.g. if its only known after performing some calculations, or additional work is needed such that the original value is not invalid.\n#\n# TIP: Consider using an `ACON::Helper::ProgressIndicator` instead of a progress bar for this use case.\n#\n# Be sure to call `#finish` when the task completes to ensure the progress bar is refreshed with a 100% completion.\n#\n# NOTE: By default the progress bar will write its output to `STDERR`, however this can be customized by using an `ACON::Output::IO` explicitly.\n#\n# If the progress information is stored within an [Enumerable](https://crystal-lang.org/api/Enumerable.html) type, the `#iterate` method\n# can be used to start, advance, and finish the progress bar automatically, yielding each item in the collection:\n#\n# ```\n# bar = ACON::Helper::ProgressBar.new output\n# arr = [1, 2, 3]\n#\n# bar.iterate(arr) do |item|\n#   # Do something\n# end\n# ```\n#\n# Which would output:\n# ```text\n# 0/2 [>---------------------------]   0%\n# 1/2 [==============>-------------]  50%\n# 2/2 [============================] 100%\n# ```\n#\n# NOTE: `Iterator` types are also supported, but need the max value provided explicitly via the second argument to `#iterate` if known.\n#\n# ### Progressing\n#\n# While the `#advance` method can be used to move the progress bar ahead by a specific number of steps,\n# the current step can be set explicitly via `#progress=`.\n#\n# It is also possible to start the progress bar at a specific step, which is useful when resuming some long-standing task:\n#\n# ```\n# # Create a 100 unit progress bar.\n# progress_bar = ACON::Helper::ProgressBar.new output, 100\n#\n# # Display the progress bar starting at already 25% complete.\n# progress_bar.start at: 25\n# ```\n#\n# TIP: The progress can also be regressed (stepped backwards) by providing `#advance` a negative value.\n#\n# ### Controlling Rendering\n#\n# If available, [ANCI Escape Codes](https://en.wikipedia.org/wiki/ANSI_escape_code) are used to handle the rendering of the progress bar,\n# otherwise updates are added as new lines. `#minimum_seconds_between_redraws=` can be used to prevent the output being flooded.\n# `#redraw_frequency=` can be used to to redraw every _N_ iterations. By default, redraw frequency is **100ms** or **10%** of your `#max_steps`.\n#\n# ## Customizing\n#\n# ### Built-in Formats\n#\n# The progress bar comes with a few built-in formats based on the `ACON::Output::Verbosity` the command was executed with:\n#\n# ```text\n# # Verbosity::NORMAL (CLI with no verbosity flag)\n#  0/3 [>---------------------------]   0%\n#  1/3 [=========>------------------]  33%\n#  3/3 [============================] 100%\n#\n# # Verbosity::VERBOSE (-v)\n#  0/3 [>---------------------------]   0%  1 sec\n#  1/3 [=========>------------------]  33%  1 sec\n#  3/3 [============================] 100%  1 sec\n#\n# # Verbosity::VERY_VERBOSE (-vv)\n#  0/3 [>---------------------------]   0%  1 sec/1 sec\n#  1/3 [=========>------------------]  33%  1 sec/1 sec\n#  3/3 [============================] 100%  1 sec/1 sec\n#\n# # Verbosity::DEBUG (-vvv)\n#  0/3 [>---------------------------]   0%  1 sec/1 sec  1kiB\n#  1/3 [=========>------------------]  33%  1 sec/1 sec  1kiB\n#  3/3 [============================] 100%  1 sec/1 sec  1kiB\n# ```\n#\n# NOTE: If a command called with `ACON::Output::Verbosity::QUIET`, the progress bar will not be displayed.\n#\n# The format may also be set explicitly in code via:\n#\n# ```\n# # If the progress bar has a maximum number of steps.\n# bar.format = :very_verbose\n#\n# # Without a maximum\n# bar.format = :very_verbose_nomax\n# ```\n#\n# ### Custom Formats\n#\n# While the built-in formats are sufficient for most use cases, custom ones may also be defined:\n#\n# ```\n# bar.format = \"%bar%\"\n# ```\n#\n# Which would set the format to only display the progress bar itself:\n#\n# ```text\n# >---------------------------\n# =========>------------------\n# ============================\n# ```\n#\n# A progress bar format is a string that contains specific placeholders (a name enclosed with the `%` character);\n# the placeholders are replaced based on the current progress of the bar. The built-in placeholders include:\n#\n# * `%current%` - The current step\n# * `%max%` - The maximum number of steps (or zero if there is not one)\n# * `%bar%` - The progress bar itself\n# * `%percent%` - The percentage of completion (not available if no max is defined)\n# * `%elapsed%` - The time elapsed since the start of the progress bar\n# * `%remaining%` - The remaining time to complete the task (not available if no max is defined)\n# * `%estimated%` - The estimated time to complete the task (not available if no max is defined)\n# * `%memory%` - The current memory usage\n# * `%message%` - Used to display arbitrary messages, more on this later\n#\n# For example, the format string for `ACON::Helper::ProgressBar::Format::NORMAL` is `\" %current% [%bar%] %elapsed:6s%\"`.\n# Individual placeholders can have their formatting tweaked by anything that [sprintf](https://crystal-lang.org/api/toplevel.html#sprintf(format_string,args:Array|Tuple):String-class-method) supports\n# by separating the name of the placeholder with a `:`.\n# The part after the colon will be passed to `sprintf`.\n#\n# If a format should be used across an entire application, they can be registered globally via `.set_format_definition`:\n#\n# ```\n# ACON::Helper::ProgressBar.set_format_definition \"minimal\", \"Progress: %percent%%\"\n#\n# bar = ACON::Helper::ProgressBar.new output, 3\n# bar.format = \"minimal\"\n# ```\n#\n# Which would output:\n#\n# ```text\n# Progress: 0%\n# Progress: 33%\n# Progress: 100%\n# ```\n#\n# TIP: It is almost always better to override the built-in formats in order to automatically vary the display based on the verbosity the command is being ran with.\n#\n# When creating a custom format, be sure to also define a `_nomax` variant if it is using a placeholder that is only available if `#max_steps` is defined.\n#\n# ```\n# ACON::Helper::ProgressBar.set_format_definition \"minimal\", \"%current%/%remaining%\"\n# ACON::Helper::ProgressBar.set_format_definition \"minimal_nomax\", \"%current%\"\n#\n# bar = ACON::Helper::ProgressBar.new output, 3\n# bar.format = \"minimal\"\n# ```\n#\n# The format will automatically be set to `minimal_nomax` if the bar does not have a maximum number of steps.\n#\n# TIP: A format can contain any valid ANSI codes, or any `ACON::Formatter::OutputStyleInterface` markup.\n#\n# TIP: A format may also span multiple lines, which can be useful to also display contextual information (like the first example).\n#\n# ### Bar Settings\n#\n# The `bar` placeholder is a bit special in that all of the characters used to display it can be customized:\n#\n# ```\n# # The Finished part of the bar.\n# bar.bar_character = \"<comment>=</comment>\"\n#\n# # The unfinished part of the bar.\n# bar.empty_bar_character = \" \"\n#\n# # The progress character.\n# bar.progress_character = \"|\"\n#\n# # The width of the bar.\n# bar.bar_width = 50\n# ```\n#\n# ### Custom Placeholders\n#\n# Just like the format, custom placeholders may also be defined.\n# This can be useful to have a common way of displaying some sort of application specific information between multiple progress bars:\n#\n# ```\n# ACON::Helper::ProgressBar.set_placeholder_formatter \"remaining_steps\" do |bar|\n#   \"#{bar.max_steps - bar.progress}\"\n# end\n# ```\n#\n# From here it could then be used in a format string as `%remaining_steps%` just like any other placeholder.\n# `.set_placeholder_formatter` registers the format globally, while `#set_placeholder_formatter` would set it on a specific progress bar.\n#\n# ### Custom Messages\n#\n# While there is a built-in `message` placeholder that can be set via `#set_message`, none of the built-in formats include it.\n# As such, before displaying these messages, a custom format needs to be defined:\n#\n# ```\n# bar = ACON::Helper::ProgressBar.new output, 100\n# bar.format = \" %current%/%max% -- %message%\"\n#\n# bar.set_message \"Start\"\n# bar.start # 0/100 -- Start\n#\n# bar.set_message \"Task is in progress...\"\n# bar.advance # 1/100 -- Task is in progress...\n# ```\n#\n# `#set_message` also allows or an optional second argument, which can be used to have multiple independent messages within the same format string:\n#\n# ```\n# files.each do |file_name|\n#   bar.set_message \"Importing files...\"\n#   bar.set_message file_name, \"filename\"\n#   bar.advance # => 2/100 -- Importing files... (foo/bar.txt)\n# end\n# ```\n#\n# ## Multiple Progress Bars\n#\n# When using `ACON::Output::Section`s, multiple progress bars can be displayed at the same time and updated independently:\n#\n# ```\n# output = output.as ACON::Output::ConsoleOutputInterface\n#\n# section1 = output.section\n# section2 = output.section\n#\n# bar1 = ACON::Helper::ProgressBar.new section1\n# bar2 = ACON::Helper::ProgressBar.new section2\n#\n# bar1.start 100\n# bar2.start 100\n#\n# 100.times do |idx|\n#   bar1.advance\n#   bar2.advance(4) if idx.divisible_by? 2\n#\n#   sleep 0.05.seconds\n# end\n# ```\n#\n# Which would ultimately look something like:\n#\n# ```text\n# 34/100 [=========>------------------]  34%\n# 68/100 [===================>--------]  68%\n# ```\nclass Athena::Console::Helper::ProgressBar\n  # Represents the built in progress bar formats.\n  #\n  # See [Built-In Formats][Athena::Console::Helper::ProgressBar--built-in-formats] for more information.\n  enum Format\n    # `\" %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%\"`\n    DEBUG\n\n    # `\" %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%\"`\n    VERY_VERBOSE\n\n    # `\" %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%\"`\n    VERBOSE\n\n    # `\" %current%/%max% [%bar%] %percent:3s%%\"`\n    NORMAL\n\n    # `\" %current% [%bar%] %elapsed:6s% %memory:6s%\"`\n    DEBUG_NOMAX\n\n    # `\" %current% [%bar%] %elapsed:6s%\"`\n    VERBOSE_NOMAX\n\n    # `\" %current% [%bar%] %elapsed:6s%\"`\n    VERY_VERBOSE_NOMAX\n\n    # `\" %current% [%bar%]\"`\n    NORMAL_NOMAX\n  end\n\n  # Represents the expected type of a [Placeholder Formatter][Athena::Console::Helper::ProgressBar--custom-placeholders].\n  alias PlaceholderFormatter = Proc(Athena::Console::Helper::ProgressBar, Athena::Console::Output::Interface, String)\n\n  # INTERNAL\n  protected class_getter formats : Hash(String, String) { self.init_formats }\n\n  # INTERNAL\n  protected class_getter placeholder_formatters : Hash(String, PlaceholderFormatter) { self.init_placeholder_formatters }\n\n  # Registers the *format* globally with the provided *name*.\n  def self.set_format_definition(name : String, format : String) : Nil\n    self.formats[name] = format\n  end\n\n  # Returns the global format string for the provided *name* if it exists, otherwise `nil`.\n  def self.format_definition(name : String) : String?\n    self.formats[name]?\n  end\n\n  private def self.init_formats : Hash(String, String)\n    {\n      \"debug\"       => \" %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%\",\n      \"debug_nomax\" => \" %current% [%bar%] %elapsed:6s% %memory:6s%\",\n\n      \"very_verbose\"       => \" %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%\",\n      \"very_verbose_nomax\" => \" %current% [%bar%] %elapsed:6s%\",\n\n      \"verbose\"       => \" %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%\",\n      \"verbose_nomax\" => \" %current% [%bar%] %elapsed:6s%\",\n\n      \"normal\"       => \" %current%/%max% [%bar%] %percent:3s%%\",\n      \"normal_nomax\" => \" %current% [%bar%]\",\n    }\n  end\n\n  # Registers a custom placeholder with the provided *name* with the block being the formatter.\n  def self.set_placeholder_formatter(name : String, &block : self, ACON::Output::Interface -> String) : Nil\n    self.set_placeholder_formatter name, block\n  end\n\n  # Registers a custom placeholder with the provided *name*, using the provided *callable* as the formatter.\n  def self.set_placeholder_formatter(name : String, callable : ACON::Helper::ProgressBar::PlaceholderFormatter) : Nil\n    self.placeholder_formatters[name] = callable\n  end\n\n  # Returns the global formatter for the provided *name* if it exists, otherwise `nil`.\n  def self.placeholder_formatter(name : String) : ACON::Helper::ProgressBar::PlaceholderFormatter?\n    self.placeholder_formatters[name]?\n  end\n\n  private def self.init_placeholder_formatters : Hash(String, PlaceholderFormatter)\n    {\n      \"bar\" => PlaceholderFormatter.new do |bar, output|\n        completed_bars = bar.bar_offset\n\n        display = bar.bar_character * completed_bars\n\n        if completed_bars < bar.bar_width\n          empty_bars = bar.bar_width - completed_bars - ACON::Helper.width(ACON::Helper.remove_decoration(output.formatter, bar.progress_character))\n\n          display += \"#{bar.progress_character}#{bar.empty_bar_character * empty_bars}\"\n        end\n\n        display\n      end,\n\n      \"remaining\" => PlaceholderFormatter.new do |bar, _|\n        if bar.max_steps.zero?\n          raise ACON::Exception::Logic.new \"Unable to display the remaining time if the maximum number of steps is not set.\"\n        end\n\n        ACON::Helper.format_time bar.remaining\n      end,\n      \"estimated\" => PlaceholderFormatter.new do |bar, _|\n        if bar.max_steps.zero?\n          raise ACON::Exception::Logic.new \"Unable to display the remaining time if the maximum number of steps is not set.\"\n        end\n\n        ACON::Helper.format_time bar.estimated\n      end,\n\n      \"memory\"  => PlaceholderFormatter.new { |_| (GC.stats.heap_size - GC.stats.free_bytes).humanize_bytes },\n      \"elapsed\" => PlaceholderFormatter.new { |bar| ACON::Helper.format_time bar.clock.now - bar.start_time },\n      \"current\" => PlaceholderFormatter.new { |bar| bar.progress.to_s.rjust bar.step_width, ' ' },\n      \"max\"     => PlaceholderFormatter.new(&.max_steps.to_s),\n      \"percent\" => PlaceholderFormatter.new { |bar| (bar.progress_percent * 100).floor.to_i.to_s },\n    }\n  end\n\n  @output : ACON::Output::Interface\n  @terminal : ACON::Terminal\n  @cursor : ACON::Cursor\n\n  @max : Int32 = 0\n  @redraw_frequency : Int32? = 1\n  @format : String? = nil\n  @internal_format : String? = nil\n  @step : Int32 = 0\n  @starting_step : Int32 = 0\n  @percent : Float64 = 0.0\n  @last_write_time : Time = Time::UNIX_EPOCH\n  @previous_message : String? = nil\n  @write_count : Int32 = 0\n  @messages : Hash(String, String) = Hash(String, String).new\n  @placeholder_formatters : Hash(String, PlaceholderFormatter) = Hash(String, PlaceholderFormatter).new\n\n  protected getter clock : ACLK::Interface\n\n  # Returns the time the progress bar was started as a Unix epoch.\n  getter start_time : Time\n\n  # Returns the width of the progress bar in pixels.\n  #\n  # ```\n  # bar1 = ...\n  # bar1.bar_width = 50\n  # bar1.start 10\n  #\n  # bar2 = ...\n  # bar2.bar_width = 10\n  # bar2.start 20\n  #\n  # bar1.finish\n  # bar2.finish\n  # ```\n  #\n  # ```\n  # 10/10 [==================================================] 100%\n  # 20/20 [==========] 100%\n  # ```\n  getter bar_width : Int32 = 28\n\n  # Explicitly sets the character to use for the finished part of the bar.\n  setter bar_character : String? = nil\n\n  # Represents the character used for the unfinished part of the bar.\n  property empty_bar_character : String = \"-\"\n\n  # Represents the character used for the current progress of the bar.\n  property progress_character : String = \">\"\n\n  # Sets if the progress bar should overwrite the progress bar.\n  # Set to `false` in order to print the progress bar on a new line for each update.\n  setter overwrite : Bool = true\n\n  # Returns the width in pixels that the current `#progress` takes up when displayed.\n  getter! step_width : Int32\n\n  # Sets the minimum amount of time between redraws.\n  #\n  # See [Controlling Rendering][Athena::Console::Helper::ProgressBar--controlling-rendering] for more information.\n  setter minimum_seconds_between_redraws : Float64 = 0\n\n  # Sets the maximum amount of time between redraws.\n  #\n  # See [Controlling Rendering][Athena::Console::Helper::ProgressBar--controlling-rendering] for more information.\n  setter maximum_seconds_between_redraws : Float64 = 1\n\n  def initialize(\n    output : ACON::Output::Interface,\n    max : Int32? = nil,\n    minimum_seconds_between_redraws : Float64 = 0.04,\n    @clock : ACLK::Interface = ACLK::Native.new,\n  )\n    if output.is_a? ACON::Output::ConsoleOutputInterface\n      output = output.error_output\n    end\n\n    @output = output\n    @terminal = ACON::Terminal.new\n\n    if 0 < minimum_seconds_between_redraws\n      @redraw_frequency = nil\n      @minimum_seconds_between_redraws = minimum_seconds_between_redraws\n    end\n\n    unless @output.decorated?\n      # Disable overwrite when output does not support ANSI codes.\n      @overwrite = false\n\n      # Set a reasonable redraw freq so output isn't flooded\n      @redraw_frequency = nil\n    end\n\n    @start_time = @clock.now\n    @cursor = ACON::Cursor.new @output\n\n    self.max_steps = max || 0\n  end\n\n  # Sets what built in *format* to use.\n  # See [Built-in Formats][Athena::Console::Helper::ProgressBar--built-in-formats] for more information.\n  def format=(format : ACON::Helper::ProgressBar::Format)\n    self.format = format.to_s.downcase\n  end\n\n  # Sets the format string used to determine how to display the progress bar.\n  # See [Custom Formats][Athena::Console::Helper::ProgressBar--custom-formats] for more information.\n  def format=(format : String)\n    @format = nil\n    @internal_format = format\n  end\n\n  # Returns the current step of the progress bar\n  def progress : Int32\n    @step\n  end\n\n  # Returns the maximum number of possible steps, or `0` if it is unknown.\n  def max_steps : Int32\n    @max\n  end\n\n  # Sets the maximum possible steps to the provided *max*.\n  def max_steps=(max : Int32) : Nil\n    @format = nil\n    @max = Math.max 0, max\n    @step_width = @max > 0 ? ACON::Helper.width(@max.to_s) : 4\n  end\n\n  # Returns the a percent of progress of `#progress` versus `#max_steps`.\n  # Returns zero if there is no max defined.\n  def progress_percent : Float64\n    @percent\n  end\n\n  # Returns the character to use for the finished part of the bar.\n  def bar_character : String\n    @bar_character || (@max > 0 ? \"=\" : @empty_bar_character)\n  end\n\n  # Sets the width of the bar in pixels to the provided *size*.\n  # See `#bar_width`.\n  def bar_width=(size : Int32) : Nil\n    @bar_width = Math.max 1, size\n  end\n\n  # Returns the amount of `#bar_character` representing the current `#progress`.\n  def bar_offset : Int32\n    if @max > 0\n      return (@percent * @bar_width).floor.to_i\n    end\n\n    if @redraw_frequency.nil?\n      return ((Math.min(5, bar_width / 15) * @write_count) % @bar_width).floor.to_i\n    end\n\n    (@step % @bar_width).floor.to_i\n  end\n\n  # Returns an estimated amount of time in seconds until the progress bar is completed.\n  def estimated : Float64\n    return 0.0 if @step.zero? || @step == @starting_step\n\n    ((@clock.now - @start_time).total_seconds / (@step - @starting_step) * @max).round\n  end\n\n  # Returns an estimated total amount of time in seconds needed for the progress bar to complete.\n  def remaining : Float64\n    return 0.0 if @step.zero?\n\n    ((@clock.now - @start_time).total_seconds / (@step - @starting_step) * (@max - @step)).round 0\n  end\n\n  # Returns the amount of time in seconds until the progress bar is completed.\n  def placeholder_formatter(name : String) : ACON::Helper::ProgressBar::PlaceholderFormatter?\n    @placeholder_formatters[name]? || self.class.placeholder_formatter name\n  end\n\n  # Same as `.set_placeholder_formatter`, but scoped to this particular progress bar.\n  def set_placeholder_formatter(name : String, &block : self, ACON::Output::Interface -> String) : Nil\n    self.set_placeholder_formatter name, block\n  end\n\n  # Same as `.set_placeholder_formatter`, but scoped to this particular progress bar.\n  def set_placeholder_formatter(name : String, callable : ACON::Helper::ProgressBar::PlaceholderFormatter) : Nil\n    @placeholder_formatters[name] = callable\n  end\n\n  # Sets the message with the provided *name* to that of the provided *message*.\n  def set_message(message : String, name : String = \"message\") : Nil\n    @messages[name] = message\n  end\n\n  # Returns the message associated with the provided *name* if defined, otherwise `nil`.\n  def message(name : String = \"message\") : String?\n    @messages[name]?\n  end\n\n  # Redraw the progress bar every after advancing the provided amount of *steps*.\n  #\n  #  See [Controlling Rendering][Athena::Console::Helper::ProgressBar--controlling-rendering] for more information.\n  def redraw_frequency=(steps : Int32?) : Nil\n    @redraw_frequency = steps.try { |s| Math.max 1, s }\n  end\n\n  # Clears the progress bar from the output.\n  # Can be used in conjunction with `#display` to allow outputting something while a progress bar is running.\n  # Call `#clear`, write the content, then call `#display` to show the progress bar again.\n  #\n  # NOTE: Requires that `#overwrite=` be set to `true`.\n  def clear : Nil\n    return unless @overwrite\n\n    if @format.nil?\n      self.set_real_format @internal_format || self.determine_best_format.to_s.downcase\n    end\n\n    self.overwrite \"\"\n  end\n\n  # Starts the progress bar.\n  #\n  # Optionally sets the maximum number of steps to *max*, or `nil` to leave unchanged.\n  # Optionally starts the progress bar *at* the provided step.\n  def start(max : Int32? = nil, at start_at : Int32 = 0) : Nil\n    @start_time = @clock.now\n    @step = start_at\n    @starting_step = start_at\n\n    if start_at > 0\n      self.progress = start_at\n    else\n      @percent = 0.0\n    end\n\n    unless max.nil?\n      self.max_steps = max\n    end\n\n    self.display\n  end\n\n  # Advanced the progress bar *by* the provided number of steps.\n  def advance(by step : Int32 = 1) : Nil\n    self.progress = @step + step\n  end\n\n  # Explicitly sets the current step number of the progress bar.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  def progress=(step : Int32) : Nil\n    if @max > 0 && (step > @max)\n      @max = step\n    elsif step < 0\n      step = 0\n    end\n\n    redraw_frequency = @redraw_frequency || ((@max > 0 ? @max : 10) / 10)\n    previous_period = @step // redraw_frequency\n    current_period = step // redraw_frequency\n    @step = step\n\n    @percent = @max > 0 ? @step / @max : 0.0\n    time_interval = @clock.now - @last_write_time\n\n    # Draw regardless of other limits\n    if @max == step\n      self.display\n\n      return\n    end\n\n    # Throttling\n    if time_interval.total_seconds < @minimum_seconds_between_redraws\n      return\n    end\n\n    # Draw each step period, but not too late\n    if previous_period != current_period || time_interval.total_seconds >= @maximum_seconds_between_redraws\n      self.display\n    end\n  end\n\n  # Displays the progress bar's current state.\n  def display : Nil\n    return if @output.verbosity.quiet?\n\n    if @format.nil?\n      self.set_real_format @internal_format || self.determine_best_format.to_s.downcase\n    end\n\n    self.overwrite self.build_line\n  end\n\n  # Finishes the progress output, making it 100% complete.\n  def finish\n    if @max.zero?\n      @max = @step\n    end\n\n    if @step == @max && !@overwrite\n      # Prevent double 100% output\n      return\n    end\n\n    self.progress = @max\n  end\n\n  # Start, advance, and finish the progress bar automatically, yielding each item in the provided *enumerable*.\n  #\n  # ```\n  # bar = ACON::Helper::ProgressBar.new output\n  # arr = [1, 2, 3]\n  #\n  # bar.iterate(arr) do |item|\n  #   # Do something\n  # end\n  # ```\n  #\n  # Which would output:\n  # ```\n  # 0/2 [>---------------------------]   0%\n  # 1/2 [==============>-------------]  50%\n  # 2/2 [============================] 100%\n  # ```\n  #\n  # NOTE: `Iterator` types are also supported, but need the max value provided explicitly via the second argument to `#iterate` if known.\n  def iterate(enumerable : Enumerable(T), max : Int32? = nil, & : T -> Nil) : Nil forall T\n    self.start(enumerable.is_a?(Indexable) ? enumerable.size : 0)\n\n    enumerable.each do |value|\n      yield value\n\n      self.advance\n    end\n\n    self.finish\n  end\n\n  private def overwrite(message : String) : Nil\n    return if message == @previous_message\n\n    original_message = message\n\n    if @overwrite\n      if previous_message = @previous_message\n        if (output = @output).is_a? ACON::Output::Section\n          message_lines = previous_message.split '\\n' # Don't use `#lines` to retain empty values\n          line_count = message_lines.size\n\n          last_line_without_decoration = ACON::Helper.remove_decoration output.formatter, (message_lines.last? || \"\")\n\n          # When the last previous line is empty (without formatting) it is already cleared by the section output, so we don't need to clear it again\n          if last_line_without_decoration.empty?\n            line_count -= 1\n          end\n\n          message_lines.each do |line|\n            message_line_length = ACON::Helper.width ACON::Helper.remove_decoration output.formatter, line\n\n            if message_line_length > @terminal.width\n              line_count += message_line_length // @terminal.width\n            end\n          end\n\n          output.clear line_count\n        else\n          previous_message.count('\\n').times do |_|\n            @cursor.move_to_column 1\n            @cursor.clear_line\n            @cursor.move_up\n          end\n          @cursor.move_to_column 1\n          @cursor.clear_line\n        end\n      end\n    elsif @step > 0\n      message = \"#{EOL}#{message}\"\n    end\n\n    @previous_message = original_message\n    @last_write_time = @clock.now\n\n    @output.print message\n    @write_count += 1\n  end\n\n  private def build_line : String\n    format = @format.not_nil!\n\n    regex = /%([a-z\\-_]+)(?::([^%]+))?%/i\n\n    callback = Proc(String, Regex::MatchData, String).new do |_, match|\n      if formatter = self.placeholder_formatter match[1]\n        text = formatter.call self, @output\n      elsif message = @messages[match[1]]?\n        text = message\n      else\n        next match[0]\n      end\n\n      if format_string = match[2]?\n        text = sprintf \"%#{format_string}\", text\n      end\n\n      text\n    end\n\n    line = format.gsub regex, &callback\n\n    # Gets string length for each sub-line with multiline format\n    lines_length = line.split(\"\\n\").map { |sub_line| ACON::Helper.width ACON::Helper.remove_decoration @output.formatter, sub_line.rstrip \"\\r\" }\n    lines_width = lines_length.max\n\n    terminal_width = @terminal.width\n\n    if lines_width <= terminal_width\n      return line\n    end\n\n    self.bar_width = @bar_width - lines_width + terminal_width\n\n    format.gsub regex, &callback\n  end\n\n  private def set_real_format(format : String) : Nil\n    # Try to use the _NOMAX variant if available\n    @format = if @max.zero? && (resolved_format = self.class.format_definition \"#{format}_nomax\")\n                resolved_format\n              elsif resolved_format = self.class.format_definition format\n                resolved_format\n              else\n                format\n              end\n  end\n\n  private def determine_best_format : Format\n    case @output.verbosity\n    when .debug?        then @max > 0 ? Format::DEBUG : Format::DEBUG_NOMAX\n    when .very_verbose? then @max > 0 ? Format::VERY_VERBOSE : Format::VERY_VERBOSE_NOMAX\n    when .verbose?      then @max > 0 ? Format::VERBOSE : Format::VERBOSE_NOMAX\n    else\n      @max > 0 ? Format::NORMAL : Format::NORMAL_NOMAX\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/progress_indicator.cr",
    "content": "# Progress indicators are useful to let users know that a command isn't stalled.\n# However, unlike `ACON::Helper::ProgressBar`s, these indicators are used when the command's duration is indeterminate,\n# such as long-running commands or tasks that are quantifiable.\n#\n# ![Progress Indicator](/img/progress_indicator.gif)\n#\n# ```\n# # Create a new progress indicator.\n# indicator = ACON::Helper::ProgressIndicator.new output\n#\n# # Start and display the progress indicator with a custom message.\n# indicator.start \"Processing...\"\n#\n# 50.times do\n#   # Do work\n#\n#   # Advance the progress indicator.\n#   indicator.advance\n# end\n#\n# # Ensure the progress indicator shows a final completion message\n# indicator.finish \"Finished!\"\n# ```\n#\n# ## Customizing\n#\n# ### Built-in Formats\n#\n# The progress indicator comes with a few built-in formats based on the `ACON::Output::Verbosity` the command was executed with:\n#\n# ```text\n# # Verbosity::NORMAL (CLI with no verbosity flag)\n#  \\ Processing...\n#  | Processing...\n#  / Processing...\n#  - Processing...\n#\n# # Verbosity::VERBOSE (-v)\n#  \\ Processing... (1 sec)\n#  | Processing... (1 sec)\n#  / Processing... (1 sec)\n#  - Processing... (1 sec)\n#\n# # Verbosity::VERY_VERBOSE (-vv) and Verbosity::DEBUG (-vvv)\n#  \\ Processing... (1 sec, 1kiB)\n#  | Processing... (1 sec, 1kiB)\n#  / Processing... (1 sec, 1kiB)\n#  - Processing... (1 sec, 1kiB)\n# ```\n#\n# NOTE: If a command called with `ACON::Output::Verbosity::QUIET`, the progress bar will not be displayed.\n#\n# The format may also be set explicitly in code within the constructor:\n#\n# ```\n# # If the progress bar has a maximum number of steps.\n# ACON::Helper::ProgressIndicator.new output, format: :very_verbose\n# ```\n#\n# ### Custom Indicator Values\n#\n# Custom indicator values may also be used:\n#\n# ```\n# indicator = ACON::Helper::ProgressIndicator.new output, indicator_values: %w(⠏ ⠛ ⠹ ⢸ ⣰ ⣤ ⣆ ⡇)\n# ```\n#\n# The progress indicator would now look like:\n#\n# ```text\n# ⠏ Processing...\n# ⠛ Processing...\n# ⠹ Processing...\n# ⢸ Processing...\n# ```\n#\n# ### Custom Placeholders\n#\n# A progress indicator uses placeholders (a name enclosed with the `%` character) to determine the output format.\n# The built-in placeholders include:\n#\n# * `%indicator%` - The current indicator\n# * `%elapsed%` - The time elapsed since the start of the progress indicator\n# * `%memory%` - The current memory usage\n# * `%message%` - Used to display arbitrary messages\n#\n# These can be customized via `.set_placeholder_formatter`.\n#\n# ```\n# ACON::Helper::ProgressIndicator.set_placeholder_formatter \"message\" do\n#   # Return any arbitrary string\n#   \"My Custom Message\"\n# end\n# ```\n#\n# NOTE: Placeholder customization is global and would affect any indicator used after calling `.set_placeholder_formatter`.\nclass Athena::Console::Helper::ProgressIndicator\n  # Represents the built in progress indicator formats.\n  #\n  # See [Built-In Formats][Athena::Console::Helper::ProgressIndicator--built-in-formats] for more information.\n  enum Format\n    # `\" %indicator% %message%\"`\n    NORMAL\n\n    # `\" %message%\"`\n    NORMAL_NO_ANSI\n\n    # `\" %indicator% %message% (%elapsed:6s%)\"`\n    VERBOSE\n\n    # `\" %message% (%elapsed:6s%)\"`\n    VERBOSE_NO_ANSI\n\n    # `\" %indicator% %message% (%elapsed:6s%, %memory:6s%)\"`\n    VERY_VERBOSE\n\n    # `\" %message% (%elapsed:6s%, %memory:6s%)\"`\n    VERY_VERBOSE_NO_ANSI\n\n    # `\" %indicator% %message% (%elapsed:6s%, %memory:6s%)\"`\n    DEBUG\n\n    # `\" %message% (%elapsed:6s%, %memory:6s%)\"`\n    DEBUG_NO_ANSI\n\n    def format : String\n      case self\n      in .normal?                                then \" %indicator% %message%\"\n      in .normal_no_ansi?                        then \" %message%\"\n      in .verbose?                               then \" %indicator% %message% (%elapsed:6s%)\"\n      in .verbose_no_ansi?                       then \" %message% (%elapsed:6s%)\"\n      in .very_verbose?, .debug?                 then \" %indicator% %message% (%elapsed:6s%, %memory:6s%)\"\n      in .very_verbose_no_ansi?, .debug_no_ansi? then \" %message% (%elapsed:6s%, %memory:6s%)\"\n      end\n    end\n  end\n\n  # Represents the expected type of a [Placeholder Formatter][Athena::Console::Helper::ProgressIndicator--custom-placeholders].\n  alias PlaceholderFormatter = Proc(Athena::Console::Helper::ProgressIndicator, String)\n\n  # INTERNAL\n  protected class_getter placeholder_formatters : Hash(String, PlaceholderFormatter) { self.init_placeholder_formatters }\n\n  # Registers a custom placeholder with the provided *name* with the block being the formatter.\n  def self.set_placeholder_formatter(name : String, &block : self -> String) : Nil\n    self.set_placeholder_formatter name, block\n  end\n\n  # Registers a custom placeholder with the provided *name*, using the provided *callable* as the formatter.\n  def self.set_placeholder_formatter(name : String, callable : ACON::Helper::ProgressIndicator::PlaceholderFormatter) : Nil\n    self.placeholder_formatters[name] = callable\n  end\n\n  # Returns the global formatter for the provided *name* if it exists, otherwise `nil`.\n  def self.placeholder_formatter(name : String) : ACON::Helper::ProgressIndicator::PlaceholderFormatter?\n    self.placeholder_formatters[name]?\n  end\n\n  private def self.init_placeholder_formatters : Hash(String, PlaceholderFormatter)\n    {\n      \"elapsed\"   => PlaceholderFormatter.new { |indicator| ACON::Helper.format_time indicator.clock.now - indicator.start_time },\n      \"indicator\" => PlaceholderFormatter.new do |indicator|\n        indicator.finished? ? indicator.finished_indicator : indicator.indicator_values[indicator.indicator_index % indicator.indicator_values.size]\n      end,\n      \"memory\"  => PlaceholderFormatter.new { (GC.stats.heap_size - GC.stats.free_bytes).humanize_bytes },\n      \"message\" => PlaceholderFormatter.new(&.message.to_s),\n    }\n  end\n\n  protected getter indicator_values : Indexable(String)\n  protected getter indicator_index : Int32 = 0\n  protected getter start_time : Time\n\n  protected getter message : String? = nil\n  protected getter clock : ACLK::Interface\n  protected getter? finished : Bool = false\n  protected getter finished_indicator : String\n\n  @output : ACON::Output::Interface\n  @format : Format\n  @indicator_change_interval : Time::Span\n  @started : Bool = false\n  @indicator_update_time : Time = Time::UNIX_EPOCH\n\n  def initialize(\n    @output : ACON::Output::Interface,\n    format : ACON::Helper::ProgressIndicator::Format? = nil,\n    indicator_change_interval : Time::Span = 100.milliseconds,\n    indicator_values : Indexable(String)? = nil,\n    @clock : ACLK::Interface = ACLK::Native.new,\n    @finished_indicator : String = \"✔\",\n  )\n    indicator_values ||= [\"-\", \"\\\\\", \"|\", \"/\"]\n\n    if 2 > indicator_values.size\n      raise ACON::Exception::InvalidArgument.new \"Must have at least 2 indicator value characters.\"\n    end\n\n    @format = format || determine_best_format\n    @indicator_values = indicator_values\n    @start_time = @clock.now\n    @indicator_change_interval = indicator_change_interval\n  end\n\n  # Sets the *message* to display alongside the indicator.\n  def message=(@message : String?) : Nil\n    self.display\n  end\n\n  # Starts and displays the indicator with the provided *message*.\n  def start(message : String) : Nil\n    raise ACON::Exception::Logic.new \"Progress indicator is already started.\" if @started\n\n    @message = message\n    @started = true\n    @start_time = @clock.now\n    @indicator_update_time = @clock.now + @indicator_change_interval\n    @indicator_index = 0\n    @finished = false\n\n    self.display\n  end\n\n  # Advance the indicator to display the next indicator character.\n  def advance : Nil\n    raise ACON::Exception::Logic.new \"Progress indicator has not yet been started.\" unless @started\n\n    return unless @output.decorated?\n\n    current_time = @clock.now\n\n    return if current_time < @indicator_update_time\n\n    @indicator_update_time = current_time + @indicator_change_interval\n    @indicator_index += 1\n\n    self.display\n  end\n\n  # Display the current state of the indicator.\n  def display : Nil\n    return if @output.verbosity.quiet?\n\n    self.overwrite(\n      @format.format.gsub /%([a-z\\-_]+)(?:\\:([^%]+))?%/i do |_, match|\n        if formatter = self.class.placeholder_formatter match[1]\n          next formatter.call self\n        end\n\n        match[0]\n      end\n    )\n  end\n\n  # Completes the indicator with the provided *message*.\n  def finish(@message : String, finished_indicator : String? = nil) : Nil\n    raise ACON::Exception::Logic.new \"Progress indicator has not yet been started.\" unless @started\n\n    if finished_indicator\n      @finished_indicator = finished_indicator\n    end\n\n    @finished = true\n    self.display\n    @output.puts \"\"\n    @started = false\n  end\n\n  private def overwrite(message : String) : Nil\n    if @output.decorated?\n      @output.print \"\\x0D\\x1B[2K\"\n      @output.print message\n    else\n      @output.puts message\n    end\n  end\n\n  private def determine_best_format : Format\n    case {@output.verbosity, @output.decorated?}\n    when {.debug?, true}         then Format::VERY_VERBOSE\n    when {.debug?, false}        then Format::VERY_VERBOSE_NO_ANSI\n    when {.very_verbose?, true}  then Format::VERY_VERBOSE\n    when {.very_verbose?, false} then Format::VERY_VERBOSE_NO_ANSI\n    when {.verbose?, true}       then Format::VERBOSE\n    when {.verbose?, false}      then Format::VERBOSE_NO_ANSI\n    else\n      @output.decorated? ? Format::NORMAL : Format::NORMAL_NO_ANSI\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/question.cr",
    "content": "# Provides a method to ask the user for more information;\n# such as to confirm an action, or to provide additional values.\n#\n# See `ACON::Question` namespace for more information.\nclass Athena::Console::Helper::Question < Athena::Console::Helper\n  @@stty : Bool = true\n\n  def self.disable_stty : Nil\n    @@stty = false\n  end\n\n  @stream : IO? = nil\n\n  def ask(input : ACON::Input::Interface, output : ACON::Output::Interface, question : ACON::Question::Base)\n    if output.is_a? ACON::Output::ConsoleOutputInterface\n      output = output.error_output\n    end\n\n    return self.default_answer question unless input.interactive?\n\n    if input.is_a?(ACON::Input::Streamable) && (stream = input.stream)\n      @stream = stream\n    end\n\n    begin\n      if question.validator.nil?\n        return self.do_ask output, question\n      end\n\n      self.validate_attempts(output, question) do\n        self.do_ask output, question\n      end\n    rescue ex : ACON::Exception::MissingInput\n      input.interactive = false\n\n      raise ex\n    end\n  end\n\n  protected def format_choice_question_choices(question : ACON::Question::AbstractChoice, tag : String) : Array(String)\n    messages = Array(String).new\n\n    choices = question.choices\n\n    max_width = choices.keys.max_of { |k| k.is_a?(String) ? self.class.width(k) : k.digits.size }\n\n    choices.each do |k, v|\n      padding = \" \" * (max_width - (k.is_a?(String) ? k.size : k.digits.size))\n\n      messages << \"  [<#{tag}>#{k}#{padding}</#{tag}>] #{v}\"\n    end\n\n    messages\n  end\n\n  protected def write_error(output : ACON::Output::Interface, error : ::Exception) : Nil\n    message = if (helper_set = self.helper_set) && (formatter_helper = helper_set[ACON::Helper::Formatter]?)\n                formatter_helper.format_block error.message || \"\", \"error\"\n              else\n                \"<error>#{error.message}</error>\"\n              end\n\n    output.puts message\n  end\n\n  protected def write_prompt(output : ACON::Output::Interface, question : ACON::Question::Base) : Nil\n    message = question.question\n\n    if question.is_a? ACON::Question::AbstractChoice\n      output.puts question.question\n      output.puts self.format_choice_question_choices question, \"info\"\n\n      message = question.prompt\n    end\n\n    output.print message\n  end\n\n  private def default_answer(question : ACON::Question::Base)\n    default = question.default\n\n    return default if default.nil?\n\n    if validator = question.validator\n      return validator.call default\n    elsif question.is_a? ACON::Question::AbstractChoice\n      choices = question.choices\n\n      unless question.is_a? ACON::Question::MultipleChoice\n        return choices[default]? || default\n      end\n\n      default = case default\n                when String then default.split(',').map! do |item|\n                  if idx = item.to_i?\n                    item = idx\n                  end\n\n                  choices[item]? || item.to_s\n                end\n                else\n                  default\n                end\n    end\n\n    default\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def do_ask(output : ACON::Output::Interface, question : ACON::Question::Base)\n    self.write_prompt output, question\n\n    input_stream = @stream || STDIN\n    autocompleter = question.autocompleter_callback\n\n    # TODO: Handle invalid input IO\n\n    if autocompleter.nil? || !@@stty || !ACON::Terminal.has_stty_available?\n      response = nil\n\n      if question.hidden?\n        begin\n          hidden_response = self.hidden_response output, input_stream\n          response = question.trimmable? ? hidden_response.strip : hidden_response\n        rescue ex : ACON::Exception\n          raise ex unless question.hidden_fallback?\n        end\n      end\n\n      if response.nil?\n        raise ACON::Exception::MissingInput.new \"Aborted.\" unless response = self.read_input input_stream, question\n        response = response.strip if question.trimmable?\n      end\n    else\n      autocomplete = self.autocomplete output, question, input_stream, autocompleter\n      response = question.trimmable? ? autocomplete.strip : autocomplete\n    end\n\n    if output.is_a? ACON::Output::Section\n      output.add_content \"\" # add EOL to the question\n      output.add_content response\n    end\n\n    question.process_response response\n  end\n\n  private def autocomplete(output : ACON::Output::Interface, question : ACON::Question::Base, input_stream : IO, autocompleter) : String\n    # TODO: Support autocompletion.\n    self.read_input(input_stream, question) || raise ACON::Exception::MissingInput.new \"Aborted.\"\n  end\n\n  private def hidden_response(output : ACON::Output::Interface, input_stream : IO) : String\n    response = if input_stream.tty? && input_stream.responds_to? :noecho\n                 input_stream.noecho &.gets 4096\n               elsif @@stty && ACON::Terminal.has_stty_available?\n                 stty_mode = `stty -g`\n                 system \"stty -echo\"\n\n                 input_stream.gets(4096).tap { system \"stty #{stty_mode}\" }\n               elsif input_stream.tty?\n                 raise ACON::Exception::Runtime.new \"Unable to hide the response.\"\n               end\n\n    raise ACON::Exception::MissingInput.new \"Aborted.\" if response.nil?\n\n    output.puts \"\"\n\n    response\n  end\n\n  private def read_input(input_stream : IO, question : ACON::Question::Base) : String?\n    unless question.multi_line?\n      return input_stream.gets 4096\n    end\n\n    # Can't just do `.gets_to_end` because we need to be able\n    # to return early if the only input provided is a newline.\n    String.build do |io|\n      input_stream.each_char do |char|\n        break if '\\n' == char && io.empty?\n        io << char\n      end\n    end\n  end\n\n  private def validate_attempts(output : ACON::Output::Interface, question : ACON::Question::Base, &)\n    error = nil\n    attempts = question.max_attempts\n\n    while attempts.nil? || attempts > 0\n      self.write_error output, error if error\n\n      begin\n        return question.validator.not_nil!.call yield\n      rescue ex : ACON::Exception::Runtime\n        raise ex\n      rescue ex : ::Exception\n        error = ex\n      ensure\n        attempts -= 1 if attempts\n        Fiber.yield\n      end\n    end\n\n    raise error.not_nil!\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/table.cr",
    "content": "# The Table helper can be used to display tabular data rendered to any `ACON::Output::Interface`.\n#\n# ```text\n# +---------------+--------------------------+------------------+\n# | ISBN          | Title                    | Author           |\n# +---------------+--------------------------+------------------+\n# | 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n# | 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens  |\n# | 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n# | 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n# +---------------+--------------------------+------------------+\n# ```\n#\n# # Usage\n#\n# Most commonly, a table will consist of a header row followed by one or more data rows:\n# ```\n# @[ACONA::AsCommand(\"table\")]\n# class TableCommand < ACON::Command\n#   protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#     ACON::Helper::Table.new(output)\n#       .headers(\"ISBN\", \"Title\", \"Author\")\n#       .rows([\n#         [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n#         [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\"],\n#         [\"960-425-059-0\", \"The Lord of the Rings\", \"J. R. R. Tolkien\"],\n#         [\"80-902734-1-6\", \"And Then There Were None\", \"Agatha Christie\"],\n#       ])\n#       .render\n#\n#     ACON::Command::Status::SUCCESS\n#   end\n# end\n# ```\n#\n# ## Separating Rows\n#\n# Row separators can be added anywhere in the output by passing an `ACON::Helper::Table::Separator` as a row.\n#\n# ```\n# table\n#   .rows([\n#     [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n#     [\"9971-5-0210-0\", \"A Tale of Two Cities\", \"Charles Dickens\"],\n#     ACON::Helper::Table::Separator.new,\n#     [\"960-425-059-0\", \"The Lord of the Rings\", \"J. R. R. Tolkien\"],\n#     [\"80-902734-1-6\", \"And Then There Were None\", \"Agatha Christie\"],\n#   ])\n# ```\n#\n# ```text\n# +---------------+--------------------------+------------------+\n# | ISBN          | Title                    | Author           |\n# +---------------+--------------------------+------------------+\n# | 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n# | 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens  |\n# +---------------+--------------------------+------------------+\n# | 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n# | 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n# +---------------+--------------------------+------------------+\n# ```\n#\n# ## Header/Footer Titles\n#\n# Header and/or footer titles can optionally be added via the `#header_title` and/or `#footer_title` methods.\n#\n# ```\n# table\n#   .header_title(\"Books\")\n#   .footer_title(\"Page 1/2\")\n# ```\n#\n# ```text\n# +---------------+----------- Books --------+------------------+\n# | ISBN          | Title                    | Author           |\n# +---------------+--------------------------+------------------+\n# | 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n# | 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens  |\n# +---------------+--------------------------+------------------+\n# | 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n# | 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n# +---------------+--------- Page 1/2 -------+------------------+\n# ```\n#\n# ## Column Sizing\n#\n# By default, the width of each column is calculated automatically based on their contents.\n# The `#column_widths` method can be used to set the column widths explicitly.\n#\n# ```\n# table\n#   .column_widths(10, 0, 30)\n#   .render\n# ```\n#\n# In this example, the first column's width will be `10`, the last column's width will be `30`, and the second column's width will be calculated automatically since it is zero.\n# If you only want to set the width of a specific column, the `#column_width` method can be used.\n#\n# ```\n# table\n#   .column_width(0, 10)\n#   .column_width(2, 30)\n#   .render\n# ```\n#\n# The resulting table would be:\n#\n# ```text\n# +---------------+------------------ Books -+--------------------------------+\n# | ISBN          | Title                    | Author                         |\n# +---------------+--------------------------+--------------------------------+\n# | 99921-58-10-7 | Divine Comedy            | Dante Alighieri                |\n# | 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens                |\n# +---------------+--------------------------+--------------------------------+\n# | 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien               |\n# | 80-902734-1-6 | And Then There Were None | Agatha Christie                |\n# +---------------+--------------------------+--------------------------------+\n# ```\n#\n# Notice that the width of the first column is greater than 10 characters wide.\n# This is because column widths are always considered as the minimum width.\n# If the content doesn't fit, it will be automatically increased to the longest content length.\n#\n# ### Max Width\n#\n# If you would rather wrap the contents in multiple rows, the `#column_max_width` method can be used.\n#\n# ```\n# table\n#   .column_max_width(0, 5)\n#   .column_max_width(1, 10)\n#   .render\n# ```\n#\n# This would cause the table to now be:\n#\n# ```text\n# +-------+------------+-- Books -----------------------+\n# | ISBN  | Title      | Author                         |\n# +-------+------------+--------------------------------+\n# | 99921 | Divine Com | Dante Alighieri                |\n# | -58-1 | edy        |                                |\n# | 0-7   |            |                                |\n# |                (the rest of the rows...)            |\n# +-------+------------+--------------------------------+\n# ```\n#\n# ## Orientation\n#\n# By default, the table contents are displayed as a normal table with the data being in rows, the first being the header row(s).\n# The table can also be rendered vertically or horizontally via the `#vertical` and `#horizontal` methods respectively.\n#\n# For example, the same contents rendered vertically would be:\n#\n# ```text\n# +----------------------------------+\n# |   ISBN: 99921-58-10-7            |\n# |  Title: Divine Comedy            |\n# | Author: Dante Alighieri          |\n# |----------------------------------|\n# |   ISBN: 9971-5-0210-0            |\n# |  Title: A Tale of Two Cities     |\n# | Author: Charles Dickens          |\n# |----------------------------------|\n# |   ISBN: 960-425-059-0            |\n# |  Title: The Lord of the Rings    |\n# | Author: J. R. R. Tolkien         |\n# |----------------------------------|\n# |   ISBN: 80-902734-1-6            |\n# |  Title: And Then There Were None |\n# | Author: Agatha Christie          |\n# +----------------------------------+\n# ```\n#\n# While horizontally, it would be:\n#\n# ```text\n# +--------+-----------------+----------------------+-----------------------+--------------------------+\n# | ISBN   | 99921-58-10-7   | 9971-5-0210-0        | 960-425-059-0         | 80-902734-1-6            |\n# | Title  | Divine Comedy   | A Tale of Two Cities | The Lord of the Rings | And Then There Were None |\n# | Author | Dante Alighieri | Charles Dickens      | J. R. R. Tolkien      | Agatha Christie          |\n# +--------+-----------------+----------------------+-----------------------+--------------------------+\n# ```\n#\n# ## Styles\n#\n# Up until now, all the tables have been rendered using the `default` style.\n# The table helper comes with a few additional built in styles, including:\n#\n# * borderless\n# * compact\n# * box\n# * double-box\n# * markdown\n#\n# The desired can be set via the `#style` method.\n#\n# ```\n# table\n#   .style(\"default\") # Same as not calling the method\n#   .render\n# ```\n#\n# ### borderless\n#\n# ```text\n# =============== ========================== ==================\n#  ISBN            Title                      Author\n# =============== ========================== ==================\n#  99921-58-10-7   Divine Comedy              Dante Alighieri\n#  9971-5-0210-0   A Tale of Two Cities       Charles Dickens\n# =============== ========================== ==================\n#  960-425-059-0   The Lord of the Rings      J. R. R. Tolkien\n#  80-902734-1-6   And Then There Were None   Agatha Christie\n# =============== ========================== ==================\n# ```\n#\n# ### compact\n#\n# ```text\n# ISBN          Title                    Author\n# 99921-58-10-7 Divine Comedy            Dante Alighieri\n# 9971-5-0210-0 A Tale of Two Cities     Charles Dickens\n# 960-425-059-0 The Lord of the Rings    J. R. R. Tolkien\n# 80-902734-1-6 And Then There Were None Agatha Christie\n# ```\n#\n# ### box\n#\n# ```text\n# ┌───────────────┬──────────────────────────┬──────────────────┐\n# │ ISBN          │ Title                    │ Author           │\n# ├───────────────┼──────────────────────────┼──────────────────┤\n# │ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  │\n# │ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  │\n# ├───────────────┼──────────────────────────┼──────────────────┤\n# │ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien │\n# │ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  │\n# └───────────────┴──────────────────────────┴──────────────────┘\n# ```\n#\n# ### double-box\n#\n# ```text\n# ╔═══════════════╤══════════════════════════╤══════════════════╗\n# ║ ISBN          │ Title                    │ Author           ║\n# ╠═══════════════╪══════════════════════════╪══════════════════╣\n# ║ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  ║\n# ║ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  ║\n# ╟───────────────┼──────────────────────────┼──────────────────╢\n# ║ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien ║\n# ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  ║\n# ╚═══════════════╧══════════════════════════╧══════════════════╝\n# ```\n#\n# ### markdown\n#\n# ```text\n# | ISBN          | Title                    | Author           |\n# |---------------|--------------------------|------------------|\n# | 99921-58-10-7 | Divine Comedy            | Dante Alighieri  |\n# | 9971-5-0210-0 | A Tale of Two Cities     | Charles Dickens  |\n# | 960-425-059-0 | The Lord of the Rings    | J. R. R. Tolkien |\n# | 80-902734-1-6 | And Then There Were None | Agatha Christie  |\n# ```\n#\n# ## Custom Styles\n#\n# If you would rather something more personal, custom styles can also be defined by providing `#style` with an `ACON::Helper::Table::Style` instance.\n#\n# ```\n# table_style = ACON::Helper::Table::Style.new\n#   .horizontal_border_chars(\"<fg=magenta>|</>\")\n#   .vertical_border_chars(\"<info>-</>\")\n#   .default_crossing_char(' ')\n#\n# table\n#   .style(table_style)\n#   .render\n# ```\n#\n# Notice you can use the same style tags as you can with `ACON::Formatter::OutputStyleInterface`s.\n# This is used by default to give some color to headers when allowed.\n#\n# TIP: Custom styles can also be registered globally:\n# ```\n# ACON::Helper::Table.set_style_definition \"colorful\", table_style\n#\n# # ...\n#\n# table.style(\"colorful\")\n# ```\n# This method can also be used to override the built-in styles.\n#\n# See `ACON::Helper::Table::Style` for more information.\n#\n# ## Table Cells\n#\n# The `ACON::Helper::Table::Cell` type can be used to style a specific cell.\n# Such as customizing the fore/background color, the alignment of the text, or the overall format of the cell.\n#\n# See the related type for more information/examples.\n#\n# ### Spanning Multiple Columns and Rows\n#\n# The `ACON::Helper::Table::Cell` type can also be used to add *colspan* and/or *rowspan* to a cell;\n# which would make it span more than one column/row.\n#\n# ```\n# ACON::Helper::Table.new(output)\n#   .headers(\"ISBN\", \"Title\", \"Author\")\n#   .rows([\n#     [\"99921-58-10-7\", \"Divine Comedy\", \"Dante Alighieri\"],\n#     ACON::Helper::Table::Separator.new,\n#     [ACON::Helper::Table::Cell.new(\"This value spans 3 columns.\", colspan: 3)],\n#   ])\n#   .render\n# ```\n#\n# This would result in:\n#\n# ```text\n# +---------------+---------------+-----------------+\n# | ISBN          | Title         | Author          |\n# +---------------+---------------+-----------------+\n# | 99921-58-10-7 | Divine Comedy | Dante Alighieri |\n# +---------------+---------------+-----------------+\n# | This value spans 3 columns.                     |\n# +---------------+---------------+-----------------+\n# ```\n#\n# TIP: This table cells with colspan and `center` alignment can be used to create header cells that span the entire table width:\n# ```\n# table\n#   .headers([\n#     [ACON::Helper::Table::Cell.new(\n#       \"Main table title\",\n#       colspan: 3,\n#       style: ACON::Helper::Table::CellStyle.new(\n#         align: :center\n#       )\n#     )],\n#     %w(ISBN Title Author),\n#   ])\n# ```\n# Would generate:\n# ```text\n# +--------+--------+--------+\n# |     Main table title     |\n# +--------+--------+--------+\n# | ISBN   | Title  | Author |\n# +--------+--------+--------+\n# ```\n#\n# In a similar way, *rowspan* can be used to have a column span multiple rows.\n# This is especially helpful for columns with line breaks.\n#\n# ```\n# ACON::Helper::Table.new(output)\n#   .headers(\"ISBN\", \"Title\", \"Author\")\n#   .rows([\n#     [\n#       \"978-0521567817\",\n#       \"De Monarchia\",\n#       ACON::Helper::Table::Cell.new(\"Dante Alighieri\\nspans multiple rows\", rowspan: 2),\n#     ],\n#     [\"978-0804169127\", \"Divine Comedy\"],\n#   ])\n#   .render\n# ```\n#\n# This would result in:\n#\n# ```text\n# +----------------+---------------+---------------------+\n# | ISBN           | Title         | Author              |\n# +----------------+---------------+---------------------+\n# | 978-0521567817 | De Monarchia  | Dante Alighieri     |\n# | 978-0804169127 | Divine Comedy | spans multiple rows |\n# +----------------+---------------+---------------------+\n# ```\n#\n# *colspan* and *rowspan* may also be used together to create any layout you can think of.\n#\n# ## Modifying Rendered Tables\n#\n# The `#render` method requires providing the entire table's content in order to fully render the table.\n# In some cases, that may not be possible if the data is generated dynamically.\n# In such cases, the `#append_row` method can be used which functions similarly to `#add_row`, but will append the rows to an already rendered table.\n#\n# INFO: This feature is only available when the table is rendered in an `ACON::Output::Section`.\n#\n# ```\n# @[ACONA::AsCommand(\"table\")]\n# class TableCommand < ACON::Command\n#   protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#     section = output.section\n#     table = ACON::Helper::Table.new(section)\n#       .add_row(\"Foo\")\n#\n#     table.render\n#\n#     table.append_row \"Bar\"\n#\n#     ACON::Command::Status::SUCCESS\n#   end\n# end\n# ```\n#\n# This ultimately results in:\n#\n# ```text\n# +-----+\n# | Foo |\n# | Bar |\n# +-----+\n# ```\nclass Athena::Console::Helper::Table\n  # Represents how the text within a cell should be aligned.\n  enum Alignment\n    # Aligns the text to the left of the cell.\n    #\n    # ```text\n    # +-----------------+\n    # | Text            |\n    # +-----------------+\n    # ```\n    LEFT\n\n    # Aligns the text to the right of the cell.\n    #\n    # ```text\n    # +-----------------+\n    # |            Text |\n    # +-----------------+\n    # ```\n    RIGHT\n\n    # Centers the text within the cell.\n    #\n    # ```text\n    # +-----------------+\n    # |      Text       |\n    # +-----------------+\n    # ```\n    CENTER\n  end\n\n  private enum Orientation\n    DEFAULT\n    HORIZONTAL\n    VERTICAL\n  end\n\n  # Represents a cell that can span more than one column/row and/or have a unique style.\n  # The cell may also have a value, which represents the value to display in the cell.\n  #\n  # For example:\n  #\n  # ```\n  # table\n  #   .rows([\n  #     [\n  #       \"Foo\",\n  #       ACON::Helper::Table::Cell.new(\n  #         \"Bar\",\n  #         style: ACON::Helper::Table::CellStyle.new(\n  #           align: :center,\n  #           foreground: \"red\",\n  #           background: \"green\"\n  #         )\n  #       ),\n  #     ],\n  #   ])\n  # ```\n  #\n  # See the [table docs][Athena::Console::Helper::Table--table-cells] and `ACON::Helper::Table::CellStyle` for more information.\n  class Cell\n    # Returns how many rows this cell should span.\n    getter rowspan : Int32\n\n    # Returns how many columns this cell should span.\n    getter colspan : Int32\n\n    # Returns the style representing how this cell should be styled.\n    getter style : Table::CellStyle?\n\n    @value : String\n\n    def initialize(\n      value : _ = \"\",\n      @rowspan : Int32 = 1,\n      @colspan : Int32 = 1,\n      @style : Table::CellStyle? = nil,\n    )\n      @value = value.to_s\n    end\n\n    def to_s(io : IO) : Nil\n      io << @value\n    end\n  end\n\n  # Represents a line that separates one or more rows.\n  #\n  # See the [separating rows][Athena::Console::Helper::Table--separating-rows] section for more information.\n  class Separator < Table::Cell\n    def initialize(\n      rowspan : Int32 = 1,\n      colspan : Int32 = 1,\n      style : Table::CellStyle? = nil,\n    )\n      super \"\", rowspan, colspan, style\n    end\n  end\n\n  # The possible types that are accepted as cell values.\n  # They are all eventually turned into strings.\n  alias CellType = String | Number::Primitive | Bool | Athena::Console::Helper::Table::Cell | Nil\n\n  # The possible types that represent a row.\n  alias RowType = Enumerable(CellType) | Athena::Console::Helper::Table::Separator\n\n  private struct Row\n    alias Type = String | Table::Cell | Nil\n\n    include Indexable::Mutable(Type)\n\n    delegate :insert, :<<, :[], to: @columns\n\n    @columns : Array(Type)\n\n    def self.new(columns : Enumerable(CellType))\n      new(columns.map do |c|\n        case c\n        when Athena::Console::Helper::Table::Cell, Nil then c\n        else                                                c.to_s\n        end\n      end)\n    end\n\n    def initialize(columns : Enumerable(Type))\n      @columns = columns.to_a.map &.as Type\n    end\n\n    def size : Int\n      @columns.size\n    end\n\n    def unsafe_fetch(index : Int) : Type\n      @columns[index]\n    end\n\n    def unsafe_put(index : Int, value : Type) : Nil\n      @columns[index] = value\n    end\n\n    def each(& : Type ->) : Nil\n      @columns.each do |c|\n        yield c\n      end\n    end\n  end\n\n  # OPTIMIZE: Can this be merged into `Row`?\n  private struct Rows\n    alias Type = Table::Separator | Array(Row::Type)\n\n    include Enumerable(Type)\n\n    @columns : Array(Array(Type))\n\n    def initialize(@columns : Array(Array(Type))); end\n\n    def each(& : Array(Type) ->) : Nil\n      @columns.each do |c|\n        yield c\n      end\n    end\n  end\n\n  # INTERNAL\n  protected class_getter styles : Hash(String, ACON::Helper::Table::Style) { self.init_styles }\n\n  # Registers the provided *style* with the provided *name*.\n  #\n  # See [custom styles][Athena::Console::Helper::Table--custom-styles].\n  def self.set_style_definition(name : String, style : ACON::Helper::Table::Style) : Nil\n    self.styles[name] = style\n  end\n\n  # Returns the `ACON::Helper::Table::Style` style with the provided *name*,\n  # raising an `ACON::Exception::InvalidArgument` if no style with that name is defined.\n  def self.style_definition(name : String) : ACON::Helper::Table::Style\n    self.styles[name]? || raise ACON::Exception::InvalidArgument.new \"The table style '#{name}' is not defined.\"\n  end\n\n  # INTERNAL\n  private def self.init_styles : Hash(String, ACON::Helper::Table::Style)\n    markdown = Table::Style.new\n      .default_crossing_char('|')\n      .display_outside_border(false)\n\n    borderless = Table::Style.new\n      .horizontal_border_chars(\"=\")\n      .vertical_border_chars(\" \")\n      .default_crossing_char(\" \")\n\n    compact = Table::Style.new\n      .horizontal_border_chars(\"\")\n      .vertical_border_chars(\"\")\n      .default_crossing_char(\"\")\n      .cell_row_content_format(\"%s \")\n\n    suggested = Table::Style.new\n      .horizontal_border_chars(\"-\")\n      .vertical_border_chars(\" \")\n      .default_crossing_char(\" \")\n      .cell_header_format(\"%s\")\n\n    box = Table::Style.new\n      .horizontal_border_chars(\"─\")\n      .vertical_border_chars(\"│\")\n      .crossing_chars(\"┼\", \"┌\", \"┬\", \"┐\", \"┤\", \"┘\", \"┴\", \"└\", \"├\")\n\n    double_box = Table::Style.new\n      .horizontal_border_chars(\"═\", \"─\")\n      .vertical_border_chars(\"║\", \"│\")\n      .crossing_chars(\"┼\", \"╔\", \"╤\", \"╗\", \"╢\", \"╝\", \"╧\", \"╚\", \"╟\", \"╠\", \"╪\", \"╣\")\n\n    {\n      \"borderless\" => borderless,\n      \"box\"        => box,\n      \"compact\"    => compact,\n      \"default\"    => ACON::Helper::Table::Style.new,\n      \"double-box\" => double_box,\n      \"markdown\"   => markdown,\n      \"suggested\"  => suggested,\n    }\n  end\n\n  @header_title : String? = nil\n  @footer_title : String? = nil\n\n  @headers = Array(Row).new\n  @rows = Array(Row | Table::Separator).new\n\n  @effective_column_widths = Hash(Int32, Int32).new\n  @number_of_columns : Int32? = nil\n  @column_styles = Hash(Int32, ACON::Helper::Table::Style).new\n  @column_widths = Hash(Int32, Int32).new\n  @column_max_widths = Hash(Int32, Int32).new\n  @rendered = false\n  @orientation : Orientation = :default\n\n  # Returns the `ACON::Helper::Table::Style` used by this table.\n  getter style : ACON::Helper::Table::Style\n\n  @output : ACON::Output::Interface\n\n  def initialize(@output : ACON::Output::Interface)\n    @style = ACON::Helper::Table::Style.new\n  end\n\n  # Sets the table [header title][Athena::Console::Helper::Table--headerfooter-titles].\n  def header_title(@header_title : String?) : self\n    self\n  end\n\n  # Sets the table [footer title][Athena::Console::Helper::Table--headerfooter-titles].\n  def footer_title(@footer_title : String?) : self\n    self\n  end\n\n  # Sets the style of this table.\n  # *style* may either be an explicit `ACON::Helper::Table::Style`,\n  # or the name of the style to use if it is built-in, or was registered via `.set_style_definition`.\n  #\n  # See [styles][Athena::Console::Helper::Table--styles] and [custom styles][Athena::Console::Helper::Table--custom-styles].\n  def style(style : String | ACON::Helper::Table::Style) : self\n    @style = self.resolve_style style\n\n    self\n  end\n\n  # Sets the style of the column at the provided *index*.\n  # *style* may either be an explicit `ACON::Helper::Table::Style`,\n  # or the name of the style to use if it is built-in, or was registered via `.set_style_definition`.\n  def column_style(index : Int32, style : ACON::Helper::Table::Style | String) : self\n    @column_styles[index] = self.resolve_style style\n\n    self\n  end\n\n  # Returns the `ACON::Helper::Table::Style` the column at the provided *index* is using, falling back on `#style`.\n  def column_style(index : Int32) : ACON::Helper::Table::Style\n    @column_styles[index]? || self.style\n  end\n\n  # Sets the minimum *width* for the column at the provided *index*.\n  #\n  # See [column sizing][Athena::Console::Helper::Table--column-sizing].\n  def column_width(index : Int32, width : Int32) : self\n    @column_widths[index] = width\n\n    self\n  end\n\n  # Sets the minimum column widths to the provided *widths*.\n  #\n  # See [column sizing][Athena::Console::Helper::Table--column-sizing].\n  def column_widths(widths : Enumerable(Int32)) : self\n    @column_widths.clear\n\n    widths.each_with_index do |w, idx|\n      self.column_width idx, w\n    end\n\n    self\n  end\n\n  # :ditto:\n  def column_widths(*widths : Int32) : self\n    self.column_widths widths\n  end\n\n  # Sets the maximum *width* for the column at the provided *index*.\n  #\n  # See [column sizing][Athena::Console::Helper::Table--column-sizing].\n  def column_max_width(index : Int32, width : Int32) : self\n    if !@output.formatter.is_a? ACON::Formatter::WrappableInterface\n      raise ACON::Exception::Logic.new \"Setting a maximum column width is only supported when using a #{ACON::Formatter::WrappableInterface} formatter, got #{@output.class}.\"\n    end\n\n    @column_max_widths[index] = width\n\n    self\n  end\n\n  def headers(*names : CellType) : self\n    self.headers names\n  end\n\n  def headers(headers : RowType) : self\n    self.headers({headers})\n  end\n\n  def headers(headers : Enumerable(RowType)) : self\n    @headers.clear\n\n    headers.each do |h|\n      @headers << Row.new h\n    end\n\n    self\n  end\n\n  # Overrides the rows of this table to those provided in *rows*.\n  #\n  # ```\n  # table\n  #   .rows(%w(Foo Bar Baz))\n  #   .render\n  # ```\n  def rows(rows : RowType) : self\n    self.rows({rows})\n  end\n\n  # Overrides the rows of this table to those provided in *rows*.\n  #\n  # ```\n  # table\n  #   .rows([\n  #     %w(One Two Three),\n  #     %w(Foo Bar Baz),\n  #   ])\n  #   .render\n  # ```\n  def rows(rows : Enumerable(RowType)) : self\n    @rows.clear\n\n    self.add_rows rows\n  end\n\n  # Similar to `#rows(rows : Enumerable(RowType))`, but appends the provided *rows* to this table.\n  #\n  # ```\n  # # Existing rows are not removed.\n  # table\n  #   .add_rows([\n  #     %w(One Two Three),\n  #     %w(Foo Bar Baz),\n  #   ])\n  #   .render\n  # ```\n  def add_rows(rows : Enumerable(RowType)) : self\n    rows.each do |r|\n      self.add_row r\n    end\n\n    self\n  end\n\n  # Adds a single new *row* to this table.\n  #\n  # ```\n  # # Existing rows are not removed.\n  # table\n  #   .add_row(%w(One Two Three))\n  #   .add_row(%w(Foo Bar Baz))\n  #   .render\n  # ```\n  def add_row(row : RowType) : self\n    @rows << case row\n    when Table::Separator then row\n    else\n      Row.new row\n    end\n\n    self\n  end\n\n  # Adds the provided *columns* as a single row to this table.\n  #\n  # ```\n  # # Existing rows are not removed.\n  # table\n  #   .add_row(\"One\", \"Two\", \"Three\")\n  #   .add_row(\"Foo\", \"Bar\", \"Baz\")\n  #   .render\n  # ```\n  def add_row(*columns : CellType) : self\n    self.add_row columns\n\n    self\n  end\n\n  # Appends *row* to an already rendered table.\n  #\n  # See [modifying rendered tables][Athena::Console::Helper::Table--modifying-rendered-tables]\n  def append_row(row : RowType) : self\n    unless (output = @output).is_a? ACON::Output::Section\n      raise ACON::Exception::Logic.new \"Appending a row is only supported when using a #{ACON::Output::Section} output, got #{@output.class}.\"\n    end\n\n    if @rendered\n      output.clear self.calculate_row_count\n    end\n\n    self.add_row row\n    self.render\n\n    self\n  end\n\n  # Appends the provided *columns* as a single row to an already rendered table.\n  #\n  # See [modifying rendered tables][Athena::Console::Helper::Table--modifying-rendered-tables]\n  def append_row(*columns : CellType) : self\n    self.append_row([*columns])\n  end\n\n  # Manually sets the provided *row* to the provided *index*.\n  #\n  # ```\n  # # Existing rows are not removed.\n  # table\n  #   .add_row(%w(One Two Three))\n  #   .row(0, %w(Foo Bar Baz)) # Overrides row 0 to this row\n  #   .render\n  # ```\n  def row(index : Int32, row : RowType) : self\n    @rows[index] = Row.new row\n\n    self\n  end\n\n  # Changes this table's [orientation][Athena::Console::Helper::Table--orientation] to horizontal.\n  def horizontal : self\n    @orientation = :horizontal\n\n    self\n  end\n\n  # Changes this table's [orientation][Athena::Console::Helper::Table--orientation] to vertical.\n  def vertical : self\n    @orientation = :vertical\n\n    self\n  end\n\n  private alias InternalRowType = Row | ACON::Helper::Table::Separator\n\n  # Renders this table to the `ACON::Output::Interface` it was instantiated with.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  def render\n    divider = ACON::Helper::Table::Separator.new\n\n    rows = self.combined_rows divider\n\n    self.calculate_number_of_columns rows\n\n    row_groups = self.build_table_rows rows\n    self.calculate_columns_width row_groups\n\n    is_header = !@orientation.horizontal?\n    is_first_row = @orientation.horizontal?\n    has_title = !!@header_title.presence\n\n    row_groups.each do |row_group|\n      is_header_separator_rendered : Bool = false\n\n      row_group.each do |row|\n        if divider == row\n          is_header = false\n          is_first_row = true\n\n          next\n        end\n\n        if row.is_a? Table::Separator\n          self.render_row_separator\n\n          next\n        end\n\n        # TODO: Handle empty/nil rows?\n\n        if is_header && !is_header_separator_rendered && @style.display_outside_border?\n          self.render_row_separator(\n            is_header ? RowSeparator::TOP : RowSeparator::TOP_BOTTOM,\n            has_title ? @header_title : nil,\n            has_title ? @style.header_title_format : nil\n          )\n\n          has_title = false\n          is_header_separator_rendered = true\n        end\n\n        if is_first_row\n          self.render_row_separator(\n            is_header ? RowSeparator::TOP : RowSeparator::TOP_BOTTOM,\n            has_title ? @header_title : nil,\n            has_title ? @style.header_title_format : nil\n          )\n\n          is_first_row = false\n          has_title = false\n        end\n\n        if @orientation.vertical?\n          is_header = false\n          is_first_row = false\n        end\n\n        if @orientation.horizontal?\n          self.render_row row, @style.cell_row_format, @style.cell_header_format\n        else\n          self.render_row row, is_header ? @style.cell_header_format : @style.cell_row_format\n        end\n      end\n    end\n\n    if @style.display_outside_border?\n      self.render_row_separator :bottom, @footer_title, @style.footer_title_format\n    end\n\n    self.cleanup\n    @rendered = true\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def combined_rows(divider : Table::Separator) : Array(InternalRowType)\n    rows = Array(InternalRowType).new\n    is_cell_with_colspan = ->(cell : CellType) { cell.is_a?(ACON::Helper::Table::Cell) && cell.colspan >= 2 }\n\n    if @orientation.horizontal?\n      @headers[0]?.try &.each_with_index do |header, idx|\n        rows.insert idx, Row.new [header]\n        @rows.each do |row|\n          next if row.is_a? Table::Separator\n\n          if rv = row[idx]?\n            rows[idx].as(Row) << rv\n          elsif is_cell_with_colspan.call rows[idx].as(Row)[0]\n            # Noop, there is a \"title\"\n          else\n            rows[idx].as(Row) << \"\"\n          end\n        end\n      end\n    elsif @orientation.vertical?\n      formatter = @output.formatter\n      max_header_length = (@headers[0]? || [] of String).reduce 0 do |acc, header|\n        Math.max acc, Helper.width(Helper.remove_decoration(formatter, header.to_s))\n      end\n\n      @rows.each do |row|\n        next if row.is_a? Table::Separator\n\n        unless rows.empty?\n          rows << Row.new [divider]\n        end\n\n        contains_colspan = false\n\n        row.each do |cell|\n          if contains_colspan = is_cell_with_colspan.call cell\n            break\n          end\n        end\n\n        headers = @headers[0]? || [] of String\n        max_rows = Math.max headers.size, row.size\n\n        max_rows.times do |idx|\n          cell = (row[idx]? || \"\").to_s\n\n          cell.split(\"\\n\").each_with_index do |part, part_idx|\n            if !headers.empty? && !contains_colspan\n              if part_idx.zero?\n                rows << Row.new([\n                  sprintf(\n                    \"<comment>%s</>: %s\",\n                    headers[idx]?.to_s.rjust(max_header_length, ' '),\n                    part\n                  ),\n                ])\n              else\n                rows << Row.new([\n                  sprintf(\n                    \"%s  %s\",\n                    \"\".rjust(max_header_length, ' '),\n                    part\n                  ),\n\n                ])\n              end\n            elsif !cell.empty?\n              rows << Row.new [part]\n            end\n          end\n        end\n      end\n    else\n      @headers.each { |h| rows << Row.new h unless h.empty? }\n      rows << divider\n      @rows.each do |r|\n        case r\n        when Table::Separator then rows << r\n        else\n          rows << Row.new r unless r.empty?\n        end\n      end\n    end\n\n    rows\n  end\n\n  private def cleanup : Nil\n    @effective_column_widths.clear\n    @number_of_columns = nil\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def build_table_rows(rows : Array(InternalRowType)) : Rows\n    formatter = @output.formatter.as ACON::Formatter::WrappableInterface\n\n    # row_key => line_key => column idx\n    unmerged_rows = Hash(Int32, Hash(Int32, Hash(Int32, String | Table::Cell))).new\n\n    row_key = 0\n    while row_key < rows.size\n      self.fill_next_rows rows, row_key\n\n      # Remove any line breaks and replace it with a new line\n      self.iterate_row(rows, row_key) do |cell, column|\n        cell_value = cell.to_s\n\n        colspan = cell.is_a?(ACON::Helper::Table::Cell) ? cell.colspan : 1\n\n        if (max_width = @column_max_widths[column]?) && (Helper.width(Helper.remove_decoration(formatter, cell_value)) > max_width)\n          cell_value = formatter.format_and_wrap cell_value, max_width * colspan\n        end\n\n        next unless cell_value.includes? '\\n'\n\n        escaped = cell_value.split('\\n').join '\\n' { |v| ACON::Formatter::Output.escape_trailing_backslash v }\n        cell = cell.is_a?(Table::Cell) ? Table::Cell.new(escaped, colspan: cell.colspan) : escaped\n        cell_value = cell.to_s\n        lines = cell_value.gsub('\\n', \"<fg=default;bg=default></>\\n\").split '\\n'\n        lines.each_with_index do |line, line_key|\n          if colspan > 1\n            line = Table::Cell.new line, colspan: colspan\n          end\n\n          if line_key.zero?\n            rows[row_key].as(Row)[column] = line\n          else\n            if !unmerged_rows.has_key?(row_key) || !unmerged_rows[row_key].has_key? line_key\n              (unmerged_rows[row_key] ||= Hash(Int32, Hash(Int32, String | Table::Cell)).new)[line_key] = self.copy_row rows, row_key\n            end\n\n            unmerged_rows[row_key][line_key][column] = line\n          end\n        end\n      end\n\n      row_key += 1\n    end\n\n    row_groups = [] of Array(Rows::Type)\n\n    rows.each_with_index do |row, rk|\n      row_group = [row.is_a?(Table::Separator) ? row : self.fill_cells(row)] of Rows::Type\n\n      if ur = unmerged_rows[rk]?\n        ur.each_value do |r|\n          row_group << (r.is_a?(Table::Separator) ? r : self.fill_cells(r))\n        end\n      end\n\n      row_groups << row_group\n    end\n\n    Rows.new row_groups\n  end\n\n  # Fills rows that contain rowspan > 1\n  private def fill_next_rows(rows : Enumerable, line : Int32) : Nil\n    unmerged_rows = Hash(Int32, Hash(Int32, Table::Cell)).new\n\n    self.iterate_row(rows, line) do |cell, column|\n      cell_value = cell.to_s\n\n      if cell.is_a?(ACON::Helper::Table::Cell) && cell.rowspan > 1\n        nb_lines = cell.rowspan - 1\n        lines = [cell_value]\n\n        if cell_value.includes? '\\n'\n          lines = cell_value.gsub(\"\\n\", \"<fg=default;bg=default>\\n</>\").split '\\n'\n          nb_lines = lines.size > nb_lines ? cell_value.count('\\n') : nb_lines\n\n          rows[line].as(Row)[column] = Table::Cell.new lines.first, colspan: cell.colspan, style: cell.style\n        end\n\n        fill = Hash(Int32, Hash(Int32, Table::Cell)).new\n        nb_lines.times do |l|\n          fill[line + 1 + l] = Hash(Int32, Table::Cell).new\n        end\n\n        unmerged_rows = fill.merge! unmerged_rows\n\n        unmerged_rows.each_key do |unmerged_row_key|\n          value = lines[unmerged_row_key - line]? || \"\"\n          (unmerged_rows[unmerged_row_key] ||= Hash(Int32, Table::Cell).new)[column] = Table::Cell.new value, colspan: cell.colspan, style: cell.style\n\n          if nb_lines == unmerged_row_key - line\n            break\n          end\n        end\n      end\n    end\n\n    unmerged_rows.each do |unmerged_row_key, unmerged_row|\n      if (ur = rows[unmerged_row_key]?) && ur.is_a?(Enumerable) && ((self.get_number_of_columns(ur) + self.get_number_of_columns(unmerged_rows[unmerged_row_key])) <= @number_of_columns.not_nil!)\n        unmerged_row.each do |cell_key, c|\n          rows[unmerged_row_key].as(Row).insert cell_key, c\n        end\n      else\n        row = self.copy_row rows, unmerged_row_key - 1\n        unmerged_row.each_key do |column|\n          row[column] = unmerged_row[column]\n        end\n\n        rows.insert unmerged_row_key, Row.new row.values\n      end\n    end\n  end\n\n  # Fills cells for a colspan > 1\n  private def fill_cells(row : Row) : Array(Row::Type)\n    new_row = [] of Row::Type\n\n    row.each_with_index do |cell, column|\n      new_row << cell\n\n      if cell.is_a?(Table::Cell) && cell.colspan > 1\n        ((column + 1)...(column + cell.colspan)).each do\n          new_row << \"\"\n        end\n      end\n    end\n\n    new_row.empty? ? row.to_a : new_row\n  end #\n\n  # OPTIMIZE: See about making Row an Enumerable({Int32, Row::Type}) to allow both Row and Hash contexts\n  private def fill_cells(row : Hash(Int32, Row::Type)) : Array(Row::Type)\n    new_row = [] of Row::Type\n\n    row.each do |column, cell|\n      new_row << cell\n\n      if cell.is_a?(Table::Cell) && cell.colspan > 1\n        ((column + 1)...(column + cell.colspan)).each do\n          new_row << \"\"\n        end\n      end\n    end\n\n    new_row\n  end\n\n  private def copy_row(rows : Array(InternalRowType), line : Int32) : Hash(Int32, String | Table::Cell)\n    new_row = Hash(Int32, String | Table::Cell).new\n    rows[line].as(Row).each_with_index do |cell, cell_key|\n      new_row[cell_key] = \"\"\n\n      if cell.is_a? Table::Cell\n        new_row[cell_key] = Table::Cell.new(\"\", colspan: cell.colspan)\n      end\n    end\n\n    new_row\n  end\n\n  private def calculate_number_of_columns(rows : Enumerable) : Nil\n    columns = [0]\n\n    rows.each do |row|\n      next if row.is_a? ACON::Helper::Table::Separator\n\n      columns << self.get_number_of_columns row\n    end\n\n    @number_of_columns = columns.max\n  end\n\n  private def calculate_columns_width(groups : Rows) : Nil\n    @number_of_columns.not_nil!.times do |column|\n      lengths = [] of Int32\n\n      groups.each do |group|\n        group.each do |row|\n          # Avoid mutating the actual row, as the logic below is just used to calculate widths\n          row = row.dup\n\n          next if row.is_a? Table::Separator\n\n          row.each_with_index do |cell, idx|\n            if cell.is_a? Table::Cell\n              text_content = Helper.remove_decoration @output.formatter, cell.to_s\n              text_length = Helper.width text_content\n\n              if text_length > 0\n                # Split content into an array of n chars each\n                content_columns = text_content.split(/(#{\".\" * (text_length / cell.colspan).ceil.to_i})/, remove_empty: true)\n\n                content_columns.each_with_index do |content, position|\n                  row[idx + position] = content\n                end\n              end\n            end\n          end\n\n          lengths << self.get_cell_width row, column\n        end\n      end\n\n      @effective_column_widths[column] = lengths.max + Helper.width(@style.cell_row_content_format) - 2\n    end\n  end\n\n  private def get_cell_width(row : Rows::Type, column : Int32) : Int32\n    cell_width = 0\n\n    if cell = row[column]?\n      cell_width = Helper.width Helper.remove_decoration @output.formatter, cell.to_s\n    end\n\n    column_width = @column_widths[column]? || 0\n    cell_width = Math.max cell_width, column_width\n\n    (max_width = @column_max_widths[column]?) ? Math.min(max_width, cell_width) : cell_width\n  end\n\n  private def get_column_separator_width : Int32\n    Helper.width sprintf @style.border_format, @style.border_chars[3]\n  end\n\n  private def get_number_of_columns(row : Enumerable) : Int32\n    columns = row.size\n\n    row.each do |column|\n      columns += column.is_a?(ACON::Helper::Table::Cell) ? column.colspan - 1 : 0\n    end\n\n    columns\n  end\n\n  private def calculate_row_count : Int32\n    number_of_rows = self.combined_rows(Table::Separator.new).size\n\n    unless @headers.empty?\n      number_of_rows += 1\n    end\n\n    unless @rows.empty?\n      number_of_rows += 1\n    end\n\n    number_of_rows\n  end\n\n  private enum RowSeparator\n    TOP\n    TOP_BOTTOM\n    MIDDLE\n    BOTTOM\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def render_row_separator(type : RowSeparator = :middle, title : String? = nil, title_format : String? = nil) : Nil\n    return unless count = @number_of_columns\n\n    borders = @style.border_chars\n    crossings = @style.crossing_chars\n\n    horizontal, left_char, middle_char, right_char = case type\n                                                     when .middle?     then {borders[2], crossings[8], crossings[0], crossings[4]}\n                                                     when .top?        then {borders[0], crossings[1], crossings[2], crossings[3]}\n                                                     when .top_bottom? then {borders[0], crossings[9], crossings[10], crossings[11]}\n                                                     else\n                                                       {borders[0], crossings[7], crossings[6], crossings[5]}\n                                                     end\n\n    markup = String.build do |io|\n      break \"\" if count.zero?\n\n      io << left_char\n\n      count.times do |column|\n        io << horizontal * @effective_column_widths[column]\n        io << ((column == (count - 1)) ? right_char : middle_char)\n      end\n    end\n\n    if !title.nil? && title_format\n      title_length = Helper.width Helper.remove_decoration((formatter = @output.formatter), (formatted_title = sprintf(title_format, title)))\n      markup_length = Helper.width markup\n\n      if title_length > (limit = markup_length - 4)\n        title_length = limit\n        format_length = Helper.width Helper.remove_decoration(formatter, sprintf(title_format, \"\"))\n        formatted_title = sprintf title_format, \"#{title[0, limit - format_length - 3]}...\"\n      end\n\n      title_start = (markup_length - title_length) // 2\n      markup = \"#{markup[0, title_start]}#{formatted_title}#{markup[((title_start + title_length)..)]}\"\n    end\n\n    return unless markup.presence\n\n    @output.puts sprintf @style.border_format, markup\n  end\n\n  private def render_row(row : Rows::Type, cell_format : String, first_cell_format : String? = nil) : Nil\n    columns = self.get_row_columns row\n    last = columns.size - 1\n\n    markup = String.build do |io|\n      io << self.render_column_separator :outside\n\n      columns.each_with_index do |column, idx|\n        io << if first_cell_format && idx.zero?\n          self.render_cell row, column, first_cell_format\n        else\n          self.render_cell row, column, cell_format\n        end\n\n        io << self.render_column_separator last == idx ? Border::OUTSIDE : Border::INSIDE\n      end\n    end\n\n    @output.puts markup\n  end\n\n  private def render_cell(row : Rows::Type, column : Int32, cell_format : String) : String\n    cell = (row[column]? || \"\")\n    cell_value = cell.to_s\n    width = @effective_column_widths[column]\n\n    if cell.is_a?(Table::Cell) && cell.colspan > 1\n      # Add the width of the following columns (numbers of colspan)\n      ((column + 1)..(column + cell.colspan - 1)).each do |next_column|\n        width += self.get_column_separator_width + @effective_column_widths[next_column]\n      end\n    end\n\n    style = self.get_column_style column\n    padding_style = style\n\n    if cell.is_a? Table::Separator\n      return sprintf style.border_format, style.border_chars[2] * width\n    end\n\n    width += cell_value.size - Helper.remove_decoration(@output.formatter, cell_value).size\n    content = sprintf style.cell_row_content_format, cell_value\n\n    if cell.is_a?(Table::Cell) && (cell_style = cell.style)\n      unless cell_value.matches? /^<(\\w+|(\\w+=[\\w,]+;?)*)>.+<\\/(\\w+|(\\w+=\\w+;?)*)?>$/\n        unless cell_format = cell_style.format\n          tag = cell_style.tag\n          cell_format = \"<#{tag}>%s</>\"\n        end\n\n        if content.includes? \"</>\"\n          content = content.gsub \"</>\", \"\"\n          width -= 3\n        end\n\n        if content.includes? \"<fg=default;bg=default>\"\n          content = content.gsub \"<fg=default;bg=default>\", \"\"\n          width -= \"<fg=default;bg=default>\".size\n        end\n      end\n\n      padding_style = cell_style\n    end\n\n    sprintf cell_format, padding_style.pad(content, width, style.padding_char)\n  end\n\n  private def get_row_columns(row : Rows::Type) : Array(Int32)\n    columns = (0...@number_of_columns).to_a\n\n    row.each_with_index do |cell, cell_key|\n      if cell.is_a? Table::Cell\n        # Exclude grouped columns\n        columns = columns - ((cell_key + 1)...(cell_key + cell.colspan)).to_a\n      end\n    end\n\n    columns\n  end\n\n  private enum Border\n    OUTSIDE\n    INSIDE\n  end\n\n  private def render_column_separator(type : Border = :outside) : String\n    borders = @style.border_chars\n\n    sprintf @style.border_format, type.outside? ? borders[1] : borders[3]\n  end\n\n  # Helper method that allows iterating over the cells of a row, skipping cell separators\n  private def iterate_row(rows : Enumerable, line : Int32, & : Row::Type, Int32 ->) : Nil\n    columns = rows[line]\n\n    return if columns.is_a? ACON::Helper::Table::Separator\n\n    columns.each_with_index do |cell, idx|\n      yield cell, idx\n    end\n  end\n\n  private def get_column_style(column : Int32) : Style\n    @column_styles[column]? || @style\n  end\n\n  private def resolve_style(style : ACON::Helper::Table::Style) : Style\n    style\n  end\n\n  private def resolve_style(style : String) : Style\n    self.class.styles[style]? || raise ACON::Exception::InvalidArgument.new \"The table style '#{style}' is not defined.\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/table_cell_style.cr",
    "content": "# Represents the styling for a specific `ACON::Helper::Table::Cell`.\nstruct Athena::Console::Helper::Table::CellStyle\n  # Returns the foreground color for this cell.\n  #\n  # Can be any color string supported via [ACON::Formatter::OutputStyleInterface][Athena::Console::Formatter::OutputStyleInterface--inline-styles],\n  # e.g. named (`\"red\"`) or hexadecimal (`\"#38bdc2\"`) colors.\n  getter foreground : String\n\n  # Returns the background color for this cell.\n  #\n  # Can be any color string supported via [ACON::Formatter::OutputStyleInterface][Athena::Console::Formatter::OutputStyleInterface--inline-styles],\n  # e.g. named (`\"red\"`) or hexadecimal (`\"#38bdc2\"`) colors.\n  getter background : String\n\n  # How the text should be aligned in the cell.\n  #\n  # See `ACON::Helper::Table::Alignment`.\n  getter align : ACON::Helper::Table::Alignment\n\n  # A `sprintf` format string representing the content of the cell.\n  # Should have a single `%s` representing the cell's value.\n  #\n  # Can be used to reuse [custom style tags][Athena::Console::Formatter::OutputStyleInterface--custom-styles].\n  # E.g. `\"<fire>%s</>\"`.\n  getter format : String?\n\n  def initialize(\n    @foreground : String = \"default\",\n    @background : String = \"default\",\n    @align : ACON::Helper::Table::Alignment = :left,\n    @format : String? = nil,\n  )\n  end\n\n  protected def tag : String\n    \"fg=#{@foreground};bg=#{@background}\"\n  end\n\n  protected def pad(string : String, width : Int32, padding_char) : String\n    case @align\n    in .left?   then string.ljust width, padding_char\n    in .right?  then string.rjust width, padding_char\n    in .center? then string.center width, padding_char\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/helper/table_style.cr",
    "content": "# Represents the overall style for a table.\n# Including the characters that make up the row/column separators, crosses, cell formats, and default alignment.\n#\n# This class provides a fluent interface for configuring each part of the style.\nclass Athena::Console::Helper::Table::Style\n  @horizontal_outside_border_char = \"-\"\n  @horizontal_inside_border_char = \"-\"\n  @vertical_outside_border_char = \"|\"\n  @vertical_inside_border_char = \"|\"\n\n  @crossing_char : String = \"+\"\n  @crossing_top_right_char = \"+\"\n  @crossing_top_middle_char = \"+\"\n  @crossing_top_left_char = \"+\"\n  @crossing_bottom_right_char = \"+\"\n  @crossing_bottom_middle_char = \"+\"\n  @crossing_bottom_left_char = \"+\"\n  @crossing_middle_right_char = \"+\"\n  @crossing_middle_left_char = \"+\"\n  @crossing_top_left_bottom_char = \"+\"\n  @crossing_top_middle_bottom_char = \"+\"\n  @crossing_top_right_bottom_char = \"+\"\n\n  protected getter padding_char : Char = ' '\n  protected getter header_title_format : String = \"<fg=black;bg=white;options=bold> %s </>\"\n  protected getter footer_title_format : String = \"<fg=black;bg=white;options=bold> %s </>\"\n  protected getter cell_header_format : String = \"<info>%s</info>\"\n  protected getter cell_row_format : String = \"%s\"\n  protected getter cell_row_content_format : String = \" %s \"\n  protected getter border_format : String = \"%s\"\n  protected getter? display_outside_border : Bool = true\n  protected getter align : ACON::Helper::Table::Alignment = :left\n\n  def_clone\n\n  # Sets the default cell alignment for the table.\n  #\n  # See `ACON::Helper::Table::Alignment`.\n  def align(@align : ACON::Helper::Table::Alignment) : self\n    self\n  end\n\n  def display_outside_border(@display_outside_border : Bool) : self\n    self\n  end\n\n  # Sets the `sprintf` format string for the border, defaulting to `\"%s\"`.\n  #\n  # For example, if set to `\"~%s~\"` with the cell's content being `text`:\n  #\n  # ```text\n  # ~+------+~\n  # ~|~ text ~|~\n  # ~+------+~\n  # ```\n  #\n  # WARNING: Customizing this format can mess with the formatting of the whole table.\n  def border_format(format : String) : self\n    @border_format = format\n\n    self\n  end\n\n  # Sets the the character that is added to the cell to ensure its content has the correct `ACON::Helper::Table::Alignment`, defaulting to `' '`.\n  #\n  # For example, if the padding character was `'_'` with a left alignment:\n  #\n  # ```text\n  # +-----+\n  # | 7 __|\n  # +-----+\n  # ```\n  def padding_char(char : Char) : self\n    @padding_char = char\n\n    self\n  end\n\n  # Sets the `sprintf` format string used for [header titles][Athena::Console::Helper::Table--headerfooter-titles], defaulting to `\"<fg=black;bg=white;options=bold> %s </>\"`.\n  def header_title_format(format : String) : self\n    @header_title_format = format.to_s\n\n    self\n  end\n\n  # Sets the `sprintf` format string used for [footer titles][Athena::Console::Helper::Table--headerfooter-titles], defaulting to `\"<fg=black;bg=white;options=bold> %s </>\"`.\n  def footer_title_format(format : String) : self\n    @footer_title_format = format.to_s\n\n    self\n  end\n\n  # Sets the `sprintf` format string used for table headings, defaulting to `\"<info>%s</info>\"`.\n  def cell_header_format(format : String) : self\n    @cell_header_format = format.to_s\n\n    self\n  end\n\n  # Sets the `sprintf` format string used for cell contents, defaulting to `\"%s\"`.\n  #\n  # For example, if set to `\"~%s~\"` with the cell's content being `text`:\n  #\n  # ```text\n  # +------+\n  # |~ text ~|\n  # +------+\n  # ```\n  #\n  # WARNING: Customizing this format can mess with the formatting of the whole table.\n  def cell_row_format(format : String) : self\n    @cell_row_format = format.to_s\n\n    self\n  end\n\n  # Sets the `sprintf` format string used for cell contents, defaulting to `\" %s \"`.\n  #\n  # For example, if set to `\" =%s= \"` with the cell's content being `text`:\n  #\n  # ```text\n  # +--------+\n  # | =text= |\n  # +--------+\n  # ```\n  def cell_row_content_format(format : String) : self\n    @cell_row_content_format = format.to_s\n\n    self\n  end\n\n  protected def pad(string : String, width : Int32, padding_char) : String\n    case @align\n    in .left?   then string.ljust width, padding_char\n    in .right?  then string.rjust width, padding_char\n    in .center? then string.center width, padding_char\n    end\n  end\n\n  # Sets the horizontal border chars, defaulting to `\"-\"`.\n  #\n  # *inside* defaults to *outside* if not provided.\n  #\n  # For example:\n  #\n  # ```\n  # ╔═══════════════╤══════════════════════════╤══════════════════╗\n  # 1 ISBN          2 Title                    │ Author           ║\n  # ╠═══════════════╪══════════════════════════╪══════════════════╣\n  # ║ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  ║\n  # ║ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  ║\n  # ║ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien ║\n  # ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  ║\n  # ╚═══════════════╧══════════════════════════╧══════════════════╝\n  # ```\n  #\n  # Legend:\n  #\n  # * #1 *outside*\n  # * #2 *inside*\n  def horizontal_border_chars(outside : String | Char, inside : String | Char | Nil = nil) : self\n    @horizontal_outside_border_char = outside.to_s\n    @horizontal_inside_border_char = inside.try &.to_s || outside.to_s\n\n    self\n  end\n\n  # Sets the vertical border chars, defaulting to `\"|\"`.\n  #\n  # *inside* defaults to *outside* if not provided.\n  #\n  # For example:\n  #\n  # ```\n  # ╔═══════════════╤══════════════════════════╤══════════════════╗\n  # ║ ISBN          │ Title                    │ Author           ║\n  # ╠═══════1═══════╪══════════════════════════╪══════════════════╣\n  # ║ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  ║\n  # ║ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  ║\n  # ╟───────2───────┼──────────────────────────┼──────────────────╢\n  # ║ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien ║\n  # ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  ║\n  # ╚═══════════════╧══════════════════════════╧══════════════════╝\n  # ```\n  #\n  # Legend:\n  #\n  # * #1 *outside*\n  # * #2 *inside*\n  def vertical_border_chars(outside : String | Char, inside : String | Char | Nil = nil) : self\n    @vertical_outside_border_char = outside.to_s\n    @vertical_inside_border_char = inside.try &.to_s || outside.to_s\n\n    self\n  end\n\n  protected def border_chars : Tuple(String, String, String, String)\n    {\n      @horizontal_outside_border_char,\n      @vertical_outside_border_char,\n      @horizontal_inside_border_char,\n      @vertical_inside_border_char,\n    }\n  end\n\n  # Sets the crossing characters individually, defaulting to `\"+\"`.\n  # See `#default_crossing_char(char)` to default them all to a single character.\n  #\n  # ```\n  # 1═══════════════2══════════════════════════2══════════════════3\n  # ║ ISBN          │ Title                    │ Author           ║\n  # 8═══════════════0══════════════════════════0══════════════════4\n  # ║ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  ║\n  # ║ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  ║\n  # 8───────────────0──────────────────────────0──────────────────4\n  # ║ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien ║\n  # ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  ║\n  # 7═══════════════6══════════════════════════6══════════════════5\n  # ```\n  #\n  # Legend:\n  #\n  # * #0 *cross*\n  # * #1 *top_left*\n  # * #2 *top_middle*\n  # * #3 *top_right*\n  # * #4 *middle_right*\n  # * #5 *bottom_right*\n  # * #6 *bottom_middle*\n  # * #7 *bottom_left*\n  # * #8 *middle_left*\n  #\n  # * #8 *top_left_bottom* - defaults to *middle_left* if `nil`\n  # * #0 *top_middle_bottom* - defaults to *cross* if `nil`\n  # * #4 *top_right_bottom* - defaults to *middle_right* if `nil`\n  def crossing_chars(\n    cross : String | Char,\n    top_left : String | Char,\n    top_middle : String | Char,\n    top_right : String | Char,\n    middle_right : String | Char,\n    bottom_right : String | Char,\n    bottom_middle : String | Char,\n    bottom_left : String | Char,\n    middle_left : String | Char,\n    top_left_bottom : String | Char | Nil = nil,\n    top_middle_bottom : String | Char | Nil = nil,\n    top_right_bottom : String | Char | Nil = nil,\n  ) : self\n    @crossing_char = cross.to_s\n    @crossing_top_left_char = top_left.to_s\n    @crossing_top_middle_char = top_middle.to_s\n    @crossing_top_right_char = top_right.to_s\n    @crossing_middle_right_char = middle_right.to_s\n    @crossing_bottom_right_char = bottom_right.to_s\n    @crossing_bottom_middle_char = bottom_middle.to_s\n    @crossing_bottom_left_char = bottom_left.to_s\n    @crossing_middle_left_char = middle_left.to_s\n\n    @crossing_top_left_bottom_char = top_left_bottom.try &.to_s || middle_left.to_s\n    @crossing_top_middle_bottom_char = top_middle_bottom.try &.to_s || cross.to_s\n    @crossing_top_right_bottom_char = top_right_bottom.try &.to_s || middle_right.to_s\n\n    self\n  end\n\n  # Sets the default character used for each cross type.\n  #\n  # See `#crossing_chars`.\n  def default_crossing_char(char : String | Char) : self\n    self\n      .crossing_chars(\n        char,\n        char,\n        char,\n        char,\n        char,\n        char,\n        char,\n        char,\n        char,\n      )\n\n    self\n  end\n\n  protected def crossing_chars : Tuple(String, String, String, String, String, String, String, String, String, String, String, String)\n    {\n      @crossing_char,\n      @crossing_top_left_char,\n      @crossing_top_middle_char,\n      @crossing_top_right_char,\n      @crossing_middle_right_char,\n      @crossing_bottom_right_char,\n      @crossing_bottom_middle_char,\n      @crossing_bottom_left_char,\n      @crossing_middle_left_char,\n      @crossing_top_left_bottom_char,\n      @crossing_top_middle_bottom_char,\n      @crossing_top_right_bottom_char,\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/input/argument.cr",
    "content": "# Represents a value (or array of values) provided to a command as a ordered positional argument,\n# that can either be required or optional, optionally with a default value and/or description.\n#\n# Arguments are strings separated by spaces that come _after_ the command name.\n# For example, `./console test arg1 \"Arg2 with spaces\"`.\n#\n# Arguments can be added via the `ACON::Command#argument` method,\n# or by instantiating one manually as part of an `ACON::Input::Definition`.\n# The value of the argument could then be accessed via one of the `ACON::Input::Interface#argument` overloads.\n#\n# See `ACON::Input::Interface` for more examples on how arguments/options are parsed, and how they can be accessed.\nclass Athena::Console::Input::Argument\n  @[Flags]\n  # Represents the possible modes of an `ACON::Input::Argument`,\n  # that describe the \"type\" of the argument.\n  #\n  # Modes can also be combined using the [Enum.[]](https://crystal-lang.org/api/Enum.html#%5B%5D%28%2Avalues%29-macro) macro.\n  # For example, `ACON::Input::Argument::Mode[:required, :is_array]` which defines a required array argument.\n  enum Mode\n    # Represents a required argument that _MUST_ be provided.\n    # Otherwise the command will not run.\n    REQUIRED\n\n    # Represents an optional argument that could be omitted.\n    OPTIONAL\n\n    # Represents an argument that accepts a variable amount of values.\n    # Arguments of this type must be last.\n    IS_ARRAY\n  end\n\n  # Returns the name of the `self`.\n  getter name : String\n\n  # Returns the `ACON::Input::Argument::Mode` of `self`.\n  getter mode : ACON::Input::Argument::Mode\n\n  # Returns the description of `self`.\n  getter description : String\n\n  @default : ACON::Input::Value? = nil\n  @suggested_values : Array(String) | Proc(ACON::Completion::Input, Array(String)) | Nil\n\n  def initialize(\n    @name : String,\n    @mode : ACON::Input::Argument::Mode = :optional,\n    @description : String = \"\",\n    default = nil,\n    @suggested_values : Array(String) | Proc(ACON::Completion::Input, Array(String)) | Nil = nil,\n  )\n    raise ACON::Exception::InvalidArgument.new \"An argument name cannot be blank.\" if name.blank?\n\n    self.default = default\n  end\n\n  # Returns the default value of `self`, if any.\n  def default\n    @default.try do |value|\n      case value\n      when ACON::Input::Value::Array\n        value.value.map &.value\n      else\n        value.value\n      end\n    end\n  end\n\n  # Returns the default value of `self`, if any, converted to the provided *type*.\n  def default(type : T.class) : T forall T\n    {% if T.nilable? %}\n      self.default.as T\n    {% else %}\n      @default.not_nil!.get T\n    {% end %}\n  end\n\n  # Sets the default value of `self`.\n  def default=(default = nil)\n    raise ACON::Exception::Logic.new \"Cannot set a default value when the argument is required.\" if @mode.required? && !default.nil?\n\n    if @mode.is_array?\n      if default.nil?\n        return @default = ACON::Input::Value::Array.new\n      elsif !default.is_a? Array\n        raise ACON::Exception::Logic.new \"Default value for an array argument must be an array.\"\n      end\n    end\n\n    @default = ACON::Input::Value.from_value default\n  end\n\n  # Returns `true` if this argument is able to suggest values, otherwise `false`\n  def has_completion? : Bool\n    !@suggested_values.nil?\n  end\n\n  # Determines what values should be added to the possible *suggestions* based on the provided *input*.\n  def complete(input : ACON::Completion::Input, suggestions : ACON::Completion::Suggestions) : Nil\n    return unless values = @suggested_values\n\n    if values.is_a?(Proc)\n      values = values.call input\n    end\n\n    suggestions.suggest_values values\n  end\n\n  # Returns `true` if `self` is a required argument, otherwise `false`.\n  def required? : Bool\n    @mode.required?\n  end\n\n  # Returns `true` if `self` expects an array of values, otherwise `false`.\n  # ameba:disable Naming/PredicateName\n  def is_array? : Bool\n    @mode.is_array?\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/input/argv.cr",
    "content": "# An `ACON::Input::Interface` based on [ARGV](https://crystal-lang.org/api/toplevel.html#ARGV).\nclass Athena::Console::Input::ARGV < Athena::Console::Input\n  @tokens : Array(String)\n  @parsed : Array(String) = [] of String\n\n  def self.new(*tokens : String)\n    new tokens.to_a\n  end\n\n  def initialize(@tokens : Array(String) = ::ARGV, definition : ACON::Input::Definition? = nil)\n    super definition\n  end\n\n  # :inherit:\n  # ameba:disable Metrics/CyclomaticComplexity\n  def first_argument : String?\n    is_option = false\n\n    @tokens.each_with_index do |token, idx|\n      if !token.empty? && token.starts_with? '-'\n        next if token.includes?('=') || @tokens[idx + 1]?.nil?\n\n        name = '-' == token.char_at(1) ? token[2..] : token[-1..]\n\n        if !@options.has_key?(name) && !@definition.has_shortcut?(name)\n          # noop\n        elsif (@options.has_key?(name) || @options.has_key?(name = @definition.shortcut_to_name(name))) && @tokens[idx + 1]? == @options[name].value\n          is_option = true\n        end\n\n        next\n      end\n\n      if is_option\n        is_option = false\n        next\n      end\n\n      return token\n    end\n\n    nil\n  end\n\n  # :inherit:\n  def has_parameter?(*values : String, only_params : Bool = false) : Bool\n    @tokens.each do |token|\n      return false if only_params && \"--\" == token\n\n      values.each do |value|\n        leading = value.starts_with?(\"--\") ? \"#{value}=\" : value\n        return true if token == value || (!leading.empty? && token.starts_with? leading)\n      end\n    end\n\n    false\n  end\n\n  # :inherit:\n  def parameter(value : String, default : _ = false, only_params : Bool = false)\n    tokens = @tokens.dup\n\n    while token = tokens.shift?\n      return default if only_params && \"--\" == token\n      return tokens.shift? if token == value\n\n      leading = value.starts_with?(\"--\") ? \"#{value}=\" : value\n      return token[leading.size..] if !leading.empty? && token.starts_with? leading\n    end\n\n    default\n  end\n\n  # :inherit:\n  def to_s(io : IO) : Nil\n    @tokens.join io, \" \" do |token, join_io|\n      if match = token.match /^(-[^=]+=)(.+)/\n        join_io << match[1]\n        join_io << self.escape_token match[2]\n        next\n      end\n\n      if !token.empty? && '-' != token[0]\n        join_io << self.escape_token token\n        next\n      end\n\n      join_io << token\n    end\n  end\n\n  protected def parse : Nil\n    parse_options = true\n    @parsed = @tokens.dup\n\n    while token = @parsed.shift?\n      parse_options = self.parse_token token, parse_options\n    end\n  end\n\n  protected def parse_token(token : String, parse_options : Bool) : Bool\n    if parse_options && token.empty?\n      self.parse_argument token\n    elsif parse_options && \"--\" == token\n      return false\n    elsif parse_options && token.starts_with? \"--\"\n      self.parse_long_option token\n    elsif parse_options && token.starts_with?('-') && \"-\" != token\n      self.parse_short_option token\n    else\n      self.parse_argument token\n    end\n\n    parse_options\n  end\n\n  private def parse_argument(token : String) : Nil\n    count = @arguments.size\n\n    # If expecting another argument, add it.\n    if @definition.has_argument? count\n      argument = @definition.argument count\n      @arguments[argument.name] = argument.is_array? ? ACON::Input::Value::Array.new(token) : ACON::Input::Value.from_value token\n\n      # If the last argument IS_ARRAY, append token to last argument.\n    elsif @definition.has_argument?(count - 1) && @definition.argument(count - 1).is_array?\n      argument = @definition.argument(count - 1)\n      @arguments[argument.name].as(ACON::Input::Value::Array) << token\n\n      # TODO: Handle unexpected argument.\n    else\n    end\n  end\n\n  private def parse_long_option(token : String) : Nil\n    name = token.lchop \"--\"\n\n    if pos = name.index '='\n      if (value = name[(pos + 1)..]).empty?\n        @parsed.unshift value\n      end\n\n      self.add_long_option name[0, pos], value\n    else\n      self.add_long_option name, nil\n    end\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def add_long_option(name : String, value : String?) : Nil\n    unless @definition.has_option?(name)\n      raise ACON::Exception::Runtime.new \"The '--#{name}' option does not exist.\" unless @definition.has_negation? name\n\n      option_name = @definition.negation_to_name name\n      raise ACON::Exception::Runtime.new \"The '--#{name}' option does not accept a value.\" unless value.nil?\n\n      return @options[option_name] = ACON::Input::Value.from_value false\n    end\n\n    option = @definition.option name\n\n    if !value.nil? && !option.accepts_value?\n      raise ACON::Exception::Runtime.new \"The --#{option.name} option does not accept a value.\"\n    end\n\n    if value.in?(\"\", nil) && option.accepts_value? && !@parsed.empty?\n      next_value = @parsed.shift?\n\n      if ((v = next_value.presence) && '-' != v.char_at(0)) || next_value.in?(\"\", nil)\n        value = next_value\n      else\n        @parsed.unshift next_value || \"\"\n      end\n    end\n\n    if value.nil?\n      raise ACON::Exception::Runtime.new \"The --#{option.name} option requires a value.\" if option.value_required?\n      value = true if !option.is_array? && !option.value_optional?\n    end\n\n    if option.is_array?\n      (@options[name] ||= ACON::Input::Value::Array.new).as(ACON::Input::Value::Array) << value\n    else\n      @options[name] = ACON::Input::Value.from_value value\n    end\n  end\n\n  private def parse_short_option(token : String) : Nil\n    name = token.lchop '-'\n\n    if name.size > 1\n      if @definition.has_shortcut?(name[0]) && @definition.option_for_shortcut(name[0]).accepts_value?\n        # Option with a value & no space\n        self.add_short_option name[0], name[1..]\n      else\n        self.parse_short_option_set name\n      end\n    else\n      self.add_short_option name, nil\n    end\n  end\n\n  private def parse_short_option_set(name : String) : Nil\n    length = name.size\n    name.each_char_with_index do |char, idx|\n      raise ACON::Exception::Runtime.new \"The -#{char} option does not exist.\" unless @definition.has_shortcut? char\n\n      option = @definition.option_for_shortcut char\n\n      if option.accepts_value?\n        self.add_long_option option.name, idx == length - 1 ? nil : name[(idx + 1)..]\n\n        break\n      else\n        self.add_long_option option.name, nil\n      end\n    end\n  end\n\n  private def add_short_option(name : String | Char, value : String?) : Nil\n    name = name.to_s\n\n    raise ACON::Exception::Runtime.new \"The -#{name} option does not exist.\" if !@definition.has_shortcut? name\n\n    self.add_long_option @definition.option_for_shortcut(name).name, value\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/input/definition.cr",
    "content": "# Represents a collection of `ACON::Input::Argument`s and `ACON::Input::Option`s that are to be parsed from an `ACON::Input::Interface`.\n#\n# Can be used to set the inputs of an `ACON::Command` via the `ACON::Command#definition=` method if so desired,\n# instead of using the dedicated methods.\nclass Athena::Console::Input::Definition\n  getter options : ::Hash(String, ACON::Input::Option) = ::Hash(String, ACON::Input::Option).new\n  getter arguments : ::Hash(String, ACON::Input::Argument) = ::Hash(String, ACON::Input::Argument).new\n\n  @last_array_argument : ACON::Input::Argument? = nil\n  @last_optional_argument : ACON::Input::Argument? = nil\n\n  @shortcuts = ::Hash(String, String).new\n  @negations = ::Hash(String, String).new\n\n  getter required_argument_count : Int32 = 0\n\n  def self.new(definition : ::Hash(String, ACON::Input::Option) | ::Hash(String, ACON::Input::Argument)) : self\n    new definition.values\n  end\n\n  def self.new(*definitions : ACON::Input::Argument | ACON::Input::Option) : self\n    new definitions.to_a\n  end\n\n  def initialize(definition : Array(ACON::Input::Argument | ACON::Input::Option) = Array(ACON::Input::Argument | ACON::Input::Option).new)\n    self.definition = definition\n  end\n\n  # Adds the provided *argument* to `self`.\n  def <<(argument : ACON::Input::Argument) : Nil\n    raise ACON::Exception::Logic.new \"An argument with the name '#{argument.name}' already exists.\" if @arguments.has_key?(argument.name)\n\n    if last_array_argument = @last_array_argument\n      raise ACON::Exception::Logic.new \"Cannot add a required argument '#{argument.name}' after Array argument '#{last_array_argument.name}'.\"\n    end\n\n    if argument.required? && (last_optional_argument = @last_optional_argument)\n      raise ACON::Exception::Logic.new \"Cannot add required argument '#{argument.name}' after the optional argument '#{last_optional_argument.name}'.\"\n    end\n\n    if argument.is_array?\n      @last_array_argument = argument\n    end\n\n    if argument.required?\n      @required_argument_count += 1\n    else\n      @last_optional_argument = argument\n    end\n\n    @arguments[argument.name] = argument\n  end\n\n  # Adds the provided *options* to `self`.\n  def <<(option : ACON::Input::Option) : Nil\n    if self.has_option?(option.name) && option != self.option(option.name)\n      raise ACON::Exception::Logic.new \"An option named '#{option.name}' already exists.\"\n    end\n\n    if self.has_negation?(option.name)\n      raise ACON::Exception::Logic.new \"An option named '#{option.name}' already exists.\"\n    end\n\n    if shortcut = option.shortcut\n      shortcut.split('|', remove_empty: true) do |s|\n        if self.has_shortcut?(s) && option != self.option_for_shortcut(s)\n          raise ACON::Exception::Logic.new \"An option with shortcut '#{s}' already exists.\"\n        end\n      end\n    end\n\n    @options[option.name] = option\n\n    if shortcut\n      shortcut.split('|', remove_empty: true) do |s|\n        @shortcuts[s] = option.name\n      end\n    end\n\n    if option.negatable?\n      negated_name = \"no-#{option.name}\"\n\n      raise ACON::Exception::Logic.new \"An option named '#{negated_name}' already exists.\" if self.has_option? negated_name\n\n      @negations[negated_name] = option.name\n    end\n  end\n\n  # Adds the provided *arguments* to `self`.\n  def <<(arguments : Array(ACON::Input::Argument | ACON::Input::Option)) : Nil\n    arguments.each do |arg|\n      self.<< arg\n    end\n  end\n\n  # Overrides the arguments and options of `self` to those in the provided *definition*.\n  def definition=(definition : Array(ACON::Input::Argument | ACON::Input::Option)) : Nil\n    arguments = Array(ACON::Input::Argument).new\n    options = Array(ACON::Input::Option).new\n\n    definition.each do |d|\n      case d\n      in ACON::Input::Argument then arguments << d\n      in ACON::Input::Option   then options << d\n      end\n    end\n\n    self.arguments = arguments\n    self.options = options\n  end\n\n  # Overrides the arguments of `self` to those in the provided *arguments* array.\n  def arguments=(arguments : Array(ACON::Input::Argument)) : Nil\n    @arguments.clear\n    @required_argument_count = 0\n    @last_array_argument = nil\n    @last_optional_argument = nil\n\n    self.<< arguments\n  end\n\n  # Returns the `ACON::Input::Argument` with the provided *name_or_index*,\n  # otherwise raises `ACON::Exception::InvalidArgument` if that argument is not defined.\n  def argument(name_or_index : String | Int32) : ACON::Input::Argument\n    raise ACON::Exception::InvalidArgument.new \"The argument '#{name_or_index}' does not exist.\" unless self.has_argument? name_or_index\n\n    case name_or_index\n    in String then @arguments[name_or_index]\n    in Int32  then @arguments.values[name_or_index]\n    end\n  end\n\n  # Returns `true` if `self` has an argument with the provided *name_or_index*.\n  def has_argument?(name_or_index : String | Int32) : Bool\n    case name_or_index\n    in String then @arguments.has_key? name_or_index\n    in Int32  then !@arguments.values.[name_or_index]?.nil?\n    end\n  end\n\n  # Returns the number of `ACON::Input::Argument`s defined within `self`.\n  def argument_count : Int32\n    !@last_array_argument.nil? ? Int32::MAX : @arguments.size\n  end\n\n  # Returns a `::Hash` whose keys/values represent the names and default values of the `ACON::Input::Argument`s defined within `self`.\n  def argument_defaults : ::Hash\n    @arguments.to_h do |(name, arg)|\n      {name, arg.default}\n    end\n  end\n\n  # Overrides the options of `self` to those in the provided *options* array.\n  def options=(options : Array(ACON::Input::Option)) : Nil\n    @options.clear\n    @shortcuts.clear\n    @negations.clear\n\n    self.<< options\n  end\n\n  # Returns the `ACON::Input::Option` with the provided *name_or_index*,\n  # otherwise raises `ACON::Exception::InvalidArgument` if that option is not defined.\n  def option(name_or_index : String | Int32) : ACON::Input::Option\n    raise ACON::Exception::InvalidArgument.new \"The '--#{name_or_index}' option does not exist.\" unless self.has_option? name_or_index\n\n    case name_or_index\n    in String then @options[name_or_index]\n    in Int32  then @options.values[name_or_index]\n    end\n  end\n\n  # Returns a `::Hash` whose keys/values represent the names and default values of the `ACON::Input::Option`s defined within `self`.\n  def option_defaults : ::Hash\n    @options.to_h do |(name, opt)|\n      {name, opt.default}\n    end\n  end\n\n  # Returns `true` if `self` has an option with the provided *name_or_index*.\n  def has_option?(name_or_index : String | Int32) : Bool\n    case name_or_index\n    in String then @options.has_key? name_or_index\n    in Int32  then !@options.values.[name_or_index]?.nil?\n    end\n  end\n\n  # Returns `true` if `self` has a shortcut with the provided *name*, otherwise `false`.\n  def has_shortcut?(name : String | Char) : Bool\n    @shortcuts.has_key? name.to_s\n  end\n\n  # Returns `true` if `self` has a negation with the provided *name*, otherwise `false`.\n  def has_negation?(name : String | Char) : Bool\n    @negations.has_key? name.to_s\n  end\n\n  # Returns the name of the `ACON::Input::Option` that maps to the provided *negation*.\n  def negation_to_name(negation : String) : String\n    raise ACON::Exception::InvalidArgument.new \"The '--#{negation}' option does not exist.\" unless self.has_negation? negation\n\n    @negations[negation]\n  end\n\n  # Returns the name of the `ACON::Input::Option` with the provided *shortcut*.\n  def option_for_shortcut(shortcut : String | Char) : ACON::Input::Option\n    self.option self.shortcut_to_name shortcut.to_s\n  end\n\n  # Returns an optionally *short* synopsis based on the `ACON::Input::Argument`s and `ACON::Input::Option`s defined within `self`.\n  #\n  # The synopsis being the [docopt](http://docopt.org) string representing the expected options/arguments.\n  # E.g. `<name> move <x> <y> [--speed=<kn>]`.\n  # ameba:disable Metrics/CyclomaticComplexity\n  def synopsis(short : Bool = false) : String\n    elements = [] of String\n\n    if short && !@options.empty?\n      elements << \"[options]\"\n    elsif !short\n      @options.each_value do |opt|\n        value = \"\"\n\n        if opt.accepts_value?\n          value = sprintf(\n            \" %s%s%s\",\n            opt.value_optional? ? \"[\" : \"\",\n            opt.name.upcase,\n            opt.value_optional? ? \"]\" : \"\",\n          )\n        end\n\n        shortcut = (s = opt.shortcut) ? sprintf(\"-%s|\", s) : \"\"\n        negation = opt.negatable? ? sprintf(\"|--no-%s\", opt.name) : \"\"\n\n        elements << \"[#{shortcut}--#{opt.name}#{value}#{negation}]\"\n      end\n    end\n\n    if !elements.empty? && !@arguments.empty?\n      elements << \"[--]\"\n    end\n\n    tail = \"\"\n\n    @arguments.each_value do |arg|\n      element = \"<#{arg.name}>\"\n      element += \"...\" if arg.is_array?\n\n      unless arg.required?\n        element = \"[#{element}\"\n        tail += \"]\"\n      end\n\n      elements << element\n    end\n\n    %(#{elements.join \" \"}#{tail})\n  end\n\n  protected def shortcut_to_name(shortcut : String) : String\n    raise ACON::Exception::InvalidArgument.new \"The '-#{shortcut}' option does not exist.\" unless self.has_shortcut? shortcut\n\n    @shortcuts[shortcut]\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/input/hash.cr",
    "content": "# An `ACON::Input::Interface` based on a [Hash](https://crystal-lang.org/api/Hash.html).\n#\n# Primarily useful for manually invoking commands, or as part of tests.\n#\n# ```\n# ACON::Input::Hash.new(name: \"George\", \"--foo\": \"bar\")\n# ```\n#\n# The keys of the input should be the name of the argument.\n# Options should have `--` prefixed to their name.\nclass Athena::Console::Input::Hash < Athena::Console::Input\n  @parameters : ::Hash(String, ACON::Input::Value)\n\n  def self.new(*args : _) : self\n    new args\n  end\n\n  def self.new(**args : _) : self\n    new args.to_h\n  end\n\n  def initialize(args : ::Hash = ::Hash(NoReturn, NoReturn).new, definition : ACON::Input::Definition? = nil)\n    hash = ::Hash(String, ACON::Input::Value).new\n\n    args.each do |key, value|\n      hash[key.to_s] = ACON::Input::Value.from_value value\n    end\n\n    @parameters = hash\n\n    super definition\n  end\n\n  def initialize(args : Enumerable, definition : ACON::Input::Definition? = nil)\n    hash = ::Hash(String, ACON::Input::Value).new\n\n    args.each do |arg|\n      hash[arg.to_s] = ACON::Input::Value::Nil.new\n    end\n\n    @parameters = hash\n\n    super definition\n  end\n\n  # :inherit:\n  def first_argument : String?\n    @parameters.each do |name, value|\n      next if name.starts_with? '-'\n\n      return value.value.as(String)\n    end\n\n    nil\n  end\n\n  # :inherit:\n  def has_parameter?(*values : String, only_params : Bool = false) : Bool\n    @parameters.each do |name, value|\n      value = value.value\n      value = name unless value.is_a? Number\n      return false if only_params && \"--\" == value\n      return true if values.includes? value\n    end\n\n    false\n  end\n\n  # :inherit:\n  def parameter(value : String, default : _ = false, only_params : Bool = false)\n    @parameters.each do |name, v|\n      return default if only_params && (\"--\" == name || \"--\" == value)\n      return v.value if value == name\n    end\n\n    default\n  end\n\n  # :inherit:\n  def to_s(io : IO) : Nil\n    params = [] of String\n\n    @parameters.each do |param, val|\n      if param[0] == '-'\n        separator = param[1] == '-' ? '=' : ' '\n\n        if val.is_a? ACON::Input::Value::Array\n          val.value.each do |v|\n            params << %(#{param}#{v.value != \"\" && !val.value.nil? ? %<#{separator}#{self.escape_token v}> : \"\"})\n          end\n        else\n          params << %(#{param}#{val.value != \"\" && !val.value.nil? ? %<#{separator}#{self.escape_token val}> : \"\"})\n        end\n      else\n        params << self.escape_token val\n      end\n    end\n\n    params.join io, \" \"\n  end\n\n  protected def parse : Nil\n    @parameters.each do |name, value|\n      return if \"--\" == name\n\n      if name.starts_with? \"--\"\n        self.add_long_option name.lchop(\"--\"), value\n      elsif name.starts_with? '-'\n        self.add_short_option name.lchop('-'), value\n      else\n        self.add_argument name, value\n      end\n    end\n  end\n\n  private def add_argument(name : String, value : ACON::Input::Value) : Nil\n    raise ACON::Exception::InvalidArgument.new \"The '#{name}' argument does not exist.\" if !@definition.has_argument? name\n\n    @arguments[name] = value\n  end\n\n  private def add_long_option(name : String, value : ACON::Input::Value) : Nil\n    unless @definition.has_option?(name)\n      raise ACON::Exception::InvalidOption.new \"The '--#{name}' option does not exist.\" unless @definition.has_negation? name\n\n      option_name = @definition.negation_to_name name\n      return @options[option_name] = ACON::Input::Value::Bool.new false\n    end\n\n    option = @definition.option name\n\n    if value.is_a? ACON::Input::Value::Nil\n      raise ACON::Exception::InvalidOption.new \"The '--#{option.name}' option requires a value.\" if option.value_required?\n      value = ACON::Input::Value::Bool.new(true) if !option.is_array? && !option.value_optional?\n    end\n\n    @options[name] = value\n  end\n\n  private def add_short_option(name : String, value : ACON::Input::Value) : Nil\n    name = name.to_s\n\n    raise ACON::Exception::InvalidOption.new \"The '-#{name}' option does not exist.\" if !@definition.has_shortcut? name\n\n    self.add_long_option @definition.option_for_shortcut(name).name, value\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/input/input.cr",
    "content": "require \"./interface\"\nrequire \"./streamable\"\n\nrequire \"./value/*\"\n\n# Common base implementation of `ACON::Input::Interface`.\nabstract class Athena::Console::Input\n  include Athena::Console::Input::Streamable\n\n  # :inherit:\n  property stream : IO? = nil\n\n  # :inherit:\n  property? interactive : Bool = true\n\n  @arguments = ::Hash(String, ACON::Input::Value).new\n  @definition : ACON::Input::Definition\n  @options = ::Hash(String, ACON::Input::Value).new\n\n  def initialize(definition : ACON::Input::Definition? = nil)\n    if definition.nil?\n      @definition = ACON::Input::Definition.new\n    else\n      @definition = definition\n      self.bind definition\n      self.validate\n    end\n  end\n\n  # :inherit:\n  def argument(name : String) : String?\n    raise ACON::Exception::InvalidArgument.new \"The '#{name}' argument does not exist.\" unless @definition.has_argument? name\n\n    value = if @arguments.has_key? name\n              @arguments[name]\n            else\n              @definition.argument(name).default\n            end\n\n    case value\n    when Nil, ACON::Input::Value::Nil then nil\n    else\n      value.to_s\n    end\n  end\n\n  # :inherit:\n  def argument(name : String, type : T.class) : T forall T\n    raise ACON::Exception::InvalidArgument.new \"The '#{name}' argument does not exist.\" unless @definition.has_argument? name\n\n    {% unless T.nilable? %}\n      if !@definition.argument(name).required? && @definition.argument(name).default.nil?\n        raise ACON::Exception::Logic.new \"Cannot cast optional argument '#{name}' to non-nilable type '#{T}' without a default.\"\n      end\n    {% end %}\n\n    if @arguments.has_key? name\n      return @arguments[name].get T\n    end\n\n    @definition.argument(name).default T\n  end\n\n  # :inherit:\n  def set_argument(name : String, value : _) : Nil\n    raise ACON::Exception::InvalidArgument.new \"The '#{name}' argument does not exist.\" unless @definition.has_argument? name\n\n    @arguments[name] = ACON::Input::Value.from_value value\n  end\n\n  # :inherit:\n  def arguments : ::Hash\n    @definition.argument_defaults.merge(self.resolve @arguments)\n  end\n\n  # :inherit:\n  def has_argument?(name : String) : Bool\n    @definition.has_argument? name\n  end\n\n  # :inherit:\n  def option(name : String) : String?\n    if @definition.has_negation?(name)\n      self.option(@definition.negation_to_name(name), Bool?).try do |v|\n        return (!v).to_s\n      end\n\n      return\n    end\n\n    raise ACON::Exception::InvalidArgument.new \"The '#{name}' option does not exist.\" unless @definition.has_option? name\n\n    value = if @options.has_key? name\n              @options[name]\n            else\n              @definition.option(name).default\n            end\n\n    case value\n    when Nil, ACON::Input::Value::Nil then nil\n    else\n      value.to_s\n    end\n  end\n\n  # :inherit:\n  def option(name : String, type : T.class) : T forall T\n    {% if T <= Bool? %}\n      if @definition.has_negation?(name)\n        negated_name = @definition.negation_to_name(name)\n\n        if @options.has_key? negated_name\n          return !@options[negated_name].get T\n        end\n\n        raise \"BUG: Didn't return negated value.\"\n      end\n    {% end %}\n\n    raise ACON::Exception::InvalidArgument.new \"The '#{name}' option does not exist.\" unless @definition.has_option? name\n\n    {% unless T <= Bool? %}\n      raise ACON::Exception::Logic.new \"Cannot cast negatable option '#{name}' to non 'Bool?' type.\" if @definition.option(name).negatable?\n    {% end %}\n\n    {% unless T.nilable? %}\n      if !@definition.option(name).value_required? && !@definition.option(name).negatable? && @definition.option(name).default.nil?\n        raise ACON::Exception::Logic.new \"Cannot cast optional option '#{name}' to non-nilable type '#{T}' without a default.\"\n      end\n    {% end %}\n\n    if @options.has_key? name\n      return @options[name].get T\n    end\n\n    @definition.option(name).default T\n  end\n\n  # :inherit:\n  def set_option(name : String, value : _) : Nil\n    if @definition.has_negation?(name)\n      return @options[@definition.negation_to_name(name)] = ACON::Input::Value.from_value !value\n    end\n\n    raise ACON::Exception::InvalidArgument.new \"The '#{name}' option does not exist.\" unless @definition.has_option? name\n\n    @options[name] = ACON::Input::Value.from_value value\n  end\n\n  # :inherit:\n  def options : ::Hash\n    @definition.option_defaults.merge(self.resolve @options)\n  end\n\n  # :inherit:\n  def has_option?(name : String) : Bool\n    @definition.has_option?(name) || @definition.has_negation?(name)\n  end\n\n  # :inherit:\n  def bind(definition : ACON::Input::Definition) : Nil\n    @arguments.clear\n    @options.clear\n    @definition = definition\n\n    self.parse\n  end\n\n  protected abstract def parse : Nil\n\n  # :inherit:\n  def validate : Nil\n    missing_args = @definition.arguments.keys.select do |arg|\n      !@arguments.has_key?(arg) && @definition.argument(arg).required?\n    end\n\n    raise ACON::Exception::Runtime.new %(Not enough arguments (missing: '#{missing_args.join(\", \")}').) unless missing_args.empty?\n  end\n\n  # Escapes a token via [Process.quote](https://crystal-lang.org/api/Process.html#quote%28arg%3AString%29%3AString-class-method) if it contains unsafe characters.\n  def escape_token(token : String) : String\n    Process.quote token\n  end\n\n  # :nodoc:\n  def escape_token(token : ACON::Input::Value::String) : String\n    self.escape_token token.value\n  end\n\n  # :nodoc:\n  def escape_token(token : ACON::Input::Value::Array) : String\n    token.value.join \" \" do |t|\n      self.escape_token t\n    end\n  end\n\n  # :nodoc:\n  def escape_token(token : ACON::Input::Value::Nil) : String\n    \"\"\n  end\n\n  # :nodoc:\n  def escape_token(token : _) : String\n    self.escape_token token.to_s\n  end\n\n  private def resolve(hash : ::Hash(String, ACON::Input::Value)) : ::Hash\n    hash.transform_values do |value|\n      case value\n      when ACON::Input::Value::Array\n        value.value.map &.value\n      else\n        value.value\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/input/interface.cr",
    "content": "require \"./definition\"\n\n# `Athena::Console` uses a dedicated interface for representing an input source.\n# This allows it to have multiple more specialized implementations as opposed to\n# being tightly coupled to `STDIN` or a raw [IO](https://crystal-lang.org/api/IO.html).\n# This interface represents the methods that _must_ be implemented, however implementations can add additional functionality.\n#\n# All input sources follow the [docopt](http://docopt.org) standard, used by many CLI utility tools.\n# Documentation on this type covers functionality/logic common to all inputs.\n# See each type for more specific information.\n#\n# Option and argument values can be accessed via `ACON::Input::Interface#option` and `ACON::Input::Interface#argument` respectively.\n# There are two overloads, the first accepting just the name of the option/argument as a `String`, returning the raw value as a `String?`,\n# with arrays being represented as a comma separated list.\n# The other two overloads accept a `T.class` representing the desired type the value should be parsed as.\n# For example, given a command with two required and one array arguments:\n#\n# ```\n# protected def configure : Nil\n#   self\n#     .argument(\"bool\", :required)\n#     .argument(\"int\", :required)\n#     .argument(\"floats\", :is_array)\n# end\n# ```\n#\n# Assuming the invocation is  `./console test false 10 3.14 172.0 123.7777`, the values could then be accessed like:\n#\n# ```\n# protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#   input.argument \"bool\"       # => \"false\" : String\n#   input.argument \"bool\", Bool # => false : Bool\n#   input.argument \"int\", Int8  # => 10 : Int8\n#\n#   input.argument \"floats\"                 # => \"3.14,172.0,123.7777\" : String\n#   input.argument \"floats\", Array(Float64) # => [3.14, 172.0, 123.7777] : Array(Float64)\n#\n#   ACON::Command::Status::SUCCESS\n# end\n# ```\n#\n# The latter syntax is preferred since it correctly types the value.\n# If a provided value cannot be converted to the expected type,\n# an `ACON::Exception::Logic` exception will be raised.\n# E.g. `'123' is not a valid 'Bool'.`.\n#\n# TIP: Argument/option modes can be combined.\n# E.g.`ACON::Input::Argument::Mode[:required, :is_array]` for a required array argument.\n#\n# There are a lot of possible combinations in regards to what options are defined versus those are provided.\n# To better illustrate how these cases are handled, let's look at an example of a command with three `ACON::Input::Option`s:\n#\n# ```\n# protected def configure : Nil\n#   self\n#     .option(\"foo\", \"f\")\n#     .option(\"bar\", \"b\", :required)\n#     .option(\"baz\", \"z\", :optional)\n# end\n# ```\n#\n# The value of `foo` will either be `true` if provided, otherwise `false`; this is the default behavior of `ACON::Input::Option`s.\n# The `bar` (`b`) option is required to have a value.\n# A value can be separated from the option's long name by either a space or `=` or by its short name by an optional space.\n# Finally, the `baz` (`z`) option's value is optional.\n#\n# This table shows how the value of each option based on the provided input:\n#\n# |        Input        |   foo   |    bar     |    baz     |\n# | :-----------------: | :-----: | :--------: | :--------: |\n# |    `--bar=Hello`    | `false` | `\"Hello\"`  |   `nil`    |\n# |    `--bar Hello`    | `false` | `\"Hello\"`  |   `nil`    |\n# |     `-b=Hello`      | `false` | `\"=Hello\"` |   `nil`    |\n# |     `-b Hello`      | `false` | `\"Hello\"`  |   `nil`    |\n# |      `-bHello`      | `false` | `\"Hello\"`  |   `nil`    |\n# | `-fzWorld -b Hello` | `true`  | `\"Hello\"`  | `\"World\"`  |\n# | `-zfWorld -b Hello` | `false` | `\"Hello\"`  | `\"fWorld\"` |\n# |     `-zbWorld`      | `false` |   `nil`    | `\"bWorld\"` |\n#\n# Things get a bit trickier when an optional `ACON::Input::Argument`:\n#\n# ```\n# protected def configure : Nil\n#   self\n#     .option(\"foo\", \"f\")\n#     .option(\"bar\", \"b\", :required)\n#     .option(\"baz\", \"z\", :optional)\n#     .argument(\"arg\", :optional)\n# end\n# ```\n#\n# In some cases you may need to use the special `--` option in order to denote later values should be parsed as arguments, not as a value to an option:\n#\n# |            Input             |      bar        |   baz     |   arg     |\n# | :--------------------------: | :-------------: | :-------: | :-------: |\n# |        `--bar Hello`         |    `\"Hello\"`    |   `nil`   |   `nil`   |\n# |     `--bar Hello World`      |    `\"Hello\"`    |   `nil`   | `\"World\"` |\n# |    `--bar \"Hello World\"`     | `\"Hello World\"` |   `nil`   |   `nil`   |\n# |  `--bar Hello --baz World`   |    `\"Hello\"`    | `\"World\"` |   `nil`   |\n# | `--bar Hello --baz -- World` |    `\"Hello\"`    |   `nil`   | `\"World\"` |\n# |     `-b Hello -z World`      |    `\"Hello\"`    | `\"World\"` |   `nil`   |\n#\n# ## Argument/Option Value Completion\n#\n# If the [completion script](/Console#console-completion) is installed, command and option names will be auto completed by the shell.\n# However, value completion may also be implemented in custom commands by providing the suggested values for a particular option/argument.\n#\n# ```\n# @[ACONA::AsCommand(\"greet\")]\n# class GreetCommand < ACON::Command\n#   protected def configure : Nil\n#     # The suggested values do not need to be a static array,\n#     # they could be sourced via a class/instance method, a constant, etc.\n#     self\n#       .argument(\"name\", suggested_values: [\"Jim\", \"Bob\", \"Sally\"])\n#   end\n#\n#   # ...\n# end\n# ```\n#\n# Additionally, a block version of `ACON::Command#argument(name,mode,description,default,&)` and `ACON::Command#option(name,shortcut,value_mode,description,default,&)` may be used if more complex logic is required.\n#\n# ```\n# @[ACONA::AsCommand(\"greet\")]\n# class GreetCommand < ACON::Command\n#   protected def configure : Nil\n#     self\n#       .argument(\"name\") do |input|\n#         # The value the user already typed, e.g. the value the user already typed,\n#         # e.g. when typing \"greet Ge\" before pressing Tab, this will contain \"Ge\".\n#         current_value = input.completion_value\n#\n#         # Get the list of username names from somewhere (e.g. the database)\n#         # you may use current_value to filter down the names\n#         available_usernames = ...\n#\n#         # then suggested the usernames as values\n#         return available_usernames\n#       end\n#   end\n#\n#   # ...\n# end\n# ```\n#\n# TIP: The shell completion script is able to handle huge amounts of suggestions and will automatically filter\n# the values based on existing input from the user.\n# You do not have to implement any filter logic in the command.\n# `input.completion_value` can still be used to filter if it helps with performance, such as reducing amount of rows the DB returns.\nmodule Athena::Console::Input::Interface\n  # Returns the first argument from the raw un-parsed input.\n  # Mainly used to get the command that should be executed.\n  abstract def first_argument : String?\n\n  # Returns `true` if the raw un-parsed input contains one of the provided *values*.\n  #\n  # This method is to be used to introspect the input parameters before they have been validated.\n  # It must be used carefully.\n  # It does not necessarily return the correct result for short options when multiple flags are combined in the same option.\n  #\n  # If *only_params* is `true`, only real parameters are checked. I.e. skipping those that come after the `--` option.\n  abstract def has_parameter?(*values : String, only_params : Bool = false) : Bool\n\n  # Returns the value of a raw un-parsed parameter for the provided *value*..\n  #\n  # This method is to be used to introspect the input parameters before they have been validated.\n  # It must be used carefully.\n  # It does not necessarily return the correct result for short options when multiple flags are combined in the same option.\n  #\n  # If *only_params* is `true`, only real parameters are checked. I.e. skipping those that come after the `--` option.\n  abstract def parameter(value : String, default : _ = false, only_params : Bool = false)\n\n  # Binds the provided *definition* to `self`.\n  # Essentially provides what should be parsed from `self`.\n  abstract def bind(definition : ACON::Input::Definition) : Nil\n\n  # Validates the input, asserting all of the required parameters are provided.\n  # Raises `ACON::Exception::Runtime` when not enough arguments are given.\n  abstract def validate : Nil\n\n  # Returns a `::Hash` representing the keys and values of the parsed arguments of `self`.\n  abstract def arguments : ::Hash\n\n  # Returns the raw string value of the argument with the provided *name*, or `nil` if is optional and was not provided.\n  abstract def argument(name : String) : String?\n\n  # Returns the value of the argument with the provided *name* converted to the desired *type*.\n  # This method is preferred over `#argument` since it provides better typing.\n  #\n  # Raises an `ACON::Exception::Logic` if the actual argument value could not be converted to a *type*.\n  abstract def argument(name : String, type : T.class) forall T\n\n  # Sets the *value* of the argument with the provided *name*.\n  abstract def set_argument(name : String, value : _) : Nil\n\n  # Returns `true` if `self` has an argument with the provided *name*, otherwise `false`.\n  abstract def has_argument?(name : String) : Bool\n\n  # Returns a `::Hash` representing the keys and values of the parsed options of `self`.\n  abstract def options : ::Hash\n\n  # Returns the raw string value of the option with the provided *name*, or `nil` if is optional and was not provided.\n  abstract def option(name : String) : String?\n\n  # Returns the value of the option with the provided *name* converted to the desired *type*.\n  # This method is preferred over `#option` since it provides better typing.\n  #\n  # Raises an `ACON::Exception::Logic` if the actual option value could not be converted to a *type*.\n  abstract def option(name : String, type : T.class) forall T\n\n  # Sets the *value* of the option with the provided *name*.\n  abstract def set_option(name : String, value : _) : Nil\n\n  # Returns `true` if `self` has an option with the provided *name*, otherwise `false`.\n  abstract def has_option?(name : String) : Bool\n\n  # Returns `true` if `self` represents an interactive input, such as a TTY.\n  abstract def interactive? : Bool\n\n  # Sets if `self` is `#interactive?`.\n  abstract def interactive=(interactive : Bool)\n\n  # Returns a string representation of the args passed to the command.\n  abstract def to_s(io : IO) : Nil\nend\n"
  },
  {
    "path": "src/components/console/src/input/option.cr",
    "content": "# Represents a value (or array of ) provided to a command as optional un-ordered flags\n# that be setup to accept a value, or represent a boolean flag.\n# Options can also have an optional shortcut, default value, and/or description.\n#\n# Options are specified with two dashes, or one dash when using the shortcut.\n# For example, `./console test --yell --dir=src -v`.\n# We have one option representing a boolean value, providing a value to another, and using the shortcut of another.\n#\n# Options can be added via the `ACON::Command#option` method,\n# or by instantiating one manually as part of an `ACON::Input::Definition`.\n# The value of the option could then be accessed via one of the `ACON::Input::Interface#option` overloads.\n#\n# See `ACON::Input::Interface` for more examples on how arguments/options are parsed, and how they can be accessed.\nclass Athena::Console::Input::Option\n  @[Flags]\n  # Represents the possible vale types of an `ACON::Input::Option`.\n  #\n  # Value modes can also be combined using the [Enum.[]](https://crystal-lang.org/api/Enum.html#%5B%5D%28%2Avalues%29-macro) macro.\n  # For example, `ACON::Input::Option::Value[:required, :is_array]` which defines a required array option.\n  enum Value\n    # Represents a boolean flag option that will be `true` if provided, otherwise `false`.\n    # E.g. `--yell`.\n    NONE = 0\n\n    # Represents an option that _MUST_ have a value if provided.\n    # The option itself is still optional.\n    # E.g. `--dir=src`.\n    REQUIRED\n\n    # Represents an option that _MAY_ have a value, but it is not a requirement.\n    # E.g. `--yell` or `--yell=loud`.\n    #\n    # When using the option value mode, it can be hard to distinguish between passing an option without a value and not passing it at all.\n    # In this case you should set the default of the option to `false`, instead of the default of `nil`.\n    # Then you would be able to tell it wasn't passed by the value being `false`, passed without a value as `nil`, and passed with a value.\n    #\n    # NOTE: In this context you will need to work with the raw `String?` representation of the value due to the union of types the value could be.\n    OPTIONAL\n\n    # Represents an option that can be provided multiple times to produce an array of values.\n    # E.g. `--dir=/foo --dir=/bar`.\n    IS_ARRAY\n\n    # Similar to `NONE`, but also accepts its negation.\n    # E.g. `--yell` or `--no-yell`.\n    NEGATABLE\n\n    def accepts_value? : Bool\n      self.required? || self.optional?\n    end\n  end\n\n  # Returns the name of `self`.\n  getter name : String\n\n  # Returns the shortcut of `self`, if any.\n  getter shortcut : String?\n\n  # Returns the `ACON::Input::Option::Value` of `self`.\n  getter value_mode : ACON::Input::Option::Value\n\n  # Returns the description of `self`.\n  getter description : String\n\n  @default : ACON::Input::Value? = nil\n  @suggested_values : Array(String) | Proc(ACON::Completion::Input, Array(String)) | Nil\n\n  def initialize(\n    name : String,\n    shortcut : String | Enumerable(String) | Nil = nil,\n    @value_mode : ACON::Input::Option::Value = :none,\n    @description : String = \"\",\n    default = nil,\n    @suggested_values : Array(String) | Proc(ACON::Completion::Input, Array(String)) | Nil = nil,\n  )\n    @name = name.lchop \"--\"\n\n    raise ACON::Exception::InvalidArgument.new \"An option name cannot be blank.\" if name.blank?\n\n    unless shortcut.nil?\n      if shortcut.is_a? Enumerable\n        shortcut = shortcut.join '|'\n      end\n\n      shortcut = shortcut.lchop('-').split(/(?:\\|)-?/, remove_empty: true).map(&.strip.lchop('-'))\n\n      # Ensure each grouping contains only the same character\n      shortcut.each do |s|\n        unless s.split(\"\").uniq!.size == 1\n          raise ACON::Exception::InvalidArgument.new \"An option shortcut must consist of the same character, got '#{s}'.\"\n        end\n      end\n\n      shortcut = shortcut.join '|'\n\n      raise ACON::Exception::InvalidArgument.new \"An option shortcut cannot be blank.\" if shortcut.blank?\n    end\n\n    @shortcut = shortcut\n\n    if @suggested_values && !self.accepts_value?\n      raise ACON::Exception::Logic.new \"Cannot set suggested values if the option does not accept a value.\"\n    end\n\n    if @value_mode.is_array? && !self.accepts_value?\n      raise ACON::Exception::InvalidArgument.new \" Cannot have VALUE::IS_ARRAY option mode when the option does not accept a value.\"\n    end\n\n    if @value_mode.negatable? && self.accepts_value?\n      raise ACON::Exception::InvalidArgument.new \" Cannot have VALUE::NEGATABLE option mode if the option also accepts a value.\"\n    end\n\n    self.default = default\n  end\n\n  def_equals @name, @shortcut, @default, @value_mode\n\n  # Returns the default value of `self`, if any.\n  def default\n    @default.try do |value|\n      case value\n      when ACON::Input::Value::Array\n        value.value.map &.value\n      else\n        value.value\n      end\n    end\n  end\n\n  # Returns the default value of `self`, if any, converted to the provided *type*.\n  def default(type : T.class) : T forall T\n    {% if T.nilable? %}\n      self.default.as T\n    {% else %}\n      @default.not_nil!.get T\n    {% end %}\n  end\n\n  # Sets the default value of `self`.\n  def default=(default = nil) : Nil\n    raise ACON::Exception::Logic.new \"Cannot set a default value when using Value::NONE mode.\" if @value_mode.none? && !default.nil?\n\n    if @value_mode.is_array?\n      if default.nil?\n        return @default = ACON::Input::Value::Array.new\n      else\n        raise ACON::Exception::Logic.new \"Default value for an array option must be an array.\" unless default.is_a? Array\n      end\n    end\n\n    @default = ACON::Input::Value.from_value (@value_mode.accepts_value? || @value_mode.negatable?) ? default : false\n  end\n\n  # Returns `true` if this option is able to suggest values, otherwise `false`\n  def has_completion? : Bool\n    !@suggested_values.nil?\n  end\n\n  # Determines what values should be added to the possible *suggestions* based on the provided *input*.\n  def complete(input : ACON::Completion::Input, suggestions : ACON::Completion::Suggestions) : Nil\n    return unless values = @suggested_values\n\n    if values.is_a?(Proc)\n      values = values.call input\n    end\n\n    suggestions.suggest_values values\n  end\n\n  # Returns `true` if `self` is able to accept a value, otherwise `false`.\n  def accepts_value? : Bool\n    @value_mode.accepts_value?\n  end\n\n  # Returns `true` if `self` is a required argument, otherwise `false`.\n  # ameba:disable Naming/PredicateName\n  def is_array? : Bool\n    @value_mode.is_array?\n  end\n\n  # Returns `true` if `self` is negatable, otherwise `false`.\n  def negatable? : Bool\n    @value_mode.negatable?\n  end\n\n  # Returns `true` if `self` accepts a value and it is required, otherwise `false`.\n  def value_required? : Bool\n    @value_mode.required?\n  end\n\n  # Returns `true` if `self` accepts a value but is optional, otherwise `false`.\n  def value_optional? : Bool\n    @value_mode.optional?\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/input/streamable.cr",
    "content": "# An extension of `ACON::Input::Interface` that supports input stream [IOs](https://crystal-lang.org/api/IO.html).\n#\n# Allows customizing where the input data is read from.\n# Defaults to `STDIN`.\nmodule Athena::Console::Input::Streamable\n  include Athena::Console::Input::Interface\n\n  # Returns the input stream.\n  abstract def stream : IO?\n\n  # Sets the input stream.\n  abstract def stream=(@stream : IO?)\nend\n"
  },
  {
    "path": "src/components/console/src/input/string_line.cr",
    "content": "# An `ACON::Input::Interface` based on a command line string.\nclass Athena::Console::Input::StringLine < Athena::Console::Input::ARGV\n  private REGEX_UNQUOTED_STRING = /([^\\s\\\\]+?)/\n  private REGEX_QUOTED_STRING   = /(?:\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"|\\'([^\\'\\\\]*(?:\\\\.[^\\'\\\\]*)*)\\')/\n\n  def initialize(input : String)\n    super [] of String\n\n    @tokens = self.tokenize input\n  end\n\n  private def tokenize(input : String) : Array(String)\n    tokens = [] of String\n    length = input.size\n    idx = 0\n    token = \"\"\n\n    while idx < length\n      if '\\\\' == input[idx]\n        idx += 1\n        token += input[idx]? || \"\"\n        idx += 1\n        next\n      end\n\n      match = if m = input.match /\\G\\s+/, idx\n                unless token.blank?\n                  tokens << token\n                  token = \"\"\n                end\n\n                m\n              elsif m = input.match /\\G([^=\"\\'\\s]+?)(=?)(#{REGEX_QUOTED_STRING}+)/, idx\n                token += %(#{m[1]}#{m[2]}#{m[3][1...-1].gsub(/(\"\\'|\\'\"|\\'\\'|\\\"\\\")/, \"\").gsub(/\\\\'/, {\"\\\\'\" => \"'\"})})\n                m\n              elsif m = input.match /\\G#{REGEX_QUOTED_STRING}/, idx\n                token += m[0][1...-1].gsub(/\\\\'/, {\"\\\\'\" => \"'\"})\n                m\n              elsif m = input.match /\\G#{REGEX_UNQUOTED_STRING}/, idx\n                token += m[1]\n                m\n              else\n                raise ACON::Exception::InvalidArgument.new \"Unable to parse input neat '... #{input[idx, 10]} ...'.\"\n              end\n\n      idx += match[0].size\n    end\n\n    tokens << token unless token.blank?\n\n    tokens\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/input/value/array.cr",
    "content": "abstract struct Athena::Console::Input::Value; end\n\n# :nodoc:\nstruct Athena::Console::Input::Value::Array < Athena::Console::Input::Value\n  getter value : ::Array(Athena::Console::Input::Value)\n\n  def self.from_array(array : ::Array) : self\n    new(array.map { |item| ACON::Input::Value.from_value item })\n  end\n\n  def self.new(value)\n    new [ACON::Input::Value.from_value value]\n  end\n\n  def self.new\n    new [] of ACON::Input::Value\n  end\n\n  def initialize(@value : ::Array(Athena::Console::Input::Value)); end\n\n  def <<(value)\n    @value << ACON::Input::Value.from_value value\n  end\n\n  def get(type : ::Array(T).class) : ::Array(T) forall T\n    arr = ::Array(T).new\n\n    @value.each do |v|\n      arr << v.get T\n    end\n\n    arr\n  end\n\n  def get(type : ::Array(T)?.class) : ::Array(T)? forall T\n    arr = ::Array(T).new\n\n    @value.each do |v|\n      arr << v.get T\n    end\n\n    arr || nil\n  end\n\n  def to_s(io : IO) : ::Nil\n    @value.join io, ','\n  end\n\n  # :nodoc:\n  forward_missing_to @value\nend\n"
  },
  {
    "path": "src/components/console/src/input/value/bool.cr",
    "content": "# :nodoc:\nstruct Athena::Console::Input::Value::Bool < Athena::Console::Input::Value\n  getter value : ::Bool\n\n  def initialize(@value : ::Bool); end\n\n  def get(type : ::Bool.class) : ::Bool\n    @value\n  end\n\n  def get(type : ::Bool?.class) : ::Bool?\n    @value.try do |v|\n      return v\n    end\n\n    nil\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/input/value/nil.cr",
    "content": "# :nodoc:\nstruct Athena::Console::Input::Value::Nil < Athena::Console::Input::Value\n  def value : ::Nil; end\nend\n"
  },
  {
    "path": "src/components/console/src/input/value/number.cr",
    "content": "# :nodoc:\nstruct Athena::Console::Input::Value::Number < Athena::Console::Input::Value\n  getter value : ::Number::Primitive\n\n  def initialize(@value : ::Number::Primitive); end\n\n  {% for type in ::Number::Primitive.union_types %}\n    def get(type : {{type.id}}.class) : {{type.id}}\n      {{type.id}}.new @value\n    end\n\n    def get(type : {{type.id}}?.class) : {{type.id}}?\n      {{type.id}}.new(@value) || nil\n    end\n  {% end %}\nend\n"
  },
  {
    "path": "src/components/console/src/input/value/string.cr",
    "content": "# :nodoc:\nstruct Athena::Console::Input::Value::String < Athena::Console::Input::Value\n  getter value : ::String\n\n  def initialize(@value : ::String); end\n\n  def get(type : ::Bool.class) : ::Bool\n    raise ACON::Exception::Logic.new \"'#{@value}' is not a valid 'Bool'.\" unless @value.in? \"true\", \"false\"\n\n    @value == \"true\"\n  end\n\n  def get(type : ::Bool?.class) : ::Bool?\n    (@value == \"true\").try do |v|\n      raise ACON::Exception::Logic.new \"'#{@value}' is not a valid 'Bool?'.\" unless @value.in? \"true\", \"false\"\n      return v\n    end\n\n    nil\n  end\n\n  def get(type : ::Array(T).class) : ::Array(T) forall T\n    Array.from_array(@value.split(',')).get ::Array(T)\n  end\n\n  def get(type : ::Array(T)?.class) : ::Array(T)? forall T\n    Array.from_array(@value.split(',')).get ::Array(T)?\n  end\n\n  {% for type in ::Number::Primitive.union_types %}\n    def get(type : {{type.id}}.class) : {{type.id}}\n      {{type.id}}.new @value\n    rescue ArgumentError\n      raise ACON::Exception::Logic.new \"'#{@value}' is not a valid '#{{{type.id}}}'.\"\n    end\n\n    def get(type : {{type.id}}?.class) : {{type.id}}?\n      {{type.id}}.new(@value) || nil\n    rescue ArgumentError\n      raise ACON::Exception::Logic.new \"'#{@value}' is not a valid '#{{{type.id}}}'.\"\n    end\n  {% end %}\nend\n"
  },
  {
    "path": "src/components/console/src/input/value/value.cr",
    "content": "# :nodoc:\nabstract struct Athena::Console::Input::Value\n  def self.from_value(value : T) : self forall T\n    case value\n    when ACON::Input::Value then value\n    when ::Nil              then ACON::Input::Value::Nil.new\n    when ::String           then ACON::Input::Value::String.new value\n    when ::Number           then ACON::Input::Value::Number.new value\n    when ::Bool             then ACON::Input::Value::Bool.new value\n    when ::Array            then ACON::Input::Value::Array.from_array value\n    else\n      raise \"Unsupported type: #{T}.\"\n    end\n  end\n\n  def get(type : ::String.class) : ::String\n    self.to_s\n  end\n\n  def get(type : ::String?.class) : ::String?\n    self.to_s.presence\n  end\n\n  def get(type : T.class) forall T\n    raise ACON::Exception::Logic.new \"'#{self.value}' is not a valid '#{T}'.\"\n  end\n\n  def to_s(io : IO) : ::Nil\n    self.value.to_s io\n  end\n\n  def inspect(io : IO) : ::Nil\n    io << \"#<Input::Value::\" << self.class.name.rpartition(\"::\").last << \">\"\n  end\n\n  abstract def value\nend\n"
  },
  {
    "path": "src/components/console/src/loader/factory.cr",
    "content": "require \"./interface\"\n\n# A default implementation of `ACON::Loader::Interface` that accepts a `Hash(String, Proc(ACON::Command))`.\n#\n# A factory could then be set on the `ACON::Application`:\n#\n# ```\n# application = MyCustomApplication.new \"My CLI\"\n#\n# application.command_loader = Athena::Console::Loader::Factory.new({\n#   \"command1\"        => Proc(ACON::Command).new { Command1.new },\n#   \"app:create-user\" => Proc(ACON::Command).new { CreateUserCommand.new },\n# })\n#\n# application.run\n# ```\nstruct Athena::Console::Loader::Factory\n  include Athena::Console::Loader::Interface\n\n  @factories : Hash(String, Proc(ACON::Command))\n\n  def initialize(@factories : Hash(String, Proc(ACON::Command))); end\n\n  # :inherit:\n  def get(name : String) : ACON::Command\n    if factory = @factories[name]?\n      factory.call\n    else\n      raise ACON::Exception::CommandNotFound.new \"Command '#{name}' does not exist.\"\n    end\n  end\n\n  # :inherit:\n  def has?(name : String) : Bool\n    @factories.has_key? name\n  end\n\n  # :inherit:\n  def names : Array(String)\n    @factories.keys\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/loader/interface.cr",
    "content": "# Normally the `ACON::Application#add` method requires instances of each command to be provided.\n# `ACON::Loader::Interface` provides a way to lazily instantiate only the command(s) being called,\n# which can be more performant since not every command needs instantiated.\nmodule Athena::Console::Loader::Interface\n  # Returns an `ACON::Command` with the provided *name*.\n  # Raises `ACON::Exception::CommandNotFound` if it is not defined.\n  abstract def get(name : String) : ACON::Command\n\n  # Returns `true` if `self` has a command with the provided *name*, otherwise `false`.\n  abstract def has?(name : String) : Bool\n\n  # Returns all of the command names defined within `self`.\n  abstract def names : Array(String)\nend\n"
  },
  {
    "path": "src/components/console/src/output/console_output.cr",
    "content": "abstract class Athena::Console::Output; end\n\nrequire \"./console_output_interface\"\nrequire \"./io\"\n\n# An `ACON::Output::ConsoleOutputInterface` that wraps `STDOUT` and `STDERR`.\nclass Athena::Console::Output::ConsoleOutput < Athena::Console::Output::IO\n  include Athena::Console::Output::ConsoleOutputInterface\n\n  # Sets the `ACON::Output::Interface` that represents `STDERR`.\n  setter stderr : ACON::Output::Interface\n  @console_section_outputs = Array(ACON::Output::Section).new\n\n  def initialize(\n    verbosity : ACON::Output::Verbosity = :normal,\n    decorated : Bool? = nil,\n    formatter : ACON::Formatter::Interface? = nil,\n  )\n    super STDOUT, verbosity, decorated, formatter\n\n    @stderr = ACON::Output::IO.new STDERR, verbosity, decorated, @formatter\n    actual_decorated = self.decorated?\n\n    if decorated.nil?\n      self.decorated = actual_decorated && @stderr.decorated?\n    end\n  end\n\n  # :inherit:\n  def section : ACON::Output::Section\n    ACON::Output::Section.new(\n      self.io,\n      @console_section_outputs,\n      self.verbosity,\n      self.decorated?,\n      self.formatter\n    )\n  end\n\n  # :inherit:\n  def error_output : ACON::Output::Interface\n    @stderr\n  end\n\n  # :inherit:\n  def error_output=(@stderr : ACON::Output::Interface) : Nil\n  end\n\n  # :inherit:\n  def decorated=(decorated : Bool) : Nil\n    super\n    @stderr.decorated = decorated\n  end\n\n  # :inherit:\n  def formatter=(formatter : Bool) : Nil\n    super\n    @stderr.formatter = formatter\n  end\n\n  # :inherit:\n  def verbosity=(verbosity : ACON::Output::Verbosity) : Nil\n    super\n    @stderr.verbosity = verbosity\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/output/console_output_interface.cr",
    "content": "require \"./interface\"\n\n# Extension of `ACON::Output::Interface` that adds additional functionality for terminal based outputs.\nmodule Athena::Console::Output::ConsoleOutputInterface\n  # include Athena::Console::Output::Interface\n\n  # Returns an `ACON::Output::Interface` that represents `STDERR`.\n  abstract def error_output : ACON::Output::Interface\n\n  # Sets the `ACON::Output::Interface` that represents `STDERR`.\n  abstract def error_output=(stderr : ACON::Output::Interface) : Nil\n\n  abstract def section : ACON::Output::Section\nend\n"
  },
  {
    "path": "src/components/console/src/output/interface.cr",
    "content": "# `Athena::Console` uses a dedicated interface for representing an output destination.\n# This allows it to have multiple more specialized implementations as opposed to\n# being tightly coupled to `STDOUT` or a raw [IO](https://crystal-lang.org/api/IO.html).\n# This interface represents the methods that _must_ be implemented, however implementations can add additional functionality.\n#\n# The most common implementations include `ACON::Output::ConsoleOutput` which is based on `STDOUT` and `STDERR`,\n# and `ACON::Output::Null` which can be used when you want to silent all output, such as for tests.\n#\n# Each output's `ACON::Output::Verbosity` and output `ACON::Output::Type` can also be configured on a per message basis.\nmodule Athena::Console::Output::Interface\n  # Outputs the provided *message* followed by a new line.\n  # The *verbosity* and/or *output_type* parameters can be used to control when and how the *message* is printed.\n  abstract def puts(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n\n  # Outputs the provided *message*.\n  # The *verbosity* and/or *output_type* parameters can be used to control when and how the *message* is printed.\n  abstract def print(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n\n  # Returns the minimum `ACON::Output::Verbosity` required for a message to be printed.\n  abstract def verbosity : ACON::Output::Verbosity\n\n  # Set the minimum `ACON::Output::Verbosity` required for a message to be printed.\n  abstract def verbosity=(verbosity : ACON::Output::Verbosity) : Nil\n\n  # Returns `true` if printed messages should have their decorations applied.\n  # I.e. `ACON::Formatter::OutputStyleInterface`.\n  abstract def decorated? : Bool\n\n  # Sets if printed messages should be *decorated*.\n  abstract def decorated=(decorated : Bool) : Nil\n\n  # Returns the `ACON::Formatter::Interface` used by `self`.\n  abstract def formatter : ACON::Formatter::Interface\n\n  # Sets the `ACON::Formatter::Interface` used by `self`.\n  abstract def formatter=(formatter : ACON::Formatter::Interface) : Nil\nend\n"
  },
  {
    "path": "src/components/console/src/output/io.cr",
    "content": "# An `ACON::Output::Interface` implementation that wraps an [IO](https://crystal-lang.org/api/IO.html).\nclass Athena::Console::Output::IO < Athena::Console::Output\n  property io : ::IO\n\n  delegate :to_s, to: @io\n\n  def initialize(\n    @io : ::IO,\n    verbosity : ACON::Output::Verbosity? = :normal,\n    decorated : Bool? = nil,\n    formatter : ACON::Formatter::Interface? = nil,\n  )\n    decorated = self.has_color_support? if decorated.nil?\n\n    super verbosity, decorated, formatter\n  end\n\n  protected def do_write(message : String, new_line : Bool) : Nil\n    message += EOL if new_line\n\n    @io.print message\n  end\n\n  private def io_do_write(message : String, new_line : Bool) : Nil\n    message += EOL if new_line\n\n    @io.print message\n  end\n\n  private def has_color_support? : Bool\n    # Respect https://no-color.org.\n    return false if ENV[\"NO_COLOR\"]?.presence\n\n    # Respect https://force-color.org.\n    return true if ENV[\"FORCE_COLOR\"]?.presence\n\n    if \"Hyper\" == ENV[\"TERM_PROGRAM\"]? ||\n       ENV.has_key?(\"COLORTERM\") ||\n       ENV.has_key?(\"ANSICON\") ||\n       \"ON\" == ENV[\"ConEmuANSI\"]?\n      return true\n    end\n\n    return @io.tty? unless term = ENV[\"TERM\"]?\n\n    return false if \"dumb\" == term\n\n    # See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157\n    term.matches? /^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/output/null.cr",
    "content": "require \"./interface\"\n\n# An `ACON::Output::Interface` that does not output anything, such as for tests.\nclass Athena::Console::Output::Null\n  include Athena::Console::Output::Interface\n\n  @formatter : ACON::Formatter::Interface? = nil\n\n  # :inherit:\n  def puts(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n  end\n\n  # :inherit:\n  def print(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n  end\n\n  def puts(message, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n  end\n\n  def print(message, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n  end\n\n  # :inherit:\n  def verbosity : ACON::Output::Verbosity\n    ACON::Output::Verbosity::SILENT\n  end\n\n  # :inherit:\n  def verbosity=(verbosity : ACON::Output::Verbosity) : Nil\n  end\n\n  # :inherit:\n  def decorated=(decorated : Bool) : Nil\n  end\n\n  # :inherit:\n  def decorated? : Bool\n    false\n  end\n\n  # :inherit:\n  def formatter : ACON::Formatter::Interface\n    @formatter ||= ACON::Formatter::Null.new\n  end\n\n  # :inherit:\n  def formatter=(formatter : ACON::Formatter::Interface) : Nil\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/output/output.cr",
    "content": "require \"./interface\"\n\n# Common base implementation of `ACON::Output::Interface`.\nabstract class Athena::Console::Output\n  include Athena::Console::Output::Interface\n\n  @formatter : ACON::Formatter::Interface\n  @verbosity : ACON::Output::Verbosity\n\n  def initialize(\n    verbosity : ACON::Output::Verbosity? = :normal,\n    decorated : Bool = false,\n    formatter : ACON::Formatter::Interface? = nil,\n  )\n    @verbosity = verbosity || ACON::Output::Verbosity::NORMAL\n    @formatter = formatter || ACON::Formatter::Output.new\n    @formatter.decorated = decorated\n  end\n\n  # :inherit:\n  def verbosity : ACON::Output::Verbosity\n    @verbosity\n  end\n\n  # :inherit:\n  def verbosity=(@verbosity : ACON::Output::Verbosity) : Nil\n  end\n\n  # :inherit:\n  def formatter : ACON::Formatter::Interface\n    @formatter\n  end\n\n  # :inherit:\n  def formatter=(@formatter : ACON::Formatter::Interface) : Nil\n  end\n\n  # :inherit:\n  def decorated? : Bool\n    @formatter.decorated?\n  end\n\n  # :inherit:\n  def decorated=(decorated : Bool) : Nil\n    @formatter.decorated = decorated\n  end\n\n  # :inherit:\n  def puts(*messages : String) : Nil\n    self.puts messages\n  end\n\n  # :inherit:\n  def puts(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n    self.write message, true, verbosity, output_type\n  end\n\n  # :inherit:\n  def puts(message : _, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n    self.puts message.to_s, verbosity, output_type\n  end\n\n  # :inherit:\n  def print(*messages : String) : Nil\n    self.print messages\n  end\n\n  # :inherit:\n  def print(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n    self.write message, false, verbosity, output_type\n  end\n\n  # :inherit:\n  def print(message : _, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n    self.print message.to_s, verbosity, output_type\n  end\n\n  protected def write(\n    message : String | Enumerable(String),\n    new_line : Bool,\n    verbosity : ACON::Output::Verbosity,\n    output_type : ACON::Output::Type,\n  )\n    messages = message.is_a?(String) ? {message} : message\n\n    return if verbosity > self.verbosity\n\n    messages.each do |m|\n      self.do_write(\n        case output_type\n        in .normal? then @formatter.format m\n        in .plain?  then @formatter.format(m).gsub(/(?:<\\/?[^>]*>)|(?:<!--(.*?)-->[\\n]?)/, \"\") # TODO: Use a more robust strip_tags implementation.\n        in .raw?    then m\n        end,\n        new_line\n      )\n    end\n  end\n\n  protected abstract def do_write(message : String, new_line : Bool) : Nil\nend\n"
  },
  {
    "path": "src/components/console/src/output/section.cr",
    "content": "require \"./io\"\n\n# A `ACON::Output::ConsoleOutput` can be divided into multiple sections that can be written to and cleared independently of one another.\n#\n# Output sections can be used for advanced console outputs, such as displaying multiple progress bars which are updated independently,\n# or appending additional rows to tables.\n#\n# ```\n# protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#   raise ArgumentError.new \"This command may only be used with `ACON::Output::ConsoleOutputInterface`.\" unless output.is_a? ACON::Output::ConsoleOutputInterface\n#\n#   section1 = output.section\n#   section2 = output.section\n#\n#   section1.puts \"Hello\"\n#   section2.puts \"World!\"\n#   # Output contains \"Hello\\nWorld!\\n\"\n#\n#   sleep 1.second\n#\n#   # Replace \"Hello\" with \"Goodbye!\"\n#   section1.overwrite \"Goodbye!\"\n#   # Output now contains \"Goodbye\\nWorld!\\n\"\n#\n#   sleep 1.second\n#\n#   # Clear \"World!\"\n#   section2.clear\n#   # Output now contains \"Goodbye!\\n\"\n#\n#   sleep 1.second\n#\n#   # Delete the last 2 lines of the first section\n#   section1.clear 2\n#   # Output is now empty\n#\n#   ACON::Command::Status::SUCCESS\n# end\n# ```\nclass Athena::Console::Output::Section < Athena::Console::Output::IO\n  protected getter lines = 0\n  protected getter max_height : Int32? = nil\n\n  @content = [] of String\n  @sections : Array(self)\n  @terminal : ACON::Terminal\n\n  def initialize(\n    io : ::IO,\n    @sections : Array(self),\n    verbosity : ACON::Output::Verbosity,\n    decorated : Bool,\n    formatter : ACON::Formatter::Interface,\n  )\n    super io, verbosity, decorated, formatter\n\n    @terminal = ACON::Terminal.new\n    @sections.unshift self\n  end\n\n  # Returns the full content string contained within `self`.\n  def content : String\n    @content.join\n  end\n\n  # Clears at most *lines* from `self`.\n  # If *lines* is `nil`, all of `self` is cleared.\n  def clear(lines : Int32? = nil) : Nil\n    return if @content.empty? || !self.decorated?\n\n    if lines && lines > 0\n      @content.delete_at -Math.min(lines, @content.size)..\n    else\n      lines = @lines\n      @content.clear\n    end\n\n    @lines -= lines\n\n    self.io_do_write self.pop_stream_content_until_current_section((mh = @max_height) ? Math.min(mh, lines) : lines), false\n  end\n\n  # Overrides the current content of `self` with the provided *messages*.\n  def overwrite(*messages : String) : Nil\n    self.overwrite messages\n  end\n\n  # Overrides the current content of `self` with the provided *message*.\n  def overwrite(message : String | Enumerable(String)) : Nil\n    self.clear\n    self.puts message\n  end\n\n  def max_height=(max_height : Int32?) : Nil\n    # Clear output of current section and redraw again with new height\n    previous_max_height = @max_height\n    @max_height = max_height\n    existing_content = self.pop_stream_content_until_current_section previous_max_height ? Math.min(previous_max_height, @lines) : @lines\n\n    self.io_do_write self.visible_content, false\n    self.io_do_write existing_content, false\n  end\n\n  protected def add_content(input : String, new_line : Bool = true) : Int32\n    width = @terminal.width\n    lines = input.split EOL, remove_empty: false\n    lines_added = 0\n    count = lines.size - 1\n\n    lines.each_with_index do |line, idx|\n      # re-add the line break that has been removed in `#lines` for:\n      # - every line that is not the last line\n      # - if new_line is required, also add it to the last line\n      if idx < count || new_line\n        line += EOL\n      end\n\n      # Skip line if there is no text (or new line)\n      next if line.empty?\n\n      # For the first line, check if the previous line (last entry of @content) needs to be continued\n      # I.e. does not end with a line break\n      if idx == 0 && @content[-1]?.try { |l| !l.ends_with? EOL }\n        # Deduct the line count of the previous line\n        w = (self.get_display_width(@content[-1]) / width).ceil.to_i\n        @lines -= w.zero? ? 1 : w\n\n        # Concat previous and new line\n        line = \"#{@content[-1]}#{line}\"\n\n        # Replace last entry of @content with the new expanded line\n        @content[-1] = line\n      else\n        @content << line\n      end\n\n      w = (self.get_display_width(line) / width).ceil.to_i\n      lines_added += w.zero? ? 1 : w\n    end\n\n    @lines += lines_added\n\n    lines_added\n  end\n\n  protected def do_write(message : String, new_line : Bool) : Nil\n    if !new_line && message.ends_with? EOL\n      message = message.chomp\n      new_line = true\n    end\n\n    unless self.decorated?\n      super message, new_line\n\n      return\n    end\n\n    # Check if the previous line (last entry of @content) needs to be continued\n    # i.e. does not end with a line break. In which case, it needs to be erased first\n    lines_to_clear = (last_line = @content[-1]? || \"\").presence.try { |l| !l.ends_with?(EOL) } ? 1 : 0\n    delete_last_line = lines_to_clear == 1\n\n    lines_added = self.add_content message, new_line\n\n    max_height = @max_height || 0\n\n    if line_overflow = (max_height > 0 && @lines > max_height)\n      # on overflow, clear the whole section and redraw again (to remove the first lines)\n      lines_to_clear = max_height\n    end\n\n    erased_content = self.pop_stream_content_until_current_section lines_to_clear\n\n    if line_overflow\n      previous_lines_of_section = @content[@lines - max_height, max_height - lines_added]\n      self.io_do_write previous_lines_of_section.join(\"\"), false\n    end\n\n    # if the last line was removed, re-print its content together with the new content\n    # otherwise, just print the new content\n    self.io_do_write delete_last_line ? \"#{last_line}#{message}\" : message, true\n    self.io_do_write erased_content, false\n  end\n\n  private def get_display_width(input : String) : Int32\n    ACON::Helper.width ACON::Helper.remove_decoration(self.formatter, input.gsub(\"\\t\", \"        \"))\n  end\n\n  private def pop_stream_content_until_current_section(lines_to_clear_from_current_section : Int32 = 0) : String\n    number_of_lines_to_clear = lines_to_clear_from_current_section\n    erased_content = Array(String).new\n\n    @sections.each do |section|\n      break if self == section\n\n      number_of_lines_to_clear += (max_height = section.max_height) ? Math.min(section.lines, max_height) : section.lines\n\n      unless (section_content = section.visible_content).empty?\n        unless section_content.ends_with? EOL\n          section_content = \"#{section_content}#{EOL}\"\n        end\n\n        erased_content << section_content\n      end\n    end\n\n    if number_of_lines_to_clear > 0\n      # Move cursor up n lines\n      self.io_do_write \"\\e[#{number_of_lines_to_clear}A\", false\n\n      # Erase to end of screen\n      self.io_do_write \"\\e[0J\", false\n    end\n\n    erased_content.reverse.join\n  end\n\n  protected def visible_content : String\n    return self.content unless max_height = @max_height\n\n    @content.replace @content[-Math.min(max_height, @content.size)..]\n\n    @content.join\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/output/sized_buffer.cr",
    "content": "# :nodoc:\nclass Athena::Console::Output::SizedBuffer < Athena::Console::Output\n  @buffer : String = \"\"\n  @max_length : Int32\n\n  def initialize(\n    @max_length : Int32,\n    verbosity : ACON::Output::Verbosity? = :normal,\n    decorated : Bool = false,\n    formatter : ACON::Formatter::Interface? = nil,\n  )\n    if @max_length < 0\n      raise ACON::Exception::InvalidArgument.new \"'#{self.class}#max_length' must be a positive, got: '#{@max_length}'.\"\n    end\n\n    super verbosity, decorated, formatter\n  end\n\n  def fetch : String\n    content = @buffer\n\n    @buffer = \"\"\n\n    content\n  end\n\n  protected def do_write(message : String, new_line : Bool) : Nil\n    @buffer += message\n\n    @buffer += EOL if new_line\n\n    @buffer = @buffer.chars.last(@max_length).join\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/output/type.cr",
    "content": "# Determines how a message should be printed.\n#\n# When you output a message via `ACON::Output::Interface#puts` or `ACON::Output::Interface#print`, they also provide a way to set the output type it should be printed:\n#\n# ```\n# protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#   output.puts \"Some Message\", output_type: :raw\n#\n#   ACON::Command::Status::SUCCESS\n# end\n# ```\nenum Athena::Console::Output::Type\n  # Normal output, with any styles applied to format the text.\n  NORMAL\n\n  # Output style tags as is without formatting the string.\n  RAW\n\n  # Strip any style tags and only output the actual text.\n  PLAIN\nend\n"
  },
  {
    "path": "src/components/console/src/output/verbosity.cr",
    "content": "# Verbosity levels determine which messages will be displayed, essentially the same idea as [Log::Severity](https://crystal-lang.org/api/Log/Severity.html) but for console output.\n#\n# For example:\n#\n# ```sh\n# # Output nothing\n# ./console my-command --silent\n#\n# # Output only errors\n# ./console my-command -q\n# ./console my-command --quiet\n#\n# # Display only useful output\n# ./console my-command\n#\n# # Increase the verbosity of messages\n# ./console my-command -v\n#\n# # Also display non-essential information\n# ./console my-command -vv\n#\n# # Display all messages, such as for debugging\n# ./console my-command -vvv\n# ```\n#\n# As used in the previous example, the verbosity can be controlled on a command invocation basis using a CLI option,\n# but may also be globally set via the `SHELL_VERBOSITY` environmental variable.\n#\n# When you output a message via `ACON::Output::Interface#puts` or `ACON::Output::Interface#print`, they also provide a way to set the verbosity at which that message should print:\n#\n# ```\n# protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#   # Via conditional logic\n#   if output.verbosity.verbose?\n#     output.puts \"Obj class: #{obj.class}\"\n#   end\n#\n#   # Inline within the method\n#   output.puts \"Only print this in verbose mode or higher\", verbosity: :verbose\n#\n#   ACON::Command::Status::SUCCESS\n# end\n# ```\n#\n# TIP: The full stack trace of an exception is printed in `ACON::Output::Verbosity::VERBOSE` mode or higher.\nenum Athena::Console::Output::Verbosity\n  # Silences all output.\n  # Equivalent to `--silent` CLI option or `SHELL_VERBOSITY=-2`.\n  SILENT = -2\n\n  # Output only errors.\n  # Equivalent to `-q`, `--quiet` CLI options or `SHELL_VERBOSITY=-1`.\n  QUIET = -1\n\n  # Normal behavior, display only useful messages.\n  # Equivalent not providing any CLI options or `SHELL_VERBOSITY=0`.\n  NORMAL = 0\n\n  # Increase the verbosity of messages.\n  # Equivalent to `-v`, `--verbose=1` CLI options or `SHELL_VERBOSITY=1`.\n  VERBOSE = 1\n\n  # Display all the informative non-essential messages.\n  # Equivalent to `-vv`, `--verbose=2` CLI options or `SHELL_VERBOSITY=2`.\n  VERY_VERBOSE = 2\n\n  # Display all messages, such as for debugging.\n  # Equivalent to `-vvv`, `--verbose=3` CLI options or `SHELL_VERBOSITY=3`.\n  DEBUG = 3\nend\n"
  },
  {
    "path": "src/components/console/src/question/abstract_choice.cr",
    "content": "class Athena::Console::Question(T); end\n\nrequire \"./base\"\n\n# Base type of choice based questions.\n# See each subclass for more information.\nabstract class Athena::Console::Question::AbstractChoice(T, ChoiceType)\n  include Athena::Console::Question::Base(T?)\n\n  # Returns the possible choices.\n  getter choices : Hash(String | Int32, T)\n\n  # Returns the message to display if the provided answer is not a valid choice.\n  getter error_message : String = \"Value '%s' is invalid.\"\n\n  # Returns/sets the prompt to use for the question.\n  # The prompt being the character(s) before the user input.\n  property prompt : String = \" > \"\n\n  # See [Validating the Answer][Athena::Console::Question--validating-the-answer].\n  property validator : Proc(T?, ChoiceType)? = nil\n\n  def self.new(question : String, choices : Indexable(T), default : Int | T | Nil = nil)\n    choices_hash = Hash(String | Int32, T).new\n\n    choices.each_with_index do |choice, idx|\n      choices_hash[idx] = choice\n    end\n\n    new question, choices_hash, (default.is_a?(Int) ? choices[default]? : default)\n  end\n\n  def initialize(question : String, choices : Hash(String | Int32, T), default : T? = nil)\n    super question, default\n\n    raise ACON::Exception::Logic.new \"Choice questions must have at least 1 choice available.\" if choices.empty?\n\n    @choices = choices.transform_keys &.as String | Int32\n\n    self.validator = ->default_validator(T?)\n    self.autocompleter_values = choices\n  end\n\n  def error_message=(@error_message : String) : self\n    self.validator = ->default_validator(T?)\n\n    self\n  end\n\n  # Sets the validator callback to the provided block.\n  # See [Validating the Answer][Athena::Console::Question--validating-the-answer].\n  def validator(&@validator : T? -> ChoiceType) : Nil\n  end\n\n  private def selected_choices(answer : String?) : Array(T)\n    selected_choices = self.parse_answers answer\n\n    if @trimmable\n      selected_choices.map! &.strip\n    end\n\n    valid_choices = [] of String\n    selected_choices.each do |value|\n      results = [] of String\n\n      @choices.each do |key, choice|\n        results << key.to_s if choice == value\n      end\n\n      raise ACON::Exception::InvalidArgument.new %(The provided answer is ambiguous. Value should be one of #{results.join(\" or \") { |i| \"'#{i}'\" }}.) if results.size > 1\n\n      result = @choices.find { |(k, v)| v == value || k.to_s == value }.try &.first.to_s\n\n      # If none of the keys are a string, assume the original choice values were an Indexable.\n      if @choices.keys.none?(String) && result\n        result = @choices[result.to_i]\n      elsif @choices.has_key? value\n        result = @choices[value]\n      elsif @choices.has_key? result\n        result = @choices[result]\n      end\n\n      if result.nil?\n        raise ACON::Exception::InvalidArgument.new sprintf(@error_message, value)\n      end\n\n      valid_choices << result\n    end\n\n    valid_choices\n  end\n\n  protected abstract def default_validator(answer : T?) : ChoiceType\n  protected abstract def parse_answers(answer : T?) : Array(String)\nend\n"
  },
  {
    "path": "src/components/console/src/question/base.cr",
    "content": "# Common logic shared between all question types.\n# See each type for more information.\nmodule Athena::Console::Question::Base(T)\n  # Returns the question that should be asked.\n  getter question : String\n\n  # Returns the default value if no valid input is provided.\n  getter default : T\n\n  # Returns the answer should be hidden.\n  # See [Hiding User Input][Athena::Console::Question--hiding-user-input].\n  getter? hidden : Bool = false\n\n  # If hidden questions should fallback on making the response visible if it was unable to be hidden.\n  # See [Hiding User Input][Athena::Console::Question--hiding-user-input].\n  property? hidden_fallback : Bool = true\n\n  # Returns how many attempts the user has to enter a valid value when a `#validator` is set.\n  # See [Validating the Answer][Athena::Console::Question--validating-the-answer].\n  getter max_attempts : Int32? = nil\n\n  # :nodoc:\n  getter autocompleter_callback : Proc(String, Array(String))? = nil\n\n  # See [Normalizing the Answer][Athena::Console::Question--normalizing-the-answer].\n  property normalizer : Proc(T | String, T)? = nil\n\n  # If multi line text should be allowed in the response.\n  # See [Multiline Input][Athena::Console::Question--multiline-input].\n  property? multi_line : Bool = false\n\n  # Returns/sets if the answer value should be automatically [trimmed](https://crystal-lang.org/api/String.html#strip%3AString-instance-method).\n  # See [Trimming the Answer][Athena::Console::Question--trimming-the-answer].\n  property? trimmable : Bool = true\n\n  def initialize(@question : String, @default : T)\n    {%\n      if T == Nil\n        T.raise \"An ACON::Question generic argument cannot be 'Nil'. Use 'String?' instead.\"\n      end\n    %}\n  end\n\n  # :nodoc:\n  def autocompleter_values : Array(String)?\n    if callback = @autocompleter_callback\n      return callback.call \"\"\n    end\n\n    nil\n  end\n\n  # :nodoc:\n  def autocompleter_values=(values : Hash(String, _)?) : self\n    self.autocompleter_values = values.keys + values.values\n  end\n\n  # :nodoc:\n  def autocompleter_values=(values : Hash?) : self\n    self.autocompleter_values = values.values\n  end\n\n  # :nodoc:\n  def autocompleter_values=(values : Indexable?) : self\n    if values.nil?\n      @autocompleter_callback = nil\n      return self\n    end\n\n    callback = Proc(String, Array(String)).new do\n      values.to_a\n    end\n\n    self.autocompleter_callback &callback\n\n    self\n  end\n\n  # :nodoc:\n  def autocompleter_callback(&block : String -> Array(String)) : Nil\n    raise ACON::Exception::Logic.new \"A hidden question cannot use the autocompleter.\" if @hidden\n\n    @autocompleter_callback = block\n  end\n\n  # Sets if the answer should be *hidden*.\n  # See [Hiding User Input][Athena::Console::Question--hiding-user-input].\n  def hidden=(hidden : Bool) : self\n    raise ACON::Exception::Logic.new \"A hidden question cannot use the autocompleter.\" if @autocompleter_callback\n\n    @hidden = hidden\n\n    self\n  end\n\n  # Allow at most *attempts* for the user to enter a valid value when a `#validator` is set.\n  # If *attempts* is `nil`, they have an unlimited amount.\n  #\n  # See [Validating the Answer][Athena::Console::Question--validating-the-answer].\n  def max_attempts=(attempts : Int32?) : self\n    raise ACON::Exception::InvalidArgument.new \"Maximum number of attempts must be a positive value.\" if attempts && attempts < 0\n\n    @max_attempts = attempts\n    self\n  end\n\n  # Sets the normalizer callback to this block.\n  # See [Normalizing the Answer][Athena::Console::Question--normalizing-the-answer].\n  def normalizer(&@normalizer : T | String -> T) : Nil\n  end\n\n  protected def process_response(response : String)\n    response = response.presence || @default\n\n    # Only call the normalizer with the actual response or a non nil default.\n    if (normalizer = @normalizer) && !response.nil?\n      return normalizer.call response\n    end\n\n    response.as T\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/question/choice.cr",
    "content": "require \"./abstract_choice\"\n\n# A question whose answer _MUST_ be within a set of predefined answers.\n# If the user enters an invalid answer, an error is displayed and they are prompted again.\n#\n# ```\n# question = ACON::Question::Choice.new \"What is your favorite color?\", {\"red\", \"blue\", \"green\"}\n#\n# helper = self.helper ACON::Helper::Question\n# color = helper.ask input, output, question\n# ```\n#\n# This would display something like the following:\n#\n# ```sh\n# What is your favorite color?\n#  [0] red\n#  [1] blue\n#  [2] green\n# >\n# ```\n#\n# The user would be able to enter the name of the color, or the index associated with it. E.g. `blue` or `2` for `green`.\n# If a `Hash` is used as the choices, the key of each choice is used instead of its index.\n#\n# Similar to `ACON::Question`, the third argument can be set to set the default choice.\n# This value can also either be the actual value, or the index/key of the related choice.\n#\n# ```\n# question = ACON::Question::Choice.new \"What is your favorite color?\", {\"c1\" => \"red\", \"c2\" => \"blue\", \"c3\" => \"green\"}, \"c2\"\n#\n# helper = self.helper ACON::Helper::Question\n# color = helper.ask input, output, question\n# ```\n#\n# Which would display something like :\n#\n# ```sh\n# What is your favorite color?\n#  [c1] red\n#  [c2] blue\n#  [c3] green\n# >\n# ```\nclass Athena::Console::Question::Choice(T) < Athena::Console::Question::AbstractChoice(T, T?)\n  protected def default_validator(answer : T?) : T?\n    self.selected_choices(answer).first?\n  end\n\n  protected def parse_answers(answer : T?) : Array(String)\n    [answer || \"\"]\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/question/confirmation.cr",
    "content": "# Allows prompting the user to confirm an action.\n#\n# ```\n# question = ACON::Question::Confirmation.new \"Continue with this action?\", false\n# helper = self.helper ACON::Helper::Question\n#\n# if !helper.ask input, output, question\n#   return ACON::Command::Status::SUCCESS\n# end\n#\n# # ...\n# ```\n#\n# In this example the user will be asked if they wish to `Continue with this action`.\n# The `#ask` method will return `true` if the user enters anything starting with `y`, otherwise `false`.\nclass Athena::Console::Question::Confirmation < Athena::Console::Question(Bool)\n  @true_answer_regex : Regex\n\n  # Creates a new instance of self with the provided *question* string.\n  # The *default* parameter represents the value to return if no valid input was entered.\n  # The *true_answer_regex* can be used to customize the pattern used to determine if the user's input evaluates to `true`.\n  def initialize(question : String, default : Bool = true, @true_answer_regex : Regex = /^y/i)\n    super question, default\n\n    self.normalizer = ->default_normalizer(String | Bool)\n  end\n\n  private def default_normalizer(answer : String | Bool) : Bool\n    if answer.is_a? Bool\n      return answer\n    end\n\n    answer_is_true = answer.matches? @true_answer_regex\n\n    if false == @default\n      return !answer.blank? && answer_is_true\n    end\n\n    answer.empty? || answer_is_true\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/question/multiple_choice.cr",
    "content": "require \"./abstract_choice\"\n\n# Similar to `ACON::Question::Choice`, but allows for more than one answer to be selected.\n# This question accepts a comma separated list of answers.\n#\n# ```\n# question = ACON::Question::MultipleChoice.new \"What is your favorite color?\", {\"red\", \"blue\", \"green\"}\n#\n# helper = self.helper ACON::Helper::Question\n# answer = helper.ask input, output, question\n# ```\n#\n# This question is also similar to `ACON::Question::Choice` in that you can provide either the index, key, or value of the choice.\n# For example submitting `green,0` would result in `[\"green\", \"red\"]` as the value of `answer`.\nclass Athena::Console::Question::MultipleChoice(T) < Athena::Console::Question::AbstractChoice(T, Array(T))\n  protected def default_validator(answer : T?) : Array(T)\n    self.selected_choices answer\n  end\n\n  protected def parse_answers(answer : T?) : Array(String)\n    answer.try(&.split(',')) || [\"\"]\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/question/question.cr",
    "content": "require \"./base\"\n\n# This namespaces contains various questions that can be asked via the `ACON::Helper::Question` helper or `ART::Style::Athena` style.\n#\n# This class can also be used to ask the user for more information in the most basic form, a simple question and answer.\n#\n# ## Usage\n#\n# ```\n# question = ACON::Question(String?).new \"What is your name?\", nil\n#\n# helper = self.helper ACON::Helper::Question\n# name = helper.ask input, output, question\n# ```\n#\n# This will prompt to user to enter their name. If they do not enter valid input, the default value of `nil` will be used.\n# The default can be customized, ideally with sane defaults to make the UX better.\n#\n# ### Trimming the Answer\n#\n# By default the answer is [trimmed](https://crystal-lang.org/api/String.html#strip%3AString-instance-method) in order to remove leading and trailing white space.\n# The `ACON::Question::Base#trimmable=` method can be used to disable this if you need the input as is.\n#\n# ```\n# question = ACON::Question(String?).new \"What is your name?\", nil\n# question.trimmable = false\n#\n# helper = self.helper ACON::Helper::Question\n# name_with_whitespace_and_newline = helper.ask input, output, question\n# ```\n#\n# ### Multiline Input\n#\n# The question helper will stop reading input when it receives a newline character. I.e. the user presses the `ENTER` key.\n# However in some cases you may want to allow for an answer that spans multiple lines.\n# The `ACON::Question::Base#multi_line=` method can be used to enable multi line mode.\n#\n# ```\n# question = ACON::Question(String?).new \"Tell me a story.\", nil\n# question.multi_line = true\n# ```\n#\n# Multiline questions stop reading user input after receiving an end-of-transmission control character. (`Ctrl+D` on Unix systems).\n#\n# ### Hiding User Input\n#\n# If your question is asking for sensitive information, such as a password, you can set a question to hidden.\n# This will make it so the input string is not displayed on the terminal, which is equivalent to how password are handled on Unix systems.\n#\n# ```\n# question = ACON::Question(String?).new \"What is your password?.\", nil\n# question.hidden = true\n# ```\n#\n# WARNING: If no method to hide the response is available on the underlying system/input, it will fallback and allow the response to be seen.\n# If having the hidden response hidden is vital, you _MUST_ set `ACON::Question::Base#hidden_fallback=` to `false`; which will\n# raise an exception instead of allowing the input to be visible.\n#\n# ### Normalizing the Answer\n#\n# The answer can be \"normalized\" before being validated to fix any small errors or tweak it as needed.\n# For example, you could normalize the casing of the input:\n#\n# ```\n# question = ACON::Question(String?).new \"Enter your name.\", nil\n# question.normalizer do |input|\n#   input.try &.downcase\n# end\n# ```\n#\n# It is possible for *input* to be `nil` in this case, so that need to also be handled in the block.\n# The block should return a value of the same type of the generic, in this case `String?`.\n#\n# NOTE: The normalizer is called first and its return value is used as the input of the validator.\n# If the answer is invalid do not raise an exception in the normalizer and let the validator handle it.\n#\n# ### Validating the Answer\n#\n# If the answer to a question needs to match some specific requirements, you can register a question validator to check the validity of the answer.\n# This callback should raise an exception if the input is not valid, such as `ArgumentError`. Otherwise, it must return the input value.\n#\n# ```\n# question = ACON::Question(String?).new \"Enter your name.\", nil\n# question.validator do |input|\n#   next input if input.nil? || !input.starts_with? /^\\d+/\n#\n#   raise ArgumentError.new \"Invalid name. Cannot start with numeric digits.\"\n# end\n# ```\n#\n# In this example, we are asserting that the user's name does not start with numeric digits.\n# If the user entered `123Jim`, they would be told it is an invalid answer and prompted to answer the question again.\n# By default the user would have an unlimited amount of retries to get it right, but this can be customized via `ACON::Question::Base#max_attempts=`.\n#\n# ### Autocompletion\n#\n# TODO: Implement this.\nclass Athena::Console::Question(T)\n  include Athena::Console::Question::Base(T)\n\n  # See [Validating the Answer][Athena::Console::Question--validating-the-answer].\n  property validator : Proc(T, T)? = nil\n\n  # Sets the validator callback to this block.\n  # See [Validating the Answer][Athena::Console::Question--validating-the-answer].\n  def validator(&@validator : T -> T) : Nil\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/spec/expectations/command_is_successful.cr",
    "content": "# :nodoc:\nstruct Athena::Console::Spec::Expectations::CommandIsSuccessful\n  def match(actual_value : ::ACON::Command::Status?) : Bool\n    ACON::Command::Status::SUCCESS == actual_value\n  end\n\n  def failure_message(actual_value : ::ACON::Command::Status?) : String\n    \"The command was unsuccessful\"\n  end\n\n  def negative_failure_message(actual_value : ::ACON::Command::Status?) : String\n    \"The command was unsuccessful\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/spec.cr",
    "content": "require \"./spec/expectations/*\"\n\n# Provides helper types for testing `ACON::Command` and `ACON::Application`s.\nmodule Athena::Console::Spec\n  # Contains common logic shared by both `ACON::Spec::CommandTester` and `ACON::Spec::ApplicationTester`.\n  module Tester\n    @capture_stderr_separately : Bool = false\n\n    # Returns the `ACON::Output::Interface` being used by the tester.\n    getter! output : ACON::Output::Interface\n\n    # Sets an array of values that will be used as the input to the command.\n    # `RETURN` is automatically assumed after each input.\n    setter inputs : Array(String) = [] of String\n\n    # Returns the output resulting from running the command.\n    # Raises if called before executing the command.\n    def display(normalize : Bool = false) : String\n      raise ACON::Exception::Logic.new \"Output not initialized. Did you execute the command before requesting the display?\" unless output = @output\n      output = output.to_s\n\n      if normalize\n        output = output.gsub EOL, \"\\n\"\n      end\n\n      output\n    end\n\n    # Returns the error output resulting from running the command.\n    # Raises if `capture_stderr_separately` was not set to `true`.\n    def error_output(normalize : Bool = false) : String\n      raise ACON::Exception::Logic.new \"The error output is not available when the test is ran without 'capture_stderr_separately' set.\" unless @capture_stderr_separately\n\n      output = self.output.as(ACON::Output::ConsoleOutput).error_output.to_s\n\n      if normalize\n        output = output.gsub EOL, \"\\n\"\n      end\n\n      output\n    end\n\n    # Helper method to setting the `#inputs=` property.\n    def inputs(*args : String) : Nil\n      @inputs = args.to_a\n    end\n\n    abstract def status : ACON::Command::Status?\n\n    # Asserts that the return `#status` is successful.\n    def assert_command_is_successful(message : String = \"\", *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n      self.status.should ACON::Spec::Expectations::CommandIsSuccessful.new, file: file, line: line, failure_message: message.presence\n    end\n\n    # Asserts that the return `#status` is _NOT_ successful.\n    def assert_command_is_not_successful(message : String = \"\", *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n      self.status.should_not ACON::Spec::Expectations::CommandIsSuccessful.new, file: file, line: line, failure_message: message.presence\n    end\n\n    protected def init_output(\n      decorated : Bool? = nil,\n      interactive : Bool? = nil,\n      verbosity : ACON::Output::Verbosity? = nil,\n      @capture_stderr_separately : Bool = false,\n    ) : Nil\n      if !@capture_stderr_separately\n        @output = ACON::Output::IO.new IO::Memory.new\n\n        decorated.try do |d|\n          self.output.decorated = d\n        end\n\n        verbosity.try do |v|\n          self.output.verbosity = v\n        end\n      else\n        @output = ACON::Output::ConsoleOutput.new(\n          verbosity || ACON::Output::Verbosity::NORMAL,\n          decorated\n        )\n\n        error_output = ACON::Output::IO.new IO::Memory.new\n        error_output.formatter = self.output.formatter\n        error_output.verbosity = self.output.verbosity\n        error_output.decorated = self.output.decorated?\n\n        self.output.as(ACON::Output::ConsoleOutput).stderr = error_output\n        self.output.as(ACON::Output::IO).io = IO::Memory.new\n      end\n    end\n\n    private def create_input_stream(inputs : Array(String)) : IO\n      input_stream = IO::Memory.new\n\n      inputs.each do |input|\n        input_stream << \"#{input}#{EOL}\"\n      end\n\n      input_stream.rewind\n\n      input_stream\n    end\n  end\n\n  # Functionally similar to `ACON::Spec::CommandTester`, but used for testing entire `ACON::Application`s.\n  #\n  # Can be useful if your project extends the base application in order to customize it in some way.\n  #\n  # NOTE: Be sure to set `ACON::Application#auto_exit=` to `false`, when testing an entire application.\n  struct ApplicationTester\n    include Tester\n\n    # Returns the `ACON::Application` instance being tested.\n    getter application : ACON::Application\n\n    # Returns the `ACON::Input::Interface` being used by the tester.\n    getter! input : ACON::Input::Interface\n\n    # Returns the `ACON::Command::Status` of the command execution, or `nil` if it has not yet been executed.\n    getter status : ACON::Command::Status? = nil\n\n    def initialize(@application : ACON::Application); end\n\n    # Runs the application, with the provided *input* being used as the input of `ACON::Application#run`.\n    #\n    # Custom values for *decorated*, *interactive*, and *verbosity* can also be provided and will be forwarded to their respective types.\n    # *capture_stderr_separately* makes it so output to `STDERR` is captured separately, in case you wanted to test error output.\n    # Otherwise both error and normal output are captured via `ACON::Spec::Tester#display`.\n    def run(\n      decorated : Bool = false,\n      interactive : Bool? = nil,\n      capture_stderr_separately : Bool = false,\n      verbosity : ACON::Output::Verbosity? = nil,\n      **input : _,\n    )\n      self.run input.to_h.transform_keys(&.to_s), decorated: decorated, interactive: interactive, capture_stderr_separately: capture_stderr_separately, verbosity: verbosity\n    end\n\n    # :ditto:\n    def run(\n      input : Hash(String, _) = Hash(String, String).new,\n      *,\n      decorated : Bool? = nil,\n      interactive : Bool? = nil,\n      capture_stderr_separately : Bool = false,\n      verbosity : ACON::Output::Verbosity? = nil,\n    ) : ACON::Command::Status\n      @input = ACON::Input::Hash.new input\n\n      interactive.try do |i|\n        self.input.interactive = i\n      end\n\n      unless (inputs = @inputs).empty?\n        self.input.stream = self.create_input_stream inputs\n      end\n\n      self.init_output(\n        decorated: decorated,\n        interactive: interactive,\n        capture_stderr_separately: capture_stderr_separately,\n        verbosity: verbosity\n      )\n\n      @status = @application.run self.input, self.output\n    end\n  end\n\n  # Allows testing the logic of an `ACON::Command`, without needing to create and run a binary.\n  #\n  # Say we have the following command:\n  #\n  # ```\n  # @[ACONA::AsCommand(\"add\", description: \"Sums two numbers, optionally making making the sum negative\")]\n  # class AddCommand < ACON::Command\n  #   protected def configure : Nil\n  #     self\n  #       .argument(\"value1\", :required, \"The first value\")\n  #       .argument(\"value2\", :required, \"The second value\")\n  #       .option(\"negative\", description: \"If the sum should be made negative\")\n  #   end\n  #\n  #   protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n  #     sum = input.argument(\"value1\", Int32) + input.argument(\"value2\", Int32)\n  #\n  #     sum = -sum if input.option \"negative\", Bool\n  #\n  #     output.puts \"The sum of values is: #{sum}\"\n  #\n  #     ACON::Command::Status::SUCCESS\n  #   end\n  # end\n  # ```\n  #\n  # We can use `ACON::Spec::CommandTester` to assert it is working as expected.\n  #\n  # ```\n  # require \"spec\"\n  # require \"athena-spec\"\n  #\n  # describe AddCommand do\n  #   describe \"#execute\" do\n  #     it \"without negative option\" do\n  #       tester = ACON::Spec::CommandTester.new AddCommand.new\n  #       tester.execute value1: 10, value2: 7\n  #       tester.display.should eq \"The sum of the values is: 17\\n\"\n  #     end\n  #\n  #     it \"with negative option\" do\n  #       tester = ACON::Spec::CommandTester.new AddCommand.new\n  #       tester.execute value1: -10, value2: 5, \"--negative\": nil\n  #       tester.display.should eq \"The sum of the values is: 5\\n\"\n  #     end\n  #   end\n  # end\n  # ```\n  #\n  # ### Commands with User Input\n  #\n  # A command that are asking `ACON::Question`s can also be tested:\n  #\n  # ```\n  # @[ACONA::AsCommand(\"question\")]\n  # class QuestionCommand < ACON::Command\n  #   protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n  #     helper = self.helper ACON::Helper::Question\n  #\n  #     question = ACON::Question(String).new \"What is your name?\", \"None\"\n  #     output.puts \"Your name is: #{helper.ask input, output, question}\"\n  #\n  #     ACON::Command::Status::SUCCESS\n  #   end\n  # end\n  # ```\n  #\n  # ```\n  # require \"spec\"\n  # require \"./src/spec\"\n  #\n  # describe QuestionCommand do\n  #   describe \"#execute\" do\n  #     it do\n  #       command = QuestionCommand.new\n  #       command.helper_set = ACON::Helper::HelperSet.new ACON::Helper::Question.new\n  #       tester = ACON::Spec::CommandTester.new command\n  #       tester.inputs \"Jim\"\n  #       tester.execute\n  #       tester.display.should eq \"What is your name?Your name is: Jim\\n\"\n  #     end\n  #   end\n  # end\n  # ```\n  #\n  # Because we are not in the context of an `ACON::Application`, we need to manually set the `ACON::Helper::HelperSet`\n  # in order to make the command aware of `ACON::Helper::Question`. After that we can use the `ACON::Spec::Tester#inputs` method\n  # to set the inputs our test should use when prompted.\n  #\n  # Multiple inputs can be provided if there are multiple questions being asked.\n  struct CommandTester\n    include Tester\n\n    # Returns the `ACON::Input::Interface` being used by the tester.\n    getter! input : ACON::Input::Interface\n\n    # Returns the `ACON::Command::Status` of the command execution, or `nil` if it has not yet been executed.\n    getter status : ACON::Command::Status? = nil\n\n    def initialize(@command : ACON::Command); end\n\n    # Executes the command, with the provided *input* being passed to the command.\n    #\n    # Custom values for *decorated*, *interactive*, and *verbosity* can also be provided and will be forwarded to their respective types.\n    # *capture_stderr_separately* makes it so output to `STDERR` is captured separately, in case you wanted to test error output.\n    # Otherwise both error and normal output are captured via `ACON::Spec::Tester#display`.\n    def execute(\n      decorated : Bool = false,\n      interactive : Bool? = nil,\n      capture_stderr_separately : Bool = false,\n      verbosity : ACON::Output::Verbosity? = nil,\n      **input : _,\n    )\n      self.execute input.to_h.transform_keys(&.to_s), decorated: decorated, interactive: interactive, capture_stderr_separately: capture_stderr_separately, verbosity: verbosity\n    end\n\n    # :ditto:\n    def execute(\n      input : Hash(String, _) = Hash(String, String).new,\n      *,\n      decorated : Bool = false,\n      interactive : Bool? = nil,\n      capture_stderr_separately : Bool = false,\n      verbosity : ACON::Output::Verbosity? = nil,\n    ) : ACON::Command::Status\n      if !input.has_key?(\"command\") && (application = @command.application?) && application.definition.has_argument?(\"command\")\n        input = input.merge({\"command\" => @command.name})\n      end\n\n      @input = ACON::Input::Hash.new input\n      self.input.stream = self.create_input_stream @inputs\n\n      interactive.try do |i|\n        self.input.interactive = i\n      end\n\n      self.init_output(\n        decorated: decorated,\n        interactive: interactive,\n        capture_stderr_separately: capture_stderr_separately,\n        verbosity: verbosity\n      )\n\n      @status = @command.run self.input, self.output\n    end\n  end\n\n  struct CommandCompletionTester\n    def initialize(@command : ACON::Command); end\n\n    def complete(*input : String) : Array(String)\n      self.complete input\n    end\n\n    def complete(input : Enumerable(String)) : Array(String)\n      completion_input = ACON::Completion::Input.from_tokens input.to_a, (input.size - 1).clamp(0, nil)\n      completion_input.bind @command.definition\n      suggestions = ACON::Completion::Suggestions.new\n\n      @command.complete completion_input, suggestions\n\n      options = [] of String\n\n      suggestions.suggested_options.each do |option|\n        options << \"--#{option.name}\"\n      end\n\n      options.concat suggestions.suggested_values.map(&.to_s)\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/style/athena.cr",
    "content": "require \"./output\"\n\n# Default implementation of `ACON::Style::Interface` that provides a slew of helpful methods for formatting output.\n#\n# Uses `ACON::Helper::AthenaQuestion` to improve the appearance of questions.\n#\n# ```\n# protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n#   style = ACON::Style::Athena.new input, output\n#\n#   style.title \"Some Fancy Title\"\n#\n#   # ...\n#\n#   ACON::Command::Status::SUCCESS\n# end\n# ```\nclass Athena::Console::Style::Athena < Athena::Console::Style::Output\n  private MAX_LINE_LENGTH = 120\n\n  protected getter question_helper : ACON::Helper::Question { ACON::Helper::AthenaQuestion.new }\n\n  @input : ACON::Input::Interface\n  @buffered_output : ACON::Output::SizedBuffer\n\n  @line_length : Int32\n  @progress_bar : ACON::Helper::ProgressBar? = nil\n\n  def initialize(@input : ACON::Input::Interface, output : ACON::Output::Interface)\n    width = ACON::Terminal.new.width || MAX_LINE_LENGTH\n\n    @buffered_output = ACON::Output::SizedBuffer.new {{flag?(:windows) ? 4 : 2}}, output.verbosity, false, output.formatter.dup\n    @line_length = Math.min(width - {{flag?(:windows) ? 1 : 0}}, MAX_LINE_LENGTH)\n\n    super output\n  end\n\n  # :inherit:\n  def ask(question : String, default : String?)\n    self.ask ACON::Question(String?).new question, default\n  end\n\n  # :ditto:\n  def ask(question : ACON::Question::Base)\n    if @input.interactive?\n      self.auto_prepend_block\n    end\n\n    answer = self.question_helper.ask @input, self, question\n\n    if @input.interactive?\n      self.new_line\n      @buffered_output.print EOL\n    end\n\n    answer\n  end\n\n  # :inherit:\n  def ask_hidden(question : String)\n    question = ACON::Question(String?).new question, nil\n\n    question.hidden = true\n\n    self.ask question\n  end\n\n  # Helper method for outputting blocks of *messages* that powers the `#caution`, `#success`, `#note`, etc. methods.\n  # It includes various optional parameters that can be used to print customized blocks.\n  #\n  # If *type* is provided, its value will be printed within `[]`. E.g. `[TYPE]`.\n  #\n  # If *style* is provided, each of the *messages* will be printed in that style.\n  #\n  # *prefix* represents what each of the *messages* should be prefixed with.\n  #\n  # If *padding* is `true`, empty lines will be added before/after the block.\n  #\n  # If *escape* is `true`, each of the *messages* will be escaped via `ACON::Formatter::Output.escape`.\n  def block(messages : String | Enumerable(String), type : String? = nil, style : String? = nil, prefix : String = \" \", padding : Bool = false, escape : Bool = true) : Nil\n    messages = messages.is_a?(Enumerable(String)) ? messages : {messages}\n\n    self.auto_prepend_block\n    self.puts self.create_block(messages, type, style, prefix, padding, escape)\n    self.new_line\n  end\n\n  # :inherit:\n  #\n  # ```text\n  # !\n  # ! [CAUTION] Some Message\n  # !\n  # ```\n  #\n  # White text on a 3 line red background block with an empty line above/below the block.\n  def caution(messages : String | Enumerable(String)) : Nil\n    self.block messages, \"CAUTION\", \"fg=white;bg=red\", \" ! \", true\n  end\n\n  # :inherit:\n  #\n  # ```text\n  # ----- -------\n  #  Foo   Bar\n  # ----- -------\n  #  Biz   Baz\n  #  12    false\n  # ----- -------\n  #\n  # ```\n  def table(headers : Enumerable, rows : Enumerable) : Nil\n    self.create_table\n      .headers(headers)\n      .rows(rows)\n      .render\n\n    self.new_line\n  end\n\n  # Sames as `#table`, but horizontal\n  def horizontal_table(headers : Enumerable, rows : Enumerable) : Nil\n    self.create_table\n      .headers(headers)\n      .rows(rows)\n      .horizontal\n      .render\n\n    self.new_line\n  end\n\n  # Sames as `#table`, but vertical\n  def vertical_table(headers : Enumerable, rows : Enumerable) : Nil\n    self.create_table\n      .headers(headers)\n      .rows(rows)\n      .vertical\n      .render\n\n    self.new_line\n  end\n\n  # Formats a list of key/value pairs horizontally.\n  #\n  # TODO: `Mappable` when/if https://github.com/crystal-lang/crystal/issues/10886 is implemented.\n  def definition_list(*rows : String | ACON::Helper::Table::Separator | Enumerable({K, V})) : Nil forall K, V\n    table_headers = [] of String | ACON::Helper::Table::Cell\n    table_row = [] of String | ACON::Helper::Table::Cell | Nil\n\n    rows.each do |row|\n      case row\n      in String\n        table_headers << ACON::Helper::Table::Cell.new row, colspan: 2\n        table_row << nil\n      in ACON::Helper::Table::Cell\n        table_headers << row\n        table_row << row\n      in Enumerable\n        table_headers << row.first_key.to_s\n        table_row << row.first_value.to_s\n      end\n    end\n\n    self.horizontal_table table_headers, {table_row}\n  end\n\n  # Creates and returns an Athena styled `ACON::Helper::Table` instance.\n  def create_table : ACON::Helper::Table\n    style = ACON::Helper::Table.style_definition(\"suggested\").clone\n    style.cell_header_format \"<info>%s</info>\"\n\n    ACON::Helper::Table.new(\n      (output = @output).is_a?(ACON::Output::ConsoleOutputInterface) ? output.section : @output\n    )\n      .style(style)\n  end\n\n  # :inherit:\n  def choice(question : String, choices : Indexable | Hash, default = nil)\n    self.ask ACON::Question::Choice.new question, choices, default\n  end\n\n  # :inherit:\n  #\n  # ```text\n  # // Some Message\n  # ```\n  #\n  # White text with one empty line above/below the message(s).\n  def comment(messages : String | Enumerable(String)) : Nil\n    self.block messages, prefix: \"<fg=default;bg=default> // </>\", escape: false\n  end\n\n  # :inherit:\n  def confirm(question : String, default : Bool = true) : Bool\n    self.ask ACON::Question::Confirmation.new question, default\n  end\n\n  # :inherit:\n  #\n  # ```text\n  # [ERROR] Some Message\n  # ```\n  #\n  # White text on a 3 line red background block with an empty line above/below the block.\n  def error(messages : String | Enumerable(String)) : Nil\n    self.block messages, \"ERROR\", \"fg=white;bg=red\", padding: true\n  end\n\n  # Returns a new instance of `self` that outputs to the error output.\n  def error_style : self\n    self.class.new @input, self.error_output\n  end\n\n  # :inherit:\n  #\n  # ```text\n  # [INFO] Some Message\n  # ```\n  #\n  # Green text with two empty lines above/below the message(s).\n  def info(messages : String | Enumerable(String)) : Nil\n    self.block messages, \"INFO\", \"fg=green\", padding: true\n  end\n\n  # :inherit:\n  #\n  # ```text\n  # * Item 1\n  # * Item 2\n  # * Item 3\n  # ```\n  #\n  # White text with one empty line above/below the list.\n  def listing(elements : Enumerable) : Nil\n    self.auto_prepend_text\n    elements.each do |element|\n      self.puts \" * #{element}\"\n    end\n    self.new_line\n  end\n\n  # :ditto:\n  def listing(*elements : String) : Nil\n    self.listing elements\n  end\n\n  # :inherit:\n  def new_line(count : Int32 = 1) : Nil\n    super\n    @buffered_output.print EOL * count\n  end\n\n  # :inherit:\n  #\n  # ```text\n  # ! [NOTE] Some Message\n  # ```\n  #\n  # Green text with one empty line above/below the message(s).\n  def note(messages : String | Enumerable(String)) : Nil\n    self.block messages, \"NOTE\", \"fg=yellow\", \" ! \"\n  end\n\n  # :inherit:\n  def puts(messages : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n    messages = messages.is_a?(String) ? {messages} : messages\n\n    messages.each do |message|\n      super message, verbosity, output_type\n      self.write_buffer message, true, verbosity, output_type\n    end\n  end\n\n  # :inherit:\n  def print(messages : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n    messages = messages.is_a?(String) ? {messages} : messages\n\n    messages.each do |message|\n      super message, verbosity, output_type\n      self.write_buffer message, false, verbosity, output_type\n    end\n  end\n\n  # :inherit:\n  #\n  # ```text\n  # Some Message\n  # ------------\n  # ```\n  #\n  # Orange text with one empty line above/below the section.\n  def section(message : String) : Nil\n    self.auto_prepend_block\n    self.puts \"<comment>#{ACON::Formatter::Output.escape_trailing_backslash message}</>\"\n    self.puts %(<comment>#{\"-\" * ACON::Helper.width(ACON::Helper.remove_decoration(self.formatter, message))}</>)\n    self.new_line\n  end\n\n  # :inherit:\n  #\n  # ```text\n  #  [OK] Some Message\n  # ```\n  #\n  # Black text on a 3 line green background block with an empty line above/below the block.\n  def success(messages : String | Enumerable(String)) : Nil\n    self.block messages, \"OK\", \"fg=black;bg=green\", padding: true\n  end\n\n  # def table(headers : Enumerable, rows : Enumerable(Enumerable)) : Nil\n  # end\n\n  # :inherit:\n  #\n  # Same as `#puts` but indented one space and an empty line above the message(s).\n  def text(messages : String | Enumerable(String)) : Nil\n    self.auto_prepend_text\n\n    messages = messages.is_a?(Enumerable(String)) ? messages : {messages}\n\n    messages.each do |message|\n      self.puts \" #{message}\"\n    end\n  end\n\n  # :inherit:\n  #\n  # ```text\n  # Some Message\n  # ============\n  # ```\n  #\n  # Orange text with one empty line above/below the title.\n  def title(message : String) : Nil\n    self.auto_prepend_block\n    self.puts \"<comment>#{ACON::Formatter::Output.escape_trailing_backslash message}</>\"\n    self.puts %(<comment>#{\"=\" * ACON::Helper.width(ACON::Helper.remove_decoration(self.formatter, message))}</>)\n    self.new_line\n  end\n\n  # :inherit:\n  #\n  # ```text\n  #  [WARNING] Some Message\n  # ```\n  #\n  # Black text on a 3 line orange background block with an empty line above/below the block.\n  def warning(messages : String | Enumerable(String)) : Nil\n    self.block messages, \"WARNING\", \"fg=black;bg=yellow\", padding: true\n  end\n\n  # :inherit:\n  def progress_start(max : Int32? = nil) : Nil\n    @progress_bar = self.create_progress_bar max\n    self.progress_bar.start\n  end\n\n  # :inherit:\n  def progress_advance(by step : Int32 = 1) : Nil\n    self.progress_bar.advance step\n  end\n\n  # :inherit:\n  def progress_finish : Nil\n    self.progress_bar.finish\n    self.new_line 2\n    @progress_bar = nil\n  end\n\n  def create_progress_bar(max : Int32? = nil) : ACON::Helper::ProgressBar\n    bar = super(max)\n\n    {% if !flag?(:windows) || env(\"TERM_PROGRAM\") == \"Hyper\" %}\n      bar.empty_bar_character = \"░\" # light shade character \\u2591\n      bar.progress_character = \"\"\n      bar.bar_character = \"▓\" # dark shade character \\u2593\n    {% end %}\n\n    bar\n  end\n\n  def progress_iterate(enumerable : Enumerable(T), max : Int32? = nil, & : T -> Nil) : Nil forall T\n    self.create_progress_bar.iterate(enumerable) do |value|\n      yield value\n    end\n\n    self.new_line 2\n  end\n\n  private def auto_prepend_block : Nil\n    chars = @buffered_output.fetch.gsub EOL, \"\\n\"\n\n    if chars.empty?\n      return self.new_line\n    end\n\n    self.new_line 2 - chars.count '\\n'\n  end\n\n  private def auto_prepend_text : Nil\n    fetched = @buffered_output.fetch\n\n    if !fetched.empty? && !fetched.ends_with? \"\\n\"\n      self.new_line\n    end\n  end\n\n  private def create_block(messages : Enumerable(String), type : String? = nil, style : String? = nil, prefix : String = \" \", padding : Bool = false, escape : Bool = true) : Array(String)\n    indent_length = 0\n    prefix_length = ACON::Helper.width ACON::Helper.remove_decoration self.formatter, prefix\n    lines = [] of String\n\n    unless type.nil?\n      type = \"[#{type}] \"\n      indent_length = ACON::Helper.width type\n      line_indentation = \" \" * indent_length\n    end\n\n    output_wrapper = ACON::Helper::OutputWrapper.new\n\n    messages.each_with_index do |message, idx|\n      message = ACON::Formatter::Output.escape message if escape\n\n      lines.concat output_wrapper.wrap(message, @line_length - prefix_length - indent_length, EOL).split EOL\n\n      lines << \"\" if messages.size > 1 && idx < (messages.size - 1)\n    end\n\n    first_line_index = 0\n    if padding && self.decorated?\n      first_line_index = 1\n      lines.unshift \"\"\n      lines << \"\"\n    end\n\n    lines.map_with_index do |line, idx|\n      unless type.nil?\n        line = first_line_index == idx ? \"#{type}#{line}\" : \"#{line_indentation}#{line}\"\n      end\n\n      line = \"#{prefix}#{line}\"\n      line += \" \" * Math.max @line_length - ACON::Helper.width(ACON::Helper.remove_decoration(self.formatter, line)), 0\n\n      if style\n        line = \"<#{style}>#{line}</>\"\n      end\n\n      line\n    end\n  end\n\n  private def write_buffer(message : String | Enumerable(String), new_line : Bool, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n    @buffered_output.write message, new_line, verbosity, output_type\n  end\n\n  # Used in specs\n  protected def progress_bar : ACON::Helper::ProgressBar\n    @progress_bar || raise ACON::Exception::Runtime.new \"The ProgressBar is not started.\"\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/style/interface.cr",
    "content": "# Represents a \"style\" that provides a way to abstract _how_ to format console input/output data\n# such that you can reduce the amount of code needed, and to standardize the appearance.\n#\n# See `ACON::Style::Athena`.\n#\n# ## Custom Styles\n#\n# Custom styles can also be created by implementing this interface, and optionally extending from `ACON::Style::Output`\n# which makes the style an `ACON::Output::Interface` as well as defining part of this interface.\n# Then you could simply instantiate your custom style within a command as you would `ACON::Style::Athena`.\nmodule Athena::Console::Style::Interface\n  # Helper method for asking `ACON::Question` questions.\n  abstract def ask(question : String, default : _)\n\n  # Helper method for asking hidden `ACON::Question` questions.\n  abstract def ask_hidden(question : String)\n\n  # Formats and prints the provided *messages* within a caution block.\n  abstract def caution(messages : String | Enumerable(String)) : Nil\n\n  # Formats and prints the provided *messages* within a comment block.\n  abstract def comment(messages : String | Enumerable(String)) : Nil\n\n  # Helper method for asking `ACON::Question::Confirmation` questions.\n  abstract def confirm(question : String, default : Bool = true) : Bool\n\n  # Formats and prints the provided *messages* within a error block.\n  abstract def error(messages : String | Enumerable(String)) : Nil\n\n  # Helper method for asking `ACON::Question::Choice` questions.\n  abstract def choice(question : String, choices : Indexable | Hash, default = nil)\n\n  # Formats and prints the provided *messages* within a info block.\n  abstract def info(messages : String | Enumerable(String)) : Nil\n\n  # Formats and prints a bulleted list containing the provided *elements*.\n  abstract def listing(elements : Enumerable) : Nil\n\n  # Prints *count* empty new lines.\n  abstract def new_line(count : Int32 = 1) : Nil\n\n  # Formats and prints the provided *messages* within a note block.\n  abstract def note(messages : String | Enumerable(String)) : Nil\n\n  # Creates a section header with the provided *message*.\n  abstract def section(message : String) : Nil\n\n  # Formats and prints the provided *messages* within a success block.\n  abstract def success(messages : String | Enumerable(String)) : Nil\n\n  # Formats and prints the provided *messages* as text.\n  abstract def text(messages : String | Enumerable(String)) : Nil\n\n  # Formats and prints *message* as a title.\n  abstract def title(message : String) : Nil\n\n  # Formats and prints a table based on the provided *headers* and *rows*, followed by a new line.\n  abstract def table(headers : Enumerable, rows : Enumerable) : Nil\n\n  # Starts an internal `ACON::Helper::ProgressBar`, optionally with the provided *max* amount of steps.\n  abstract def progress_start(max : Int32? = nil) : Nil\n\n  # Advances the internal `ACON::Helper::ProgressBar` *by* the provided amount of steps.\n  abstract def progress_advance(by step : Int32 = 1) : Nil\n\n  # Completes the internal `ACON::Helper::ProgressBar`.\n  abstract def progress_finish : Nil\n\n  # Formats and prints the provided *messages* within a warning block.\n  abstract def warning(messages : String | Enumerable(String)) : Nil\nend\n"
  },
  {
    "path": "src/components/console/src/style/output.cr",
    "content": "require \"./interface\"\n\n# Base implementation of `ACON::Style::Interface` and `ACON::Output::Interface` that provides logic common to all styles.\nabstract class Athena::Console::Style::Output\n  include Athena::Console::Style::Interface\n  include Athena::Console::Output::Interface\n\n  @output : ACON::Output::Interface\n\n  def initialize(@output : ACON::Output::Interface); end\n\n  # See `ACON::Output::Interface#decorated?`.\n  def decorated? : Bool\n    @output.decorated?\n  end\n\n  # See `ACON::Output::Interface#decorated=`.\n  def decorated=(decorated : Bool) : Nil\n    @output.decorated = decorated\n  end\n\n  # See `ACON::Output::Interface#formatter`.\n  def formatter : ACON::Formatter::Interface\n    @output.formatter\n  end\n\n  # See `ACON::Output::Interface#formatter=`.\n  def formatter=(formatter : ACON::Formatter::Interface) : Nil\n    @output.formatter = formatter\n  end\n\n  # :inherit:\n  def new_line(count : Int32 = 1) : Nil\n    @output.print EOL * count\n  end\n\n  # Creates and returns an `ACON::Helper::ProgressBar`, optionally with the provided *max* amount of steps.\n  def create_progress_bar(max : Int32? = nil) : ACON::Helper::ProgressBar\n    ACON::Helper::ProgressBar.new @output, max\n  end\n\n  # See `ACON::Output::Interface#puts`.\n  def puts(message, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n    @output.puts message, verbosity, output_type\n  end\n\n  # See `ACON::Output::Interface#print`.\n  def print(message, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil\n    @output.print message, verbosity, output_type\n  end\n\n  # See `ACON::Output::Interface#verbosity`.\n  def verbosity : ACON::Output::Verbosity\n    @output.verbosity\n  end\n\n  # See `ACON::Output::Interface#verbosity=`.\n  def verbosity=(verbosity : ACON::Output::Verbosity) : Nil\n    @output.verbosity = verbosity\n  end\n\n  protected def error_output : ACON::Output::Interface\n    unless (output = @output).is_a? ACON::Output::ConsoleOutputInterface\n      return @output\n    end\n\n    output.error_output\n  end\nend\n"
  },
  {
    "path": "src/components/console/src/terminal.cr",
    "content": "require \"./ext/terminal\"\n\n# :nodoc:\nstruct Athena::Console::Terminal\n  @@width : Int32? = nil\n  @@height : Int32? = nil\n  @@stty : Bool? = nil\n\n  def self.has_stty_available? : Bool\n    @@stty.try { |stty| return stty }\n\n    @@stty = !Process.find_executable(\"stty\").nil?\n  end\n\n  def width : Int32\n    if env_width = ENV[\"COLUMNS\"]?\n      return env_width.to_i\n    end\n\n    if @@width.nil?\n      self.class.init_dimensions\n    end\n\n    @@width || 80\n  end\n\n  def height : Int32\n    if env_height = ENV[\"LINES\"]?\n      return env_height.to_i\n    end\n\n    if @@height.nil?\n      self.class.init_dimensions\n    end\n\n    @@height || 50\n  end\n\n  def size : {Int32, Int32}\n    return self.width, self.height\n  end\n\n  private def self.check_size(size) : Bool\n    if size && (cols = size[0]) && (rows = size[1]) && cols != 0 && rows != 0\n      @@width = cols\n      @@height = rows\n\n      return true\n    end\n\n    false\n  end\n\n  {% if flag?(:win32) %}\n    protected def self.init_dimensions : Nil\n      return if check_size(size_from_screen_buffer)\n      return if check_size(size_from_ansicon)\n    end\n\n    # Detect terminal size Windows `GetConsoleScreenBufferInfo`.\n    private def self.size_from_screen_buffer\n      return unless LibC.GetConsoleScreenBufferInfo(LibC.GetStdHandle(LibC::STDOUT_HANDLE), out csbi)\n\n      cols = csbi.srWindow.right - csbi.srWindow.left + 1\n      rows = csbi.srWindow.bottom - csbi.srWindow.top + 1\n\n      {cols.to_i32, rows.to_i32}\n    end\n\n    # Detect terminal size from Windows ANSICON\n    private def self.size_from_ansicon\n      return unless ENV[\"ANSICON\"]?.to_s =~ /\\((.*)x(.*)\\)/\n\n      rows, cols = [$2, $1].map(&.to_i)\n      {cols, rows}\n    end\n  {% else %}\n    protected def self.init_dimensions : Nil\n      return if self.check_size(self.size_from_ioctl(0)) # STDIN\n      return if self.check_size(self.size_from_ioctl(1)) # STDOUT\n      return if self.check_size(self.size_from_ioctl(2)) # STDERR\n      return if self.check_size(self.size_from_tput)\n      return if self.check_size(self.size_from_stty)\n    end\n\n    # Read terminal size from Unix ioctl\n    private def self.size_from_ioctl(fd)\n      winsize = uninitialized LibC::Winsize\n      ret = LibC.ioctl(fd, LibC::TIOCGWINSZ, pointerof(winsize))\n      return if ret < 0\n\n      {winsize.ws_col.to_i32, winsize.ws_row.to_i32}\n    end\n\n    # Detect terminal size from tput utility\n    private def self.size_from_tput\n      return unless STDOUT.tty?\n\n      lines = `tput lines`.to_i?\n      cols = `tput cols`.to_i?\n\n      {cols, lines}\n    rescue\n      nil\n    end\n\n    # Detect terminal size from stty utility\n    private def self.size_from_stty\n      return unless STDOUT.tty?\n\n      parts = `stty size`.split(/\\s+/)\n      return unless parts.size > 1\n      lines, cols = parts.map(&.to_i?)\n\n      {cols, lines}\n    rescue\n      nil\n    end\n  {% end %}\nend\n"
  },
  {
    "path": "src/components/contracts/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.1.0] - 2025-08-02\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/contracts/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/contracts/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/contracts/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2025 George Dietrich\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": "src/components/contracts/README.md",
    "content": "# Contracts\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/contracts.svg)](https://github.com/athena-framework/contracts/releases)\n\nA set of abstractions extracted out of the Athena components.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Contracts).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/contracts/docs/README.md",
    "content": "A set of abstractions extracted out of the Athena components.\nCan be used to build on semantics that the Athena components proved useful.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-contracts:\n    github: athena-framework/contracts\n    version: ~> 0.1.0\n```\n\n## Usage\n\nThe [Athena::Contracts](/Contracts/top_level/) component provides types and interfaces to achieve loose coupling and interoperability.\nThe intended use case is that other components, or third party libraries, can depend upon the `contracts` component and use its interfaces.\nThen, the code could be usable with any implementation that is also based on them.\nIt could be an Athena component, or another one provided by the greater Crystal community.\n"
  },
  {
    "path": "src/components/contracts/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Contracts\nsite_url: https://athenaframework.org/Contracts/\nrepo_url: https://github.com/athena-framework/contracts\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-contracts/src/athena-contracts.cr\n          source_locations:\n            lib/athena-contracts: https://github.com/athena-framework/contracts/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/contracts/shard.yml",
    "content": "name: athena-contracts\n\nversion: 0.1.0\n\ncrystal: ~> 1.4\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/contracts\n\ndocumentation: https://athenaframework.org/Contracts\n\ndescription: |\n  A set of abstractions extracted out of the Athena components.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n"
  },
  {
    "path": "src/components/contracts/spec/.gitkeep",
    "content": ""
  },
  {
    "path": "src/components/contracts/src/alias.cr",
    "content": "# Convenience alias to make referencing `Athena::Contracts` types easier.\nalias ACTR = Athena::Contracts\n"
  },
  {
    "path": "src/components/contracts/src/athena-contracts.cr",
    "content": "# Main entrypoint that requires _all_ contracts.\n# Does _not_ include common code as those are required by the underlying component,\n# for docs, tests, etc.\n\nrequire \"./event_dispatcher\"\nrequire \"./alias\"\n\n# A set of robust/battle-tested types and interfaces to achieve loose coupling and interoperability.\nmodule Athena::Contracts\n  VERSION = \"0.1.0\"\n\n  # Contracts that relate to the [Athena::EventDispatcher](/EventDispatcher/) component.\n  module EventDispatcher; end\nend\n"
  },
  {
    "path": "src/components/contracts/src/contracts/event_dispatcher/event.cr",
    "content": "require \"./stoppable_event\"\n\n# An event consists of a subclass of this type, usually with extra context specific information.\n# The metaclass of the event type is used as a unique identifier, which generally should end in a verb that indicates what action has been taken.\n#\n# ```\n# # Define a custom event\n# class ExceptionRaisedEvent < ACTR::EventDispatcher::Event\n#   getter exception : Exception\n#\n#   def initialize(@exception : Exception); end\n# end\n#\n# # Dispatch a custom event\n# exception = ArgumentError.new \"Value cannot be negative\"\n# dispatcher.dispatch ExceptionRaisedEvent.new exception\n# ```\n#\n# Abstract event classes may also be used to share common data/methods between a group of related events.\n# However they cannot be used as a catchall to listen on all events that extend it.\n#\n# ## Stopping Propagation\n#\n# In some cases it may make sense for a listener to prevent any other listeners from being called for a specific event.\n# In order to do this, the listener needs a way to tell the dispatcher that it should stop propagation, i.e. do not notify any more listeners.\n# The base event type includes `ACTR::EventDispatcher::StoppableEvent` that enables this behavior.\n# Checkout the related module for more information.\nabstract class Athena::Contracts::EventDispatcher::Event\n  include Athena::Contracts::EventDispatcher::StoppableEvent\nend\n"
  },
  {
    "path": "src/components/contracts/src/contracts/event_dispatcher/interface.cr",
    "content": "# Represents the most basic interface that event dispatchers must implement.\n# Can be further extended to provide additional functionality.\n#\n# All dispatchers:\n#\n# * _MUST_ call listeners synchronously\n# * _MUST_ return the same even object it was originally passed.\n# * _MUST NOT_ return until all listeners have executed.\n# * _MUST_ handle the case where the provided *event* is a `ACTR::EventDispatcher::StoppableEvent`.\nmodule Athena::Contracts::EventDispatcher::Interface\n  # Dispatches the provided *event* to all listeners listening on that event.\n  abstract def dispatch(event : ACTR::EventDispatcher::Event) : ACTR::EventDispatcher::Event\nend\n"
  },
  {
    "path": "src/components/contracts/src/contracts/event_dispatcher/stoppable_event.cr",
    "content": "# An `ACTR::EventDispatcher::Event` whose processing may be interrupted when the event has been handled.\n#\n# `ACTR::EventDispatcher::Interface` implementations *MUST* check to determine if an `ACTR::EventDispatcher::Event` is marked as stopped after each listener is called.\n# If it is, then the dispatcher should return immediately without calling any further listeners.\nmodule Athena::Contracts::EventDispatcher::StoppableEvent\n  @propagation_stopped : Bool = false\n\n  # If future listeners should be executed.\n  def propagate? : Bool\n    !@propagation_stopped\n  end\n\n  # Prevent future listeners from executing once any listener calls `#stop_propagation`.\n  def stop_propagation : Nil\n    @propagation_stopped = true\n  end\nend\n"
  },
  {
    "path": "src/components/contracts/src/event_dispatcher.cr",
    "content": "require \"./alias\"\nrequire \"./contracts/event_dispatcher/*\"\n"
  },
  {
    "path": "src/components/dependency_injection/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/dependency_injection/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/dependency_injection/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.4.5] - 2026-04-19\n\n### Changed\n\n- Improve compile time error messages ([#646]) (George Dietrich) <!-- blacksmoke16 -->\n- Reduce the amount of ivars within `ADI::ServiceContainer` ([#649]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Add ability to define schema configuration maps; with arbitrary keys, but structured values ([#641]) (George Dietrich) <!-- blacksmoke16 -->\n- Add ability to define re-usable schema object types ([#641]) (George Dietrich) <!-- blacksmoke16 -->\n- Add ability for aliases to take constructor parameter names into account ([#660]) (George Dietrich) <!-- blacksmoke16 -->\n- Add support for nested `>>` doc markup when using `object_schema` ([#684]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix global extension schema `Enum` types not retaining their `::` prefix ([#639]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix falsey binding values not resolving ([#647]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix issue with using multiple extensions when one has a nested schema ([#658]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix service argument validation errors overriding schema validation errors ([#659]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix enum typed `object_schema` types not allowing symbol/number values ([#661]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix compile time error when inadvertently using a type name that conflicts with an internal component type ([#678]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix being unable to link to non top-level types within a nested properties' `>>` doc markup ([#684]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.5]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.5\n[#646]: https://github.com/athena-framework/athena/pull/646\n[#649]: https://github.com/athena-framework/athena/pull/649\n[#641]: https://github.com/athena-framework/athena/pull/641\n[#660]: https://github.com/athena-framework/athena/pull/660\n[#684]: https://github.com/athena-framework/athena/pull/684\n[#639]: https://github.com/athena-framework/athena/pull/639\n[#647]: https://github.com/athena-framework/athena/pull/647\n[#658]: https://github.com/athena-framework/athena/pull/658\n[#659]: https://github.com/athena-framework/athena/pull/659\n[#661]: https://github.com/athena-framework/athena/pull/661\n[#678]: https://github.com/athena-framework/athena/pull/678\n\n## [0.4.4] - 2025-09-04\n\n### Changed\n\n- Relax DI argument validation for string parameters ([#548]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.4]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.4\n[#548]: https://github.com/athena-framework/athena/pull/548\n\n## [0.4.3] - 2025-02-08\n\n### Changed\n\n- **Breaking:** prevent auto registering of already registered services ([#520]) (George Dietrich)\n\n### Fixed\n\n- Ensure all array values have proper `#of` type ([#508]) (George Dietrich)\n\n[0.4.3]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.3\n[#508]: https://github.com/athena-framework/athena/pull/508\n[#520]: https://github.com/athena-framework/athena/pull/520\n\n## [0.4.2] - 2025-01-26\n\n_Administrative release, no functional changes_\n\n[0.4.2]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.2\n\n## [0.4.1] - 2024-07-31\n\n### Changed\n\n- **Breaking:** single implementation aliases are now explicit ([#408]) (George Dietrich)\n\n### Fixed\n\n- Fix default/nil values related to `object_of` and `array_of` being unavailable in bundle extensions ([#432]) (George Dietrich)\n\n[0.4.1]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.1\n[#408]: https://github.com/athena-framework/athena/pull/408\n[#432]: https://github.com/athena-framework/athena/pull/432\n\n## [0.4.0] - 2024-04-09\n\n### Changed\n\n- **Breaking:** remove `Clock`, `Console`, and `EventDispatcher` built-in integrations ([#337]) (George Dietrich)\n- **Breaking:** major internal refactor ([#337], [#378]) (George Dietrich)\n- **Breaking:** replace `ADI.auto_configure` with [ADI::Autoconfigure](https://athenaframework.org/DependencyInjection/Autoconfigure/) ([#387]) (George Dietrich)\n- **Breaking:** replace `alias` `ADI::Register` field with [ADI::AsAlias](https://athenaframework.org/DependencyInjection/AsAlias/) ([#389]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- Add ability to easily extend/customize the container ([#337], [#348], [#371], [#372], [#373], [#374], [#377], [#379], [#382], [#383]) (George Dietrich)\n- Add ability to define method calls that should be made during service instantiation ([#384]) (George Dietrich)\n- Add new [ADI::AutoconfigureTag](https://athenaframework.org/DependencyInjection/AutoconfigureTag/) and [ADI::TaggedIterator](https://athenaframework.org/DependencyInjection/TaggedIterator/) to make working with tagged services easier ([#387]) (George Dietrich)\n- Add `ADI.configuration_annotation` to `Athena::DependencyInjection` from `Athena::Config` ([#392]) (George Dietrich)\n\n[0.4.0]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.4.0\n[#337]: https://github.com/athena-framework/athena/pull/337\n[#348]: https://github.com/athena-framework/athena/pull/348\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#371]: https://github.com/athena-framework/athena/pull/371\n[#372]: https://github.com/athena-framework/athena/pull/372\n[#373]: https://github.com/athena-framework/athena/pull/373\n[#374]: https://github.com/athena-framework/athena/pull/374\n[#377]: https://github.com/athena-framework/athena/pull/377\n[#378]: https://github.com/athena-framework/athena/pull/378\n[#379]: https://github.com/athena-framework/athena/pull/379\n[#382]: https://github.com/athena-framework/athena/pull/382\n[#383]: https://github.com/athena-framework/athena/pull/383\n[#384]: https://github.com/athena-framework/athena/pull/384\n[#387]: https://github.com/athena-framework/athena/pull/387\n[#389]: https://github.com/athena-framework/athena/pull/389\n[#392]: https://github.com/athena-framework/athena/pull/392\n\n## [0.3.8] - 2023-12-16\n\n### Fixed\n\n- Avoid depending directly on Crystal macro types ([#335]) (George Dietrich)\n\n[0.3.8]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.8\n[#335]: https://github.com/athena-framework/athena/pull/335\n\n## [0.3.7] - 2023-10-09\n\n### Added\n\n- Add integration between `Athena::DependencyInjection` and the `Athena::Clock` component ([#318]) (George Dietrich)\n\n[0.3.7]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.7\n[#318]: https://github.com/athena-framework/athena/pull/318\n\n## [0.3.6] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.3.6]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.6\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.3.5] - 2023-02-04\n\n### Added\n\n- Add better integration between `Athena::DependencyInjection` and the `Athena::Console` and `Athena::EventDispatcher` components ([#259]) (George Dietrich)\n\n[0.3.5]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.5\n[#259]: https://github.com/athena-framework/athena/pull/259\n\n## [0.3.4] - 2023-01-07\n\n### Changed\n\n- Refactor various internal logic (George Dietrich)\n\n[0.3.4]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.4\n\n## [0.3.3] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.3\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.3.2] - 2021-10-30\n\n### Changed\n\n- Unused services are now excluded from the container ([#30]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.2\n[#30]: https://github.com/athena-framework/dependency-injection/pull/30\n\n## [0.3.1] - 2021-03-28\n\n### Fixed\n\n- Fix error with untyped parameters with default values injecting ([#28]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.1\n[#28]: https://github.com/athena-framework/dependency-injection/pull/28\n\n## [0.3.0] - 2021-03-20\n\n### Added\n\n- Allow injecting [configuration](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--configuration) into services ([#27]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.3.0\n[#27]: https://github.com/athena-framework/dependency-injection/pull/27\n\n## [0.2.6] - 2021-03-15\n\n### Added\n\n- Allow using the `ADI::Inject` annotation on class methods to create [factories](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--factories) ([#25]) (George Dietrich)\n\n[0.2.6]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.6\n[#25]: https://github.com/athena-framework/dependency-injection/pull/25\n\n## [0.2.5] - 2021-01-30\n\n### Changed\n\n- Migrate documentation to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#23], [#24]) (George Dietrich)\n\n[0.2.5]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.5\n[#23]: https://github.com/athena-framework/dependency-injection/pull/23\n[#24]: https://github.com/athena-framework/dependency-injection/pull/24\n\n## [0.2.4] - 2021-01-29\n\n### Added\n\n- Add dependency on `athena-framework/config` ([#20]) (George Dietrich)\n- Add support for injecting [parameters](https://athenaframework.org/architecture/config/#parameters) into a service ([#20]) (George Dietrich)\n- Add support for [service proxies](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--service-proxies) ([#21]) (George Dietrich)\n\n### Removed\n\n- Remove the `lazy` `ADI::Register` field. All services are lazy by default now ([#21]) (George Dietrich)\n\n### Fixed\n\n- Fix issue building documentation ([#22]) (George Dietrich)\n\n[0.2.4]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.4\n[#20]: https://github.com/athena-framework/dependency-injection/pull/20\n[#21]: https://github.com/athena-framework/dependency-injection/pull/21\n[#22]: https://github.com/athena-framework/dependency-injection/pull/22\n\n## [0.2.3] - 2020-12-24\n\n### Fixed\n\n- Fix error when a parameter has a default value after an array parameter ([#19]) (George Dietrich)\n\n[0.2.3]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.3\n[#19]: https://github.com/athena-framework/dependency-injection/pull/19\n\n## [0.2.2] - 2020-12-03\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#18]) (George Dietrich)\n\n[0.2.2]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.2\n[#18]: https://github.com/athena-framework/dependency-injection/pull/18\n\n## [0.2.1] - 2020-11-14\n\n### Added\n\n- Add a mock container instance to allow mocking services ([#15]) (George Dietrich)\n- Add ability to customize the type of a service within the container ([#15]) (George Dietrich)\n- Add support for [factory pattern](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--factories) constructors ([#16]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.1\n[#15]: https://github.com/athena-framework/dependency-injection/pull/15\n[#16]: https://github.com/athena-framework/dependency-injection/pull/16\n\n## [0.2.0] - 2020-06-09\n\n_Major refactor of the component._\n\n### Added\n\n- Add concept of [aliasing services](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--aliasing-services) ([#10]) (George Dietrich)\n- Add concept of [binding values](https://athenaframework.org/DependencyInjection/#Athena::DependencyInjection:bind(key,value)) ([#10]) (George Dietrich)\n- Add concept of [auto configuration](https://athenaframework.org/DependencyInjection/#Athena::DependencyInjection:auto_configure(type,options)) ([#10]) (George Dietrich)\n- Add [ADI::Inject](https://athenaframework.org/DependencyInjection/Inject/) annotation ([#10]) (George Dietrich)\n- Add support for [generic services](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--generic-services) ([#10]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** manually provided arguments now need to be prefixed with a `_` ([#10]) (George Dietrich)\n- **Breaking:** service names are now based on the `FQN` of the type, downcase underscored by default ([#10]) (George Dietrich)\n- Updated [optional services](https://athenaframework.org/DependencyInjection/Register/#Athena::DependencyInjection::Register--optional-services) to now be based on the type/default value of the parameter ([#10]) (George Dietrich)\n- Service dependencies are now resolved automatically, removes need to manually provide them ([#10]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** remove the `ADI::Service` module ([#10]) (George Dietrich)\n- **Breaking:** remove the `ADI::Injectable` module ([#10]) (George Dietrich)\n- **Breaking:** remove the `@?` syntax ([#10]) (George Dietrich)\n- **Breaking:** remove the `#get`, `#has`, `#resolve`, `#tagged`, and `#tags` methods from `ADI::ServiceContainer` ([#10]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.2.0\n[#10]: https://github.com/athena-framework/dependency-injection/pull/10\n\n## [0.1.3] - 2020-04-06\n\n### Fixed\n\n- Fix an edge case by checking includers via `<=` ([#7]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.1.3\n[#7]: https://github.com/athena-framework/dependency-injection/pull/7\n\n## [0.1.2] - 2020-02-22\n\n### Changed\n\n- Change type resolution logic to operate at compile time instead of runtime ([#6]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.1.2\n[#6]: https://github.com/athena-framework/dependency-injection/pull/6\n\n## [0.1.1] - 2020-02-06\n\n### Added\n\n- Add the ability to redefine services ([#4]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.1.1\n[#4]: https://github.com/athena-framework/dependency-injection/pull/4\n\n## [0.1.0] - 2020-01-31\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/dependency-injection/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/dependency_injection/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/dependency_injection/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 George Dietrich\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": "src/components/dependency_injection/README.md",
    "content": "# Dependency Injection\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/dependency-injection.svg)](https://github.com/athena-framework/dependency-injection/releases)\n\nRobust dependency injection service container framework.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/DependencyInjection).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/dependency_injection/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.4.1\n\n### Single implementation aliases are now explicit\n\nPreviously if you had a service that implemented an interface (module), that interface would auto resolve to that service if there was only the one implementation.\nThis implicit aliasing of the interface was removed and now requires an explicit aliasing.\n\nBefore:\n```crystal\nmodule SomeInterface; end\n\n@[ADI::Register]\nclass Foo\n  include SomeInterface\nend\n\n@[ADI::Register(public: true)]\nrecord Bar, a : SomeInterface\n```\n\nAfter:\n```crystal\nmodule SomeInterface; end\n\n@[ADI::Register]\n@[ADI::AsAlias]\nclass Foo\n  include SomeInterface\nend\n\n@[ADI::Register(public: true)]\nrecord Bar, a : SomeInterface\n```\n\nIf the service only implements a single interface, you just need to apply the [`@[ADI::AsAlias]`](https://athenaframework.org/DependencyInjection/AsAlias/) annotation to the service.\nIf it implements more than one interface, you'll need an annotation for each one.\nSee the API docs for more information on how to use the annotation.\n\n## Upgrade to 0.4.0\n\n_The component went through a large internal refactor as part of this release. Please create an issue or PR if you find a breaking change not captured here._\n\n### Remove built-in integrations\n\nThe built-in `Clock`, `Console`, and `EventDispatcher` integrations have been removed. The Athena Framework is now responsible for the integration of all the components. If you were using one of these components with the `DependencyInjection` component outside of the framework, you will need to manually handle wiring things up.\n\n### Remove `ADI.auto_configure`\n\nThe `ADI.auto_configure` macro has been replaced with the `ADI::Autoconfigure` annotation.\n\nBefore:\n```crystal\nmodule ConfigInterface; end\n\nADI.auto_configure ConfigInterface, {tags: [\"config\"]}\n```\n\nAfter:\n```crystal\n@[ADI::Autoconfigure(tags: [\"config\"])]\nmodule ConfigInterface; end\n```\n\nSee the [API Docs](https://athenaframework.org/DependencyInjection/Autoconfigure/) for more information.\n\n### Remove `alias` field of `ADI::Register`\n\nService aliases are no longer defined via the `alias` field as part of the `ADI::Register` annotation. Instead, they are now handled via the new `ADI::AsAlias` annotation.\n\nBefore:\n```crystal\nmodule TransformerInterface\n  abstract def transform(value : String) : String\nend\n\n@[ADI::Register(alias: TransformerInterface)]\nstruct ShoutTransformer\n  include TransformerInterface\n\n  # ...\nend\n```\n\nAfter:\n```crystal\nmodule TransformerInterface\n  abstract def transform(value : String) : String\nend\n\n@[ADI::Register]\n@[ADI::AsAlias(TransformerInterface)]\nstruct ShoutTransformer\n  include TransformerInterface\n\n  # ...\nend\n```\n\nSee the [API Docs](https://athenaframework.org/DependencyInjection/AsAlias/) for more information.\n"
  },
  {
    "path": "src/components/dependency_injection/docs/README.md",
    "content": "The `Athena::DependencyInjection` component provides a robust dependency injection service container framework.\nSome of the reasoning for how this can/would be useful is called out in the [Why Athena?](/why_athena) page.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-dependency_injection:\n    github: athena-framework/dependency-injection\n    version: ~> 0.4.0\n```\n\n## Usage\n\nA special class called the `ADI::ServiceContainer` (SC) stores useful objects, aka services, that can be shared throughout the project.\nThe SC is lazily initialized on fibers; this allows the SC to be accessed anywhere within the project.\nThe [ADI.container](/DependencyInjection/top_level/#Athena::DependencyInjection.container) method will return the SC for the current fiber.\n\nIf you are a user of a project/framework making use of this component, checkout [ADI::Register](/DependencyInjection/Register/) as most of all the information you need is documented there.\n\nOtherwise, if you are the creator/maintainer of a project wishing to integrate this component,\nthe best way to integrate/use this component depends on the execution flow of your application, and how it uses [Fibers](https://crystal-lang.org/api/Fiber.html).\nSince each fiber has its own container instance, if your application only uses Crystal's main fiber and is short lived, then you most likely only need to set up your services\nand expose one of them as [public](/DependencyInjection/Register/#Athena::DependencyInjection::Register--optional-arguments) to serve as the entry point.\n\nIf your application is meant to be long lived, such as using a [HTTP::Server](https://crystal-lang.org/api/HTTP/Server.html), then you will want to ensure that each\nfiber is truly independent from one another, with them not being reused or sharing state external to the container.\nAn example of this is how `HTTP::Server` reuses fibers for `connection: keep-alive` requests.\nBecause of this, or in cases similar to, you may want to manually reset the container via `Fiber.current.container = ADI::ServiceContainer.new`.\n\n"
  },
  {
    "path": "src/components/dependency_injection/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Dependency Injection\nsite_url: https://athenaframework.org/DependencyInjection/\nrepo_url: https://github.com/athena-framework/dependency-injection\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-dependency_injection/src/athena-dependency_injection.cr\n          source_locations:\n            lib/athena-dependency_injection: https://github.com/athena-framework/dependency-injection/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/dependency_injection/shard.yml",
    "content": "name: athena-dependency_injection\n\nversion: 0.4.5\n\ncrystal: ~> 1.19\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/dependency-injection\n\ndocumentation: https://athenaframework.org/DependencyInjection\n\ndescription: |\n  Robust dependency injection service container framework.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n"
  },
  {
    "path": "src/components/dependency_injection/spec/abstract_bundle_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, <<-CR, line: line\n    require \"./spec_helper.cr\"\n    #{code}\n  CR\nend\n\ndescribe ADI::AbstractBundle do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"when the bundle does not inherit from ADI::AbstractBundle\" do\n      assert_compile_time_error \"The provided bundle 'String' be inherit from 'ADI::AbstractBundle'.\", <<-CR\n        ADI.register_bundle String\n      CR\n    end\n\n    it \"when the bundle does not provide its name\" do\n      assert_compile_time_error \"Unable to determine extension name. It was not provided as the first positional argument nor via the 'name' field.\", <<-CR\n        @[ADI::Bundle]\n        struct MyBundle < ADI::AbstractBundle\n        end\n\n        ADI.register_bundle MyBundle\n      CR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/athena-dependency_injection_spec.cr",
    "content": "require \"./spec_helper\"\n\n@[ADI::Register(public: true)]\nclass ValueStore\n  property value : Int32 = 1\nend\n\ndescribe Athena::DependencyInjection do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"errors when passing a non-module to add_compiler_pass\" do\n      ASPEC::Methods.assert_compile_time_error \"Pass type must be a module.\", <<-CR\n        require \"./spec_helper.cr\"\n\n        ADI.add_compiler_pass String\n      CR\n    end\n  end\n\n  describe \".container\" do\n    it \"returns a container\" do\n      ADI.container.should be_a ADI::ServiceContainer\n    end\n\n    it \"returns a fiber specific container\" do\n      channel = Channel(Int32).new\n\n      container = ADI.container\n\n      spawn do\n        inner_container = ADI.container\n        inner_container.value_store.value = 2\n        channel.send inner_container.value_store.value\n      end\n\n      channel.receive.should_not eq container.value_store.value\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/auto_wire_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\")\nend\n\nmodule AutoWireInterface; end\n\n@[ADI::Register]\nrecord AutoWireOne do\n  include AutoWireInterface\nend\n\n@[ADI::Register]\nrecord AutoWireTwo do\n  include AutoWireInterface\nend\n\n@[ADI::Register(public: true)]\nrecord AutoWireService, auto_wire_two : AutoWireInterface\n\nmodule SameInstanceAliasInterface; end\n\n@[ADI::Register]\n@[ADI::AsAlias]\nclass SameInstancePrimary\n  include SameInstanceAliasInterface\nend\n\n@[ADI::Register(public: true)]\nrecord SameInstanceClient, a : SameInstancePrimary, b : SameInstanceAliasInterface\n\n# Named alias tests\nmodule NamedAliasInterface; end\n\n@[ADI::Register]\n@[ADI::AsAlias(NamedAliasInterface, name: \"file_logger\")]\nclass FileLoggerImpl\n  include NamedAliasInterface\nend\n\n@[ADI::Register]\n@[ADI::AsAlias(NamedAliasInterface, name: \"console_logger\")]\nclass ConsoleLoggerImpl\n  include NamedAliasInterface\nend\n\n@[ADI::Register(public: true)]\nrecord NamedAliasService, file_logger : NamedAliasInterface, console_logger : NamedAliasInterface\n\n# Fallback alias tests\nmodule FallbackInterface; end\n\n@[ADI::Register]\n@[ADI::AsAlias(FallbackInterface, name: \"specific\")]\nclass SpecificImpl\n  include FallbackInterface\nend\n\n@[ADI::Register]\n@[ADI::AsAlias(FallbackInterface)]\nclass DefaultImpl\n  include FallbackInterface\nend\n\n@[ADI::Register(public: true)]\nrecord FallbackService, specific : FallbackInterface, other : FallbackInterface\n\ndescribe ADI::ServiceContainer do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"does not resolve an un-aliased interface when there is only 1 implementation\" do\n      assert_compile_time_error \"Failed to resolve argument for service 'bar' (Bar).\", <<-CR\n        module SomeInterface; end\n\n        @[ADI::Register]\n        class Foo\n          include SomeInterface\n        end\n\n        @[ADI::Register(public: true)]\n        record Bar, a : SomeInterface\n\n        ADI.container.bar\n      CR\n    end\n  end\n\n  it \"resolves the service with a matching constructor name\" do\n    ADI.container.auto_wire_service.auto_wire_two.should be_a AutoWireTwo\n  end\n\n  it \"resolves aliases to the same underlying instance\" do\n    service = ADI.container.same_instance_client\n    service.a.should be service.b\n  end\n\n  it \"resolves named aliases by parameter name\" do\n    service = ADI.container.named_alias_service\n    service.file_logger.should be_a FileLoggerImpl\n    service.console_logger.should be_a ConsoleLoggerImpl\n  end\n\n  it \"falls back to type-only alias when no named match\" do\n    service = ADI.container.fallback_service\n    service.specific.should be_a SpecificImpl\n    service.other.should be_a DefaultImpl\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/define_getters_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\")\nend\n\nprivate def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compiles code, line: line, preamble: %(require \"../spec_helper.cr\")\nend\n\nmodule PublicStringAliasInterface; end\n\n@[ADI::Register]\n@[ADI::AsAlias(\"bar_string_alias\", public: true)]\nclass PublicStringAlias\n  include PublicStringAliasInterface\nend\n\nmodule TypedGetterAliasInterface; end\n\n@[ADI::Register]\n@[ADI::AsAlias(public: true)]\nclass TypedGetterAlias\n  include TypedGetterAliasInterface\nend\n\nmodule ArrayServiceInterface; end\n\n@[ADI::Register]\nstruct ArrayOne\n  include ArrayServiceInterface\nend\n\n@[ADI::Register]\nstruct ArrayTwo\n  include ArrayServiceInterface\nend\n\n@[ADI::Register]\nstruct ArrayThree\n  include ArrayServiceInterface\nend\n\n@[ADI::Register(_services: [\"@array_one\", \"@array_three\"], public: true)]\nrecord ImplicitArrayClient, services : Array(ArrayServiceInterface)\n\n@[ADI::Register(public: true)]\nrecord ExplicitArrayClient, services : Array(ArrayServiceInterface) = [] of ArrayServiceInterface\n\ndescribe ADI::ServiceContainer::DefineGetters, tags: \"compiled\" do\n  describe \"compiler errors\" do\n    describe \"aliases\" do\n      it \"does not expose named getter for non-public string aliases\" do\n        assert_compile_time_error \"undefined method 'bar' for Athena::DependencyInjection::ServiceContainer\", <<-'CR'\n          module SomeInterface; end\n\n          @[ADI::Register]\n          @[ADI::AsAlias(\"bar\")]\n          class Foo\n            include SomeInterface\n          end\n\n          ADI.container.bar\n        CR\n      end\n\n      it \"does not expose typed getter for non-public typed aliases\" do\n        assert_compile_time_error \"undefined method 'get' for Athena::DependencyInjection::ServiceContainer\", <<-'CR'\n          module SomeInterface; end\n\n          @[ADI::Register]\n          @[ADI::AsAlias]\n          class Foo\n            include SomeInterface\n          end\n\n          ADI.container.get SomeInterface\n        CR\n      end\n    end\n  end\n\n  it \"compiles when a ServiceContainer type conflicts with internal ADI types\" do\n    assert_compiles <<-CR\n      @[ADI::Register(public: true)]\n      class ServiceContainer::Foo\n      end\n    CR\n  end\n\n  describe \"aliases\" do\n    it \"exposes named getter for public string alias\" do\n      ADI.container.bar_string_alias.should be_a PublicStringAlias\n    end\n\n    it \"exposes typed getter for public typed alias\" do\n      ADI.container.get(TypedGetterAliasInterface).should be_a TypedGetterAlias\n    end\n\n    it \"implicitly applies `of Type` restrictions to array values\" do\n      ADI.container.implicit_array_client.services.should eq [ArrayOne.new, ArrayThree.new]\n    end\n\n    it \"does not apply `of Type` restriction to values that already explicitly have one\" do\n      ADI.container.explicit_array_client.services.should be_empty\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/inline_service_definitions_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compiles code, line: line, preamble: %(require \"../spec_helper.cr\")\nend\n\n# 1. Basic inlining: single-use private service\n@[ADI::Register]\nclass BasicInlinedDep\n  def value\n    42\n  end\nend\n\n@[ADI::Register(public: true)]\nclass BasicInlineClient\n  def initialize(@dep : BasicInlinedDep)\n  end\n\n  def value\n    @dep.value\n  end\nend\n\n# 2. Nested inlining: chain of inlined services\n@[ADI::Register]\nclass NestedInlineDep1\n  def value\n    1\n  end\nend\n\n@[ADI::Register]\nclass NestedInlineDep2\n  def initialize(@dep : NestedInlineDep1)\n  end\n\n  def value\n    @dep.value + 1\n  end\nend\n\n@[ADI::Register(public: true)]\nclass NestedInlineClient\n  def initialize(@dep : NestedInlineDep2)\n  end\n\n  def value\n    @dep.value\n  end\nend\n\n# 3. Public alias target - should NOT be inlined\nmodule InlineTestAliasInterface; end\n\n@[ADI::Register]\n@[ADI::AsAlias(\"inline_test_alias\", public: true)]\nclass InlineAliasTargetService\n  include InlineTestAliasInterface\n\n  def value\n    100\n  end\nend\n\n@[ADI::Register(public: true)]\nclass InlineAliasClient\n  def initialize(@dep : InlineAliasTargetService)\n  end\n\n  def value\n    @dep.value\n  end\nend\n\n# 4. Proxy target - should NOT be inlined\n@[ADI::Register]\nclass InlineProxyTargetService\n  def value\n    200\n  end\nend\n\n@[ADI::Register(public: true)]\nclass InlineProxyClient\n  def initialize(@proxy : ADI::Proxy(InlineProxyTargetService))\n  end\n\n  def value\n    @proxy.value\n  end\nend\n\n# 5. Public service - should NOT be inlined\n@[ADI::Register(public: true)]\nclass InlinePublicDepService\n  def value\n    300\n  end\nend\n\n@[ADI::Register(public: true)]\nclass InlinePublicDepClient\n  def initialize(@dep : InlinePublicDepService)\n  end\n\n  def value\n    @dep.value\n  end\nend\n\n# 6. Multiple references - should NOT be inlined\n@[ADI::Register]\nclass InlineMultiRefService\n  def value\n    400\n  end\nend\n\n@[ADI::Register(public: true)]\nclass InlineMultiRefClient1\n  def initialize(@dep : InlineMultiRefService)\n  end\n\n  def value\n    @dep.value\n  end\nend\n\n@[ADI::Register(public: true)]\nclass InlineMultiRefClient2\n  def initialize(@dep : InlineMultiRefService)\n  end\n\n  def value\n    @dep.value + 1\n  end\nend\n\n# 7. Factory method inlining\n@[ADI::Register(factory: \"create\")]\nclass FactoryInlineService\n  getter value : Int32\n\n  def initialize(@value : Int32)\n  end\n\n  def self.create\n    new(500)\n  end\nend\n\n@[ADI::Register(public: true)]\nclass FactoryInlineClient\n  def initialize(@dep : FactoryInlineService)\n  end\n\n  def value\n    @dep.value\n  end\nend\n\n# 8. Calls argument inlining\n@[ADI::Register]\nclass CallsInlineService\n  def value\n    600\n  end\nend\n\n@[ADI::Register(public: true, calls: [{\"set_service\", {calls_inline_service}}])]\nclass CallsInlineClient\n  getter service : CallsInlineService?\n\n  def set_service(@service : CallsInlineService)\n  end\nend\n\n# 9. Inlined service with array parameter containing inlined services\n# This exercises the array handling code paths in inline_service_definitions.cr\nmodule InlineArrayInterface\n  abstract def value : Int32\nend\n\n@[ADI::Register]\nclass ArrayLeafService1\n  include InlineArrayInterface\n\n  def value : Int32\n    10\n  end\nend\n\n@[ADI::Register]\nclass ArrayLeafService2\n  include InlineArrayInterface\n\n  def value : Int32\n    20\n  end\nend\n\n# This service is private + single-use, so it gets inlined into ArrayParentClient\n# It takes an array of inlined services, exercising array handling code paths\n@[ADI::Register(_items: [\"@array_leaf_service1\", \"@array_leaf_service2\"])]\nclass ArrayMiddleService\n  def initialize(@items : Array(InlineArrayInterface))\n  end\n\n  def total\n    @items.sum(&.value)\n  end\nend\n\n@[ADI::Register(public: true)]\nclass ArrayParentClient\n  def initialize(@middle : ArrayMiddleService)\n  end\n\n  def total\n    @middle.total\n  end\nend\n\n# 10. Inlined service with calls: containing inlined service args\n# This exercises the calls argument handling code paths\n@[ADI::Register]\nclass CallsLeafService\n  def value\n    700\n  end\nend\n\n# This service is private + single-use, gets inlined into CallsParentClient\n# It uses calls: with an inlined service arg\n@[ADI::Register(calls: [{\"set_leaf\", {calls_leaf_service}}])]\nclass CallsMiddleService\n  getter leaf : CallsLeafService?\n\n  def set_leaf(@leaf : CallsLeafService)\n  end\n\n  def value\n    @leaf.not_nil!.value\n  end\nend\n\n@[ADI::Register(public: true)]\nclass CallsParentClient\n  def initialize(@middle : CallsMiddleService)\n  end\n\n  def value\n    @middle.value\n  end\nend\n\n# 11. Service with no-arg method call\n# This exercises the no-args branch in define_getters.cr\n@[ADI::Register(public: true, calls: [{\"init\"}])]\nclass NoArgCallClient\n  getter initialized = false\n\n  def init\n    @initialized = true\n  end\nend\n\n# 12. Inlined service with array containing mix of inlined and non-inlined services\n@[ADI::Register(public: true)]\nclass MixedArrayPublicService\n  include InlineArrayInterface\n\n  def value : Int32\n    100\n  end\nend\n\n@[ADI::Register]\nclass MixedArrayInlinedService\n  include InlineArrayInterface\n\n  def value : Int32\n    50\n  end\nend\n\n# This service is private + single-use, so gets inlined\n# Its array has a mix: one inlined service, one public (non-inlined) service\n@[ADI::Register(_items: [\"@mixed_array_inlined_service\", \"@mixed_array_public_service\"])]\nclass MixedArrayMiddleService\n  def initialize(@items : Array(InlineArrayInterface))\n  end\n\n  def total\n    @items.sum(&.value)\n  end\nend\n\n@[ADI::Register(public: true)]\nclass MixedArrayClient\n  def initialize(@middle : MixedArrayMiddleService)\n  end\n\n  def total\n    @middle.total\n  end\nend\n\n# 13. Out-of-order dependency to exercise re-queue logic\n# Define the dependent service BEFORE its dependency to test re-queue\n# OrderingMiddleService depends on OrderingDepService but is defined first\n\n@[ADI::Register]\nclass OrderingMiddleService\n  def initialize(@dep : OrderingDepService)\n  end\n\n  def value\n    @dep.value\n  end\nend\n\n@[ADI::Register]\nclass OrderingDepService\n  def value\n    999\n  end\nend\n\n@[ADI::Register(public: true)]\nclass OrderingTestClient\n  def initialize(@middle : OrderingMiddleService)\n  end\n\n  def value\n    @middle.value\n  end\nend\n\n# 14. Out-of-order array element dependency\n# ArrayOrderingMiddle depends on ArrayOrderingDep via array, but is defined first\n@[ADI::Register(_items: [\"@array_ordering_dep\"])]\nclass ArrayOrderingMiddle\n  def initialize(@items : Array(InlineArrayInterface))\n  end\n\n  def total\n    @items.sum(&.value)\n  end\nend\n\n@[ADI::Register]\nclass ArrayOrderingDep\n  include InlineArrayInterface\n\n  def value : Int32\n    777\n  end\nend\n\n@[ADI::Register(public: true)]\nclass ArrayOrderingClient\n  def initialize(@middle : ArrayOrderingMiddle)\n  end\n\n  def total\n    @middle.total\n  end\nend\n\n# 15. Out-of-order call arg dependency\n# CallOrderingMiddle has call with arg CallOrderingDep, but is defined first\n@[ADI::Register(calls: [{\"set_dep\", {call_ordering_dep}}])]\nclass CallOrderingMiddle\n  getter dep : CallOrderingDep?\n\n  def set_dep(@dep : CallOrderingDep)\n  end\n\n  def value\n    @dep.not_nil!.value\n  end\nend\n\n@[ADI::Register]\nclass CallOrderingDep\n  def value\n    888\n  end\nend\n\n@[ADI::Register(public: true)]\nclass CallOrderingClient\n  def initialize(@middle : CallOrderingMiddle)\n  end\n\n  def value\n    @middle.value\n  end\nend\n\n# 16. Inlined service with call arg that is NOT an inlined service\n@[ADI::Register(public: true)]\nclass NonInlinedCallArgService\n  def value\n    800\n  end\nend\n\n# This service is private + single-use, gets inlined\n# Its call arg references a PUBLIC service (not inlined)\n@[ADI::Register(calls: [{\"set_dep\", {non_inlined_call_arg_service}}])]\nclass NonInlinedCallArgMiddleService\n  getter dep : NonInlinedCallArgService?\n\n  def set_dep(@dep : NonInlinedCallArgService)\n  end\n\n  def value\n    @dep.not_nil!.value\n  end\nend\n\n@[ADI::Register(public: true)]\nclass NonInlinedCallArgClient\n  def initialize(@middle : NonInlinedCallArgMiddleService)\n  end\n\n  def value\n    @middle.value\n  end\nend\n\ndescribe ADI::ServiceContainer::InlineServiceDefinitions do\n  it \"compiles when a ServiceContainer type conflicts with internal ADI types\", tags: \"compiled\" do\n    assert_compiles <<-CR\n      @[ADI::Register]\n      class ServiceContainer::Foo\n      end\n\n      @[ADI::Register(public: true)]\n      class BasicInlineClient\n        def initialize(@dep : ::ServiceContainer::Foo)\n        end\n      end\n\n      ADI.container.basic_inline_client\n    CR\n  end\n\n  it \"inlines single-use private services\" do\n    ADI.container.basic_inline_client.value.should eq 42\n  end\n\n  it \"supports nested inlined services\" do\n    ADI.container.nested_inline_client.value.should eq 2\n  end\n\n  it \"works with public alias targets\" do\n    ADI.container.inline_alias_client.value.should eq 100\n  end\n\n  it \"works with proxy targets\" do\n    ADI.container.inline_proxy_client.value.should eq 200\n  end\n\n  it \"works with public services as dependencies\" do\n    ADI.container.inline_public_dep_client.value.should eq 300\n  end\n\n  it \"works with multi-referenced services\" do\n    ADI.container.inline_multi_ref_client1.value.should eq 400\n    ADI.container.inline_multi_ref_client2.value.should eq 401\n  end\n\n  it \"supports factory methods for inlined services\" do\n    ADI.container.factory_inline_client.value.should eq 500\n  end\n\n  it \"supports inlined services passed via calls\" do\n    ADI.container.calls_inline_client.service.not_nil!.value.should eq 600\n  end\n\n  it \"supports inlined services with array parameters containing inlined services\" do\n    ADI.container.array_parent_client.total.should eq 30\n  end\n\n  it \"supports inlined services with calls using inlined service args\" do\n    ADI.container.calls_parent_client.value.should eq 700\n  end\n\n  it \"supports method calls without arguments\" do\n    ADI.container.no_arg_call_client.initialized.should be_true\n  end\n\n  it \"supports inlined services with mixed array params (inlined and non-inlined)\" do\n    ADI.container.mixed_array_client.total.should eq 150\n  end\n\n  it \"supports inlined services with non-inlined call args\" do\n    ADI.container.non_inlined_call_arg_client.value.should eq 800\n  end\n\n  it \"handles out-of-order dependency processing\" do\n    ADI.container.ordering_test_client.value.should eq 999\n  end\n\n  it \"handles out-of-order array element dependencies\" do\n    ADI.container.array_ordering_client.total.should eq 777\n  end\n\n  it \"handles out-of-order call arg dependencies\" do\n    ADI.container.call_ordering_client.value.should eq 888\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/merge_configs_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ADI::ServiceContainer::MergeConfigs do\n  it \"deep merges consecutive `ADI.configure` call\", tags: \"compiled\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        property default_locale : String\n\n        module Cors\n          include ADI::Extension::Schema\n\n          module Defaults\n            include ADI::Extension::Schema\n\n            property allow_credentials : Bool = false\n            property allow_origin : Array(String) = [] of String\n          end\n        end\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          cors: {\n            defaults: {\n              allow_credentials: false,\n              allow_origin:      [\"*\"] of String,\n            },\n          },\n        },\n      })\n\n      ADI.configure({\n        test: {\n          cors: {\n            defaults: {\n              allow_credentials: true,\n            },\n          },\n        },\n      })\n\n      ADI.configure({\n        test: {\n          default_locale: \"en\",\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"test\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"default_locale\"] == \"en\" }}, \"Expected default_locale to be en\")\n          ASPEC.compile_time_assert(\\{{ config[\"cors\"][\"defaults\"][\"allow_credentials\"] == true }}, \"Expected allow_credentials to be true\")\n          ASPEC.compile_time_assert(\\{{ config[\"cors\"][\"defaults\"][\"allow_origin\"].size == 1 }}, \"Expected allow_origin size to be 1\")\n          ASPEC.compile_time_assert(\\{{ config[\"cors\"][\"defaults\"][\"allow_origin\"][0] == \"*\" }}, \"Expected allow_origin[0] to be *\")\n        end\n      end\n    CR\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/merge_extension_config_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\ndescribe ADI::ServiceContainer::MergeExtensionConfig, tags: \"compiled\" do\n  describe \"compiler errors\" do\n    describe \"root level\" do\n      it \"errors if a required configuration value has not been provided\" do\n        assert_compile_time_error \"Required configuration property 'test.id : Int32' must be provided.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            property id : Int32\n            property name : String\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              name: \"Fred\"\n            }\n          })\n        CR\n      end\n\n      it \"errors if there is a collection type mismatch\" do\n        assert_compile_time_error \"Expected configuration value 'test.foo' to be a 'Array(Int32)', but got 'Array(String)'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            property foo : Array(Int32)\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              foo: [] of String\n            }\n          })\n        CR\n      end\n\n      it \"errors if there is a type mismatch within an array\" do\n        assert_compile_time_error \"Expected configuration value 'test.foo[0]' to be a 'Int32', but got 'UInt64'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            property foo : Array(Int32)\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              foo: [10_u64] of Int32\n            }\n          })\n        CR\n      end\n\n      it \"errors if a configuration value not found in the schema is encountered\" do\n        assert_compile_time_error \"Unexpected property 'test.name'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            property id : Int64\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              id: 10,\n              name: \"Fred\"\n            }\n          })\n        CR\n      end\n    end\n\n    describe \"nested level\" do\n      it \"errors if a configuration value has the incorrect type\" do\n        assert_compile_time_error \"Required configuration property 'test.sub_config.defaults.id : Int32' must be provided.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            module SubConfig\n              include ADI::Extension::Schema\n\n              module Defaults\n                include ADI::Extension::Schema\n\n                property name : String\n                property id : Int32\n              end\n            end\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              sub_config: {\n                defaults: {\n                  name: \"Fred\"\n                }\n              }\n            }\n          })\n        CR\n      end\n\n      it \"errors if there is a collection type mismatch\" do\n        assert_compile_time_error \"Expected configuration value 'test.sub_config.defaults.foo' to be a 'Array(Int32)', but got 'Array(String)'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            module SubConfig\n              include ADI::Extension::Schema\n\n              module Defaults\n                include ADI::Extension::Schema\n\n                property foo : Array(Int32)\n              end\n            end\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              sub_config: {\n                defaults: {\n                  foo: [] of String\n                }\n              }\n            }\n          })\n        CR\n      end\n\n      it \"errors if there is a type mismatch within an array\" do\n        assert_compile_time_error \"Expected configuration value 'test.sub_config.defaults.foo[1]' to be a 'Int32', but got 'UInt64'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            module SubConfig\n              include ADI::Extension::Schema\n\n              module Defaults\n                include ADI::Extension::Schema\n\n                property foo : Array(Int32)\n              end\n            end\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              sub_config: {\n                defaults: {\n                  foo: [1, 10_u64] of Int32\n                }\n              }\n            }\n          })\n        CR\n      end\n\n      it \"errors if there is a type mismatch within an array without type hint\" do\n        assert_compile_time_error \"Expected configuration value 'test.sub_config.defaults.foo[1]' to be a 'Int32', but got 'UInt64'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            module SubConfig\n              include ADI::Extension::Schema\n\n              module Defaults\n                include ADI::Extension::Schema\n\n                property foo : Array(Int32)\n              end\n            end\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              sub_config: {\n                defaults: {\n                  foo: [1, 10_u64]\n                }\n              }\n            }\n          })\n        CR\n      end\n\n      it \"errors if there is a type mismatch within an array using NoReturn schema default\" do\n        assert_compile_time_error \"Expected configuration value 'test.sub_config.defaults.foo[1]' to be a 'Int32', but got 'UInt64'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            module SubConfig\n              include ADI::Extension::Schema\n\n              module Defaults\n                include ADI::Extension::Schema\n\n                property foo : Array(Int32)\n              end\n            end\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              sub_config: {\n                defaults: {\n                  foo: [1, 10_u64]\n                }\n              }\n            }\n          })\n        CR\n      end\n\n      it \"errors if a configuration value not found in the schema is encountered\" do\n        assert_compile_time_error \"Unexpected property 'test.sub_config.defaults.name'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            module SubConfig\n              include ADI::Extension::Schema\n\n              module Defaults\n                include ADI::Extension::Schema\n\n                property id : Int32\n              end\n            end\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              sub_config: {\n                defaults: {\n                  id: 10,\n                  name: \"Fred\"\n                }\n              }\n            }\n          })\n        CR\n      end\n    end\n\n    it \"errors if a configuration value has the incorrect type\" do\n      assert_compile_time_error \"Extension 'foo' is configured, but no extension with that name has been registered.\", <<-'CR'\n        ADI.configure({\n          foo: {\n            id: 1\n          }\n        })\n      CR\n    end\n\n    it \"errors if nothing is configured, but a property is required\" do\n      assert_compile_time_error \"Required configuration property 'test.id : Int32' must be provided.\", <<-'CR'\n        require \"../spec_helper\"\n\n        module Schema\n          include ADI::Extension::Schema\n\n          property id : Int32\n        end\n\n        ADI.register_extension \"test\", Schema\n      CR\n    end\n  end\n\n  it \"extension configuration value resolution\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      enum Color\n        Red\n        Green\n        Blue\n      end\n\n      module Schema\n        include ADI::Extension::Schema\n\n        ID = 10.0\n\n        property id : Int32\n        property float : Float64 = Schema::ID\n        property name : String = \"fred\"\n        property nilable : String?\n        property color_type : Color\n        property color_sym : Color\n        property color_default : Color = :green\n        property color_global : ::Color = :red\n        property value : Hash(String, String)\n        property regex : Regex\n      end\n\n      ADI.register_extension \"blah\", Schema\n\n      ADI.configure({\n        blah: {\n          id:    123,\n          color_type: Color::Red,\n          color_sym: :blue,\n          value: {\"id\" => \"10\", \"name\" => \"fred\"},\n          regex: /foo/\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"blah\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"id\"] == 123 }}, \"Expected id to be 123\")\n          ASPEC.compile_time_assert(\\{{ config[\"name\"] == \"fred\" }}, \"Expected name to be fred\")\n          ASPEC.compile_time_assert(\\{{ config[\"float\"] == 10.0 }}, \"Expected float to be 10.0\")\n          ASPEC.compile_time_assert(\\{{ config[\"nilable\"].nil? }}, \"Expected nilable to be nil\")\n          ASPEC.compile_time_assert(\\{{ config[\"color_type\"].stringify == \"Color.new(0)\" }}, \"Expected color_type to be Color.new(0)\")\n          ASPEC.compile_time_assert(\\{{ config[\"color_sym\"].stringify == \"Color.new(:blue)\" }}, \"Expected color_sym to be Color.new(:blue)\")\n          ASPEC.compile_time_assert(\\{{ config[\"color_default\"].stringify == \"Color.new(:green)\" }}, \"Expected color_default to be Color.new(:green)\")\n          ASPEC.compile_time_assert(\\{{ config[\"color_global\"].stringify == \"::Color.new(:red)\" }}, \"Expected color_global to be ::Color.new(:red)\")\n          ASPEC.compile_time_assert(\\{{ config[\"value\"] == {\"id\" => \"10\", \"name\" => \"fred\"} }}, \"Expected value to be the expected hash\")\n          ASPEC.compile_time_assert(\\{{ config[\"regex\"] == /foo/ }}, \"Expected regex to be /foo/\")\n        end\n      end\n    CR\n  end\n\n  it \"does not error if nothing is configured, but all properties have defaults or are nilable\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        property id : Int32 = 123\n      end\n\n      ADI.register_extension \"blah\", Schema\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"blah\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"id\"] == 123 }}, \"Expected id to be 123\")\n        end\n      end\n    CR\n  end\n\n  it \"inherits type of arrays from property if not explicitly set\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        property foo : Array(Int32)\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          foo: [1, 2]\n        }\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"test\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"foo\"] == [1, 2] }}, \"Expected foo to be [1, 2]\")\n        end\n      end\n    CR\n  end\n\n  it \"allows using NoReturn to type empty arrays in schema\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        property foo : Array(Int32 | String) = [] of NoReturn\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"test\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"foo\"].stringify == \"Array(Int32 | String).new\" }}, \"Expected foo to stringify as empty array\")\n        end\n      end\n    CR\n  end\n\n  it \"allows customizing values when using NoReturn to type empty arrays defaults in schema\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        property foo : Array(Int32 | String) = [] of NoReturn\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          foo: [1, 2]\n        }\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"test\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"foo\"] == [1, 2] }}, \"Expected foo to be [1, 2]\")\n        end\n      end\n    CR\n  end\n\n  it \"expands schema to include expected structure/defaults if not configuration is provided\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        module One\n          include ADI::Extension::Schema\n\n          property enabled : Bool = false\n        end\n\n        module Two\n          include ADI::Extension::Schema\n\n          property enabled : Bool = false\n        end\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"test\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"one\"][\"enabled\"] == false }}, \"Expected one.enabled to be false\")\n          ASPEC.compile_time_assert(\\{{ config[\"two\"][\"enabled\"] == false }}, \"Expected two.enabled to be false\")\n        end\n      end\n    CR\n  end\n\n  it \"expands schema to include expected structure/defaults if not explicitly provided\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        module One\n          include ADI::Extension::Schema\n\n          property enabled : Bool = false\n          property id : Int32\n        end\n\n        module Two\n          include ADI::Extension::Schema\n\n          property enabled : Bool = false\n\n          module Three\n            include ADI::Extension::Schema\n\n            property enabled : Bool = false\n          end\n        end\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          one: {\n            id: 10,\n          },\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"test\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"one\"][\"enabled\"] == false }}, \"Expected one.enabled to be false\")\n          ASPEC.compile_time_assert(\\{{ config[\"one\"][\"id\"] == 10 }}, \"Expected one.id to be 10\")\n          ASPEC.compile_time_assert(\\{{ config[\"two\"][\"enabled\"] == false }}, \"Expected two.enabled to be false\")\n          ASPEC.compile_time_assert(\\{{ config[\"two\"][\"three\"][\"enabled\"] == false }}, \"Expected two.three.enabled to be false\")\n        end\n      end\n    CR\n  end\n\n  it \"merges missing array_of defaults\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n        array_of rules, id : Int32, stop : Bool = false\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          rules: [\n            {id: 10},\n          ],\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"test\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"rules\"][0][\"id\"] == 10 }}, \"Expected rules[0].id to be 10\")\n          ASPEC.compile_time_assert(\\{{ config[\"rules\"][0][\"stop\"] == false }}, \"Expected rules[0].stop to be false\")\n        end\n      end\n    CR\n  end\n\n  it \"merges missing array_of defaults in time for other compiler passes\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n        array_of rules, id : Int32, stop : Bool = false\n      end\n\n      module MyExtension\n        macro included\n            macro finished\n              {% verbatim do %}\n                {%\n                  ADI::CONFIG[\"parameters\"][\"stop\"] = ADI::CONFIG[\"test\"][\"rules\"][0][\"stop\"]\n                %}\n              {% end %}\n            end\n          end\n      end\n\n      ADI.register_extension \"test\", Schema\n      ADI.add_compiler_pass MyExtension, :before_optimization, 500 # Ensure the Config passes run first\n\n      ADI.configure({\n        test: {\n          rules: [\n            {id: 10},\n          ],\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            parameters = ADI::CONFIG[\"parameters\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ parameters[\"stop\"] == false }}, \"Expected parameters stop to be false\")\n        end\n      end\n    CR\n  end\n\n  it \"array_of with nested object_schema fills in nested defaults\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        object_schema JwtConfig,\n          secret : String,\n          algorithm : String = \"hmac.sha256\"\n\n        array_of items,\n          name : String,\n          jwt : JwtConfig\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          items: [\n            {name: \"item1\", jwt: {secret: \"secret1\"}},\n            {name: \"item2\", jwt: {secret: \"secret2\"}},\n          ],\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"test\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"items\"][0][\"name\"] == \"item1\" }}, \"Expected items[0].name to be item1\")\n          ASPEC.compile_time_assert(\\{{ config[\"items\"][0][\"jwt\"][\"secret\"] == \"secret1\" }}, \"Expected items[0].jwt.secret to be secret1\")\n          ASPEC.compile_time_assert(\\{{ config[\"items\"][0][\"jwt\"][\"algorithm\"] == \"hmac.sha256\" }}, \"Expected items[0].jwt.algorithm to be hmac.sha256\")\n          ASPEC.compile_time_assert(\\{{ config[\"items\"][1][\"name\"] == \"item2\" }}, \"Expected items[1].name to be item2\")\n          ASPEC.compile_time_assert(\\{{ config[\"items\"][1][\"jwt\"][\"secret\"] == \"secret2\" }}, \"Expected items[1].jwt.secret to be secret2\")\n          ASPEC.compile_time_assert(\\{{ config[\"items\"][1][\"jwt\"][\"algorithm\"] == \"hmac.sha256\" }}, \"Expected items[1].jwt.algorithm to be hmac.sha256\")\n        end\n      end\n    CR\n  end\n\n  it \"object_of with nested object_schema fills in nested defaults\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        object_schema JwtConfig,\n          secret : String,\n          algorithm : String = \"hmac.sha256\"\n\n        object_of connection,\n          url : String,\n          jwt : JwtConfig\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          connection: {\n            url: \"localhost\",\n            jwt: {secret: \"my-secret\"},\n          },\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"test\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"url\"] == \"localhost\" }}, \"Expected connection.url to be localhost\")\n          ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"jwt\"][\"secret\"] == \"my-secret\" }}, \"Expected connection.jwt.secret to be my-secret\")\n          ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"jwt\"][\"algorithm\"] == \"hmac.sha256\" }}, \"Expected connection.jwt.algorithm to be hmac.sha256\")\n        end\n      end\n    CR\n  end\n\n  it \"fills in missing nilable keys with `nil`\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        object_of config, id : Int32, name : String?\n      end\n\n      ADI.register_extension \"blah\", Schema\n\n      ADI.configure({\n        blah: {\n          config: {id: 10},\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"blah\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"config\"].keys.stringify == %([__nil, id, name]) }}, \"Expected config keys to be [__nil, id, name]\")\n          ASPEC.compile_time_assert(\\{{ config[\"config\"][\"id\"] == 10 }}, \"Expected config.id to be 10\")\n          ASPEC.compile_time_assert(\\{{ config[\"config\"][\"name\"].nil? }}, \"Expected config.name to be nil\")\n        end\n      end\n    CR\n  end\n\n  it \"fills in missing nilable keys with `nil` when missing from default value\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        object_of config = {id: 123}, id : Int32, name : String?\n      end\n\n      ADI.register_extension \"blah\", Schema\n\n      macro finished\n        macro finished\n          \\{%\n            config = ADI::CONFIG[\"blah\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ config[\"config\"].keys.stringify == %([id, name]) }}, \"Expected config keys to be [id, name]\")\n          ASPEC.compile_time_assert(\\{{ config[\"config\"][\"id\"] == 123 }}, \"Expected config.id to be 123\")\n          ASPEC.compile_time_assert(\\{{ config[\"config\"][\"name\"].nil? }}, \"Expected config.name to be nil\")\n        end\n      end\n    CR\n  end\n\n  describe \"map_of\" do\n    it \"merges missing map_of defaults for each value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        module Schema\n          include ADI::Extension::Schema\n          map_of hubs, url : String, port : Int32 = 5432\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {url: \"localhost\"},\n              secondary: {url: \"remote\", port: 5433},\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"url\"] == \"localhost\" }}, \"Expected hubs.primary.url to be localhost\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"port\"] == 5432 }}, \"Expected hubs.primary.port to be 5432\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"secondary\"][\"url\"] == \"remote\" }}, \"Expected hubs.secondary.url to be remote\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"secondary\"][\"port\"] == 5433 }}, \"Expected hubs.secondary.port to be 5433\")\n          end\n        end\n      CR\n    end\n\n    it \"defaults to empty hash when not provided\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        module Schema\n          include ADI::Extension::Schema\n          map_of hubs, url : String\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {} of Nil => Nil,\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n\n              # Check that hubs key exists and is empty (when converted to string, looks like \"{}\" or \"{hubs => {}}\")\n              found_hubs = false\n              hubs_empty = false\n              config.each do |k, v|\n                if k.stringify == \"hubs\"\n                  found_hubs = true\n                  hubs_empty = v.keys.reject { |vk| vk.stringify == \"__nil\" }.empty?\n                end\n              end\n            %}\n            ASPEC.compile_time_assert(\\{{ found_hubs }}, \"Expected hubs key to exist in config\")\n            ASPEC.compile_time_assert(\\{{ hubs_empty }}, \"Expected empty hash for hubs\")\n          end\n        end\n      CR\n    end\n\n    it \"map_of? defaults to nil when not provided\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        module Schema\n          include ADI::Extension::Schema\n          map_of? hubs, url : String\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {} of Nil => Nil,\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"].nil? }}, \"Expected hubs to be nil\")\n          end\n        end\n      CR\n    end\n\n    it \"map_of with nested object_schema fills in nested defaults\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : String = \"hmac.sha256\"\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"my-secret\"},\n              },\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"url\"] == \"localhost\" }}, \"Expected hubs.primary.url to be localhost\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"secret\"] == \"my-secret\" }}, \"Expected hubs.primary.jwt.secret to be my-secret\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"algorithm\"] == \"hmac.sha256\" }}, \"Expected hubs.primary.jwt.algorithm to be hmac.sha256\")\n          end\n        end\n      CR\n    end\n\n    it \"errors if a required hash value property is missing\" do\n      assert_compile_time_error \"Configuration value 'test.hubs.primary' is missing required value for 'url' of type 'String'.\", <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          map_of hubs, url : String, port : Int32 = 5432\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {port: 5432},\n            },\n          },\n        })\n      CR\n    end\n\n    it \"errors if a hash value has an unexpected key\" do\n      assert_compile_time_error \"Expected configuration value 'test.hubs.primary' to be a '{url: url : String, port: port : Int32 = 5432}', but encountered unexpected key 'invalid'.\", <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          map_of hubs, url : String, port : Int32 = 5432\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {url: \"localhost\", invalid: \"foo\"},\n            },\n          },\n        })\n      CR\n    end\n\n    it \"errors if a nested map value has an unexpected type\" do\n      assert_compile_time_error \"Expected configuration value 'test.hubs.primary.jwt.secret' to be a 'String', but got 'Int32'.\", <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: 123},\n              },\n            },\n          },\n        })\n      CR\n    end\n\n    it \"errors if a hash value property has wrong type\" do\n      assert_compile_time_error \"Expected configuration value 'test.hubs.primary.port' to be a 'Int32', but got 'String'.\", <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          map_of hubs, url : String, port : Int32\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {url: \"localhost\", port: \"not-a-number\"},\n            },\n          },\n        })\n      CR\n    end\n\n    it \"errors if map_of direct member has wrong type when object_schema also present\" do\n      assert_compile_time_error \"Expected configuration value 'test.hubs.primary.url' to be a 'String', but got 'Int32'.\", <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: 123,\n                jwt: {secret: \"valid\"},\n              },\n            },\n          },\n        })\n      CR\n    end\n\n    it \"fills in nested object_schema defaults for multiple map entries independently\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : String = \"hmac.sha256\"\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"secret1\"},\n              },\n              secondary: {\n                url: \"remote\",\n                jwt: {secret: \"secret2\"},\n              },\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            # Verify both entries get their own independent defaults\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"algorithm\"] == \"hmac.sha256\" }}, \"Expected hubs.primary.jwt.algorithm to be hmac.sha256\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"secondary\"][\"jwt\"][\"algorithm\"] == \"hmac.sha256\" }}, \"Expected hubs.secondary.jwt.algorithm to be hmac.sha256\")\n            # And their unique values are preserved\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"secret\"] == \"secret1\" }}, \"Expected hubs.primary.jwt.secret to be secret1\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"secondary\"][\"jwt\"][\"secret\"] == \"secret2\" }}, \"Expected hubs.secondary.jwt.secret to be secret2\")\n          end\n        end\n      CR\n    end\n\n    it \"errors if nested object_schema field is missing required value\" do\n      assert_compile_time_error \"Configuration value 'test.hubs.primary.jwt' is missing required value for 'secret' of type 'String'.\", <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : String = \"hmac.sha256\"\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {algorithm: \"rsa\"},\n              },\n            },\n          },\n        })\n      CR\n    end\n\n    it \"errors if nested object_schema has unexpected key\" do\n      assert_compile_time_error \"Expected configuration value 'test.hubs.primary.jwt' to be a '{secret: secret : String, algorithm: algorithm : String = \\\"hmac.sha256\\\"}', but encountered unexpected key 'invalid'.\", <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : String = \"hmac.sha256\"\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"my-secret\", invalid: \"foo\"},\n              },\n            },\n          },\n        })\n      CR\n    end\n\n    it \"uses custom default for map_of with assignment syntax\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        module Schema\n          include ADI::Extension::Schema\n          map_of hubs = {default: {url: \"localhost\", port: 8080}}, url : String, port : Int32 = 5432\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        # Don't configure hubs at all - it should use the custom default\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            # Custom default entry should be present with its values\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"default\"][\"url\"] == \"localhost\" }}, \"Expected hubs.default.url to be localhost\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"default\"][\"port\"] == 8080 }}, \"Expected hubs.default.port to be 8080\")\n          end\n        end\n      CR\n    end\n\n    it \"object_schema enum member with symbol default value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"my-secret\"},\n              },\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"url\"] == \"localhost\" }}, \"Expected hubs.primary.url to be localhost\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"secret\"] == \"my-secret\" }}, \"Expected hubs.primary.jwt.secret to be my-secret\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(:hs256)\" }}, \"Expected hubs.primary.jwt.algorithm to be Algorithm.new(:hs256)\")\n          end\n        end\n      CR\n    end\n\n    it \"object_schema enum member with user-provided symbol value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"my-secret\", algorithm: :hs512},\n              },\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"url\"] == \"localhost\" }}, \"Expected hubs.primary.url to be localhost\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"secret\"] == \"my-secret\" }}, \"Expected hubs.primary.jwt.secret to be my-secret\")\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(:hs512)\" }}, \"Expected hubs.primary.jwt.algorithm to be Algorithm.new(:hs512)\")\n          end\n        end\n      CR\n    end\n\n    it \"object_schema enum member with global type\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : ::Algorithm = :hs256\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"my-secret\"},\n              },\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"algorithm\"].stringify == \"::Algorithm.new(:hs256)\" }}, \"Expected hubs.primary.jwt.algorithm to be ::Algorithm.new(:hs256)\")\n          end\n        end\n      CR\n    end\n\n    it \"errors for invalid enum symbol in object_schema\" do\n      assert_compile_time_error \"Unknown 'Algorithm' enum member for default value of 'test.hubs.primary.jwt.algorithm'.\", <<-'CR'\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :invalid_algo\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"my-secret\"},\n              },\n            },\n          },\n        })\n      CR\n    end\n\n    it \"errors for invalid user-provided enum symbol in object_schema\" do\n      assert_compile_time_error \"Unknown 'Algorithm' enum member for 'test.hubs.primary.jwt.algorithm'.\", <<-'CR'\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"my-secret\", algorithm: :invalid_algo},\n              },\n            },\n          },\n        })\n      CR\n    end\n\n    it \"array_of object_schema enum member with symbol default value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          array_of items,\n            name : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            items: [\n              {name: \"item1\", jwt: {secret: \"my-secret\"}},\n            ],\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"items\"][0][\"name\"] == \"item1\" }}, \"Expected items[0].name to be item1\")\n            ASPEC.compile_time_assert(\\{{ config[\"items\"][0][\"jwt\"][\"secret\"] == \"my-secret\" }}, \"Expected items[0].jwt.secret to be my-secret\")\n            ASPEC.compile_time_assert(\\{{ config[\"items\"][0][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(:hs256)\" }}, \"Expected items[0].jwt.algorithm to be Algorithm.new(:hs256)\")\n          end\n        end\n      CR\n    end\n\n    it \"array_of object_schema enum member with user-provided symbol value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          array_of items,\n            name : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            items: [\n              {name: \"item1\", jwt: {secret: \"my-secret\", algorithm: :hs512}},\n            ],\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"items\"][0][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(:hs512)\" }}, \"Expected items[0].jwt.algorithm to be Algorithm.new(:hs512)\")\n          end\n        end\n      CR\n    end\n\n    it \"errors for invalid enum symbol in array_of object_schema default\" do\n      assert_compile_time_error \"Unknown 'Algorithm' enum member for default value of 'test.items.jwt.algorithm'.\", <<-'CR'\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :invalid_algo\n\n          array_of items,\n            name : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            items: [\n              {name: \"item1\", jwt: {secret: \"my-secret\"}},\n            ],\n          },\n        })\n      CR\n    end\n\n    it \"errors for invalid user-provided enum symbol in array_of object_schema\" do\n      assert_compile_time_error \"Unknown 'Algorithm' enum member for 'test.items.jwt.algorithm'.\", <<-'CR'\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          array_of items,\n            name : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            items: [\n              {name: \"item1\", jwt: {secret: \"my-secret\", algorithm: :invalid_algo}},\n            ],\n          },\n        })\n      CR\n    end\n\n    it \"object_of object_schema enum member with symbol default value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          object_of connection,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            connection: {\n              url: \"localhost\",\n              jwt: {secret: \"my-secret\"},\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"url\"] == \"localhost\" }}, \"Expected connection.url to be localhost\")\n            ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"jwt\"][\"secret\"] == \"my-secret\" }}, \"Expected connection.jwt.secret to be my-secret\")\n            ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(:hs256)\" }}, \"Expected connection.jwt.algorithm to be Algorithm.new(:hs256)\")\n          end\n        end\n      CR\n    end\n\n    it \"object_of object_schema enum member with user-provided symbol value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          object_of connection,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            connection: {\n              url: \"localhost\",\n              jwt: {secret: \"my-secret\", algorithm: :hs512},\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(:hs512)\" }}, \"Expected connection.jwt.algorithm to be Algorithm.new(:hs512)\")\n          end\n        end\n      CR\n    end\n\n    it \"errors for invalid enum symbol in object_of object_schema default\" do\n      assert_compile_time_error \"Unknown 'Algorithm' enum member for default value of 'test.connection.jwt.algorithm'.\", <<-'CR'\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :invalid_algo\n\n          object_of connection,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            connection: {\n              url: \"localhost\",\n              jwt: {secret: \"my-secret\"},\n            },\n          },\n        })\n      CR\n    end\n\n    it \"errors for invalid user-provided enum symbol in object_of object_schema\" do\n      assert_compile_time_error \"Unknown 'Algorithm' enum member for 'test.connection.jwt.algorithm'.\", <<-'CR'\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          object_of connection,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            connection: {\n              url: \"localhost\",\n              jwt: {secret: \"my-secret\", algorithm: :invalid_algo},\n            },\n          },\n        })\n      CR\n    end\n\n    it \"map_of object_schema enum member with number default value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = 0\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"my-secret\"},\n              },\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(0)\" }}, \"Expected hubs.primary.jwt.algorithm to be Algorithm.new(0)\")\n          end\n        end\n      CR\n    end\n\n    it \"map_of object_schema enum member with user-provided number value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            hubs: {\n              primary: {\n                url: \"localhost\",\n                jwt: {secret: \"my-secret\", algorithm: 2},\n              },\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"hubs\"][\"primary\"][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(2)\" }}, \"Expected hubs.primary.jwt.algorithm to be Algorithm.new(2)\")\n          end\n        end\n      CR\n    end\n\n    it \"array_of object_schema enum member with number default value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = 0\n\n          array_of items,\n            name : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            items: [\n              {name: \"item1\", jwt: {secret: \"my-secret\"}},\n            ],\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"items\"][0][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(0)\" }}, \"Expected items[0].jwt.algorithm to be Algorithm.new(0)\")\n          end\n        end\n      CR\n    end\n\n    it \"array_of object_schema enum member with user-provided number value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          array_of items,\n            name : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            items: [\n              {name: \"item1\", jwt: {secret: \"my-secret\", algorithm: 2}},\n            ],\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"items\"][0][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(2)\" }}, \"Expected items[0].jwt.algorithm to be Algorithm.new(2)\")\n          end\n        end\n      CR\n    end\n\n    it \"object_of object_schema enum member with number default value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = 0\n\n          object_of connection,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            connection: {\n              url: \"localhost\",\n              jwt: {secret: \"my-secret\"},\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(0)\" }}, \"Expected connection.jwt.algorithm to be Algorithm.new(0)\")\n          end\n        end\n      CR\n    end\n\n    it \"object_of object_schema enum member with user-provided number value\" do\n      ASPEC::Methods.assert_compiles <<-'CR'\n        require \"../spec_helper\"\n\n        enum Algorithm\n          Hs256\n          Hs384\n          Hs512\n        end\n\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : Algorithm = :hs256\n\n          object_of connection,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            connection: {\n              url: \"localhost\",\n              jwt: {secret: \"my-secret\", algorithm: 2},\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n              config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"jwt\"][\"algorithm\"].stringify == \"Algorithm.new(2)\" }}, \"Expected connection.jwt.algorithm to be Algorithm.new(2)\")\n          end\n        end\n      CR\n    end\n  end\n\n  it \"handles multiple extensions where one has nested schemas\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      # First extension with nested schema modules\n      module FirstSchema\n        include ADI::Extension::Schema\n\n        property root_prop : String = \"root\"\n\n        module Nested\n          include ADI::Extension::Schema\n\n          property nested_prop : Int32 = 42\n        end\n      end\n\n      # Second extension without nested schemas\n      module SecondSchema\n        include ADI::Extension::Schema\n\n        property other_prop : Bool = true\n      end\n\n      ADI.register_extension \"first\", FirstSchema\n      ADI.register_extension \"second\", SecondSchema\n\n      macro finished\n        macro finished\n          \\{%\n            first_config = ADI::CONFIG[\"first\"]\n            second_config = ADI::CONFIG[\"second\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ first_config[\"root_prop\"] == \"root\" }}, \"Expected first.root_prop to be root\")\n          ASPEC.compile_time_assert(\\{{ first_config[\"nested\"][\"nested_prop\"] == 42 }}, \"Expected first.nested.nested_prop to be 42\")\n          ASPEC.compile_time_assert(\\{{ second_config[\"other_prop\"] == true }}, \"Expected second.other_prop to be true\")\n          ASPEC.compile_time_assert(\\{{ second_config[\"nested\"] == nil }}, \"Expected second to not have nested key\")\n        end\n      end\n    CR\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/namespaced_spec.cr",
    "content": "require \"../spec_helper\"\n\n@[ADI::Register]\nclass MyApp::Models::Foo\nend\n\n@[ADI::Register(public: true)]\nclass NamespaceClient\n  getter service\n\n  def initialize(@service : MyApp::Models::Foo); end\nend\n\ndescribe ADI::ServiceContainer do\n  it \"correctly resolves the service\" do\n    ADI.container.namespace_client.service.should be_a MyApp::Models::Foo\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/normalize_definitions_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: <<-'CRYSTAL', postamble: <<-'CR'\n    require \"../spec_helper.cr\"\n    module MySchema\n      include ADI::Extension::Schema\n\n      property id : Int32 = 10\n    end\n\n    class SomeService; end\n\n    module MyExtension\n      macro included\n          macro finished\n            {% verbatim do %}\n              {%\n  CRYSTAL\n              %}\n            {% end %}\n          end\n        end\n    end\n\n    ADI.register_extension \"test\", MySchema\n    ADI.add_compiler_pass MyExtension, :before_optimization, 1028\n    ADI::ServiceContainer.new\n  CR\nend\n\ndescribe ADI::ServiceContainer::NormalizeDefinitions, tags: \"compiled\" do\n  describe \"compiler errors\" do\n    it \"`class` is not provided\" do\n      assert_compile_time_error \"Service 'some_service' is missing required 'class' property.\", <<-'CR'\n        SERVICE_HASH[\"some_service\"] = {\n          public: false,\n        }\n      CR\n    end\n  end\n\n  it \"applies defaults to missing properties\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper.cr\"\n\n      module MySchema\n        include ADI::Extension::Schema\n\n        property id : Int32 = 10\n      end\n\n      class SomeService; end\n\n      module MyExtension\n        macro included\n            macro finished\n              {% verbatim do %}\n                {%\n                  SERVICE_HASH[\"some_service\"] = {\n                    class: SomeService,\n                    public: true,\n                  }\n                %}\n              {% end %}\n            end\n          end\n      end\n\n      ADI.register_extension \"test\", MySchema\n      ADI.add_compiler_pass MyExtension, :before_optimization, 1028\n      ADI::ServiceContainer.new\n\n      macro finished\n        macro finished\n          \\{%\n            some_service = ADI::ServiceContainer::SERVICE_HASH[\"some_service\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ some_service[\"class\"] == SomeService }}, \"Expected class to be SomeService\")\n          ASPEC.compile_time_assert(\\{{ some_service[\"public\"] == true }}, \"Expected public to be true\")\n          ASPEC.compile_time_assert(\\{{ some_service[\"calls\"].size == 0 }}, \"Expected calls to be empty\")\n          ASPEC.compile_time_assert(\\{{ some_service[\"tags\"].size == 0 }}, \"Expected tags to be empty\")\n          ASPEC.compile_time_assert(\\{{ some_service[\"generics\"].size == 0 }}, \"Expected generics to be empty\")\n          ASPEC.compile_time_assert(\\{{ some_service[\"parameters\"].size == 0 }}, \"Expected parameters to be empty\")\n          ASPEC.compile_time_assert(\\{{ some_service[\"shared\"] == true }}, \"Expected shared to be true\")\n          ASPEC.compile_time_assert(\\{{ some_service[\"referenced_services\"].size == 0 }}, \"Expected referenced_services to be empty\")\n        end\n      end\n    CR\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/optional_services_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct OptionalMissingService\nend\n\n@[ADI::Register]\nstruct OptionalExistingService\nend\n\n@[ADI::Register(public: true)]\nclass OptionalClient\n  getter service_missing, service_existing, service_default\n\n  def initialize(\n    @service_missing : OptionalMissingService?,\n    @service_existing : OptionalExistingService?,\n    @service_default : OptionalMissingService | Int32 | Nil = 12,\n  ); end\nend\n\ndescribe ADI::ServiceContainer do\n  describe \"where a dependency is optional\" do\n    describe \"and does not exist\" do\n      describe \"without a default value\" do\n        it \"should inject `nil`\" do\n          ADI.container.optional_client.service_missing.should be_nil\n        end\n      end\n\n      describe \"with a default value\" do\n        it \"should inject the default\" do\n          ADI.container.optional_client.service_default.should eq 12\n        end\n      end\n    end\n\n    describe \"and does exist\" do\n      it \"should inject that service\" do\n        ADI.container.optional_client.service_existing.should be_a OptionalExistingService\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/parameters_spec.cr",
    "content": "require \"../spec_helper\"\n\n@[ADI::Register(\n  public: true,\n  _reference: \"%app.domain%\",\n  _with_percent: \"%app.with_percent%\",\n  _with_percent_and_placeholder: \"%app.with_percent_placeholder%\",\n  _with_single_placeholder: \"%app.placeholder%\",\n  _with_multiple_placeholders: \"%app.placeholders%\",\n  _hash: \"%app.mapping%\",\n  _array: \"%app.array%\",\n  _nested_array: \"%app.nested_array%\",\n  _nested_hash: \"%app.nested_mapping%\",\n  _non_string: \"%app.enable_v2_protocol%\",\n  _nested_deferred_string: \"%app.full_url%\"\n)]\nclass ParametersClient\n  def initialize(\n    reference : String,\n    with_percent : String,\n    with_percent_and_placeholder : String,\n    with_single_placeholder : String,\n    with_multiple_placeholders : String,\n    hash : Hash(Int32, String),\n    array : Array(String),\n    nested_array : Array(Array(String) | String),\n    nested_hash : Hash(String, Bool | String | Array(String) | Array(Array(String) | String)),\n    non_string : Bool,\n    nested_deferred_string : String,\n  )\n    reference.should eq \"google.com\"\n    with_percent.should eq \"foo%bar\"\n    with_percent_and_placeholder.should eq \"https://google.com/path/t%o/thing\"\n    with_single_placeholder.should eq \"https://google.com/path/to/thing\"\n    with_multiple_placeholders.should eq \"https://google.com/path/to/false\"\n    hash.should eq({10 => \"google.com\", 20 => \"https://google.com/path/to/thing\"})\n    array.should eq [\"google.com\", \"https://google.com/path/to/thing\", \"foo%bar\", \"https://google.com/path/t%o/thing\"]\n    nested_array.should eq [[\"google.com\", \"https://google.com/path/to/thing\", \"foo%bar\", \"https://google.com/path/t%o/thing\"], \"google.com\", \"foo%bar\"]\n    nested_hash.should eq({\"string\" => \"google.com\", \"array\" => [\"google.com\", \"https://google.com/path/to/thing\", \"foo%bar\", \"https://google.com/path/t%o/thing\"], \"nested_array\" => [[\"google.com\", \"https://google.com/path/to/thing\", \"foo%bar\", \"https://google.com/path/t%o/thing\"], \"google.com\", \"foo%bar\"], \"bool\" => false, \"escaped\" => \"foo%bar\"})\n    non_string.should be_false\n    nested_deferred_string.should eq \"Visit: https://google.com/path/to/thing!\"\n  end\nend\n\ndescribe ADI::ServiceContainer do\n  it \"resolves parameters\" do\n    ADI.container.parameters_client\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/process_aliases_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compiles code, line: line, preamble: %(require \"../spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\ndescribe ADI::ServiceContainer::ProcessAliases, tags: \"compiled\" do\n  it \"errors if unable to determine the alias name\" do\n    assert_compile_time_error \"Alias cannot be automatically determined for 'foo' (Foo). If the type includes multiple interfaces, provide the interface to alias as the first positional argument to `@[ADI::AsAlias]`.\", <<-'CR'\n      module SomeInterface; end\n      module OtherInterface; end\n\n      @[ADI::Register]\n      @[ADI::AsAlias]\n      class Foo\n        include SomeInterface\n        include OtherInterface\n      end\n    CR\n  end\n\n  it \"allows explicit string alias name\" do\n    assert_compiles <<-'CR'\n      @[ADI::Register]\n      @[ADI::AsAlias(\"bar\")]\n      class Foo; end\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES.keys == [\"bar\"] }}, \"Expected alias keys to be [bar]\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[\"bar\"].size == 1 }}, \"Expected bar alias size to be 1\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[\"bar\"][0][\"id\"] == \"foo\" }}, \"Expected bar alias id to be foo\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[\"bar\"][0][\"public\"] == false }}, \"Expected bar alias public to be false\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[\"bar\"][0][\"name\"].nil? }}, \"Expected bar alias name to be nil\")\n        end\n      end\n    CR\n  end\n\n  it \"allows explicit const alias name\" do\n    assert_compiles <<-'CR'\n      BAR = \"bar\"\n\n      @[ADI::Register]\n      @[ADI::AsAlias(BAR)]\n      class Foo; end\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES.keys == [\"bar\"] }}, \"Expected alias keys to be [bar]\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[\"bar\"].size == 1 }}, \"Expected bar alias size to be 1\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[\"bar\"][0][\"id\"] == \"foo\" }}, \"Expected bar alias id to be foo\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[\"bar\"][0][\"public\"] == false }}, \"Expected bar alias public to be false\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[\"bar\"][0][\"name\"].nil? }}, \"Expected bar alias name to be nil\")\n        end\n      end\n    CR\n  end\n\n  it \"allows explicit TypeNode alias name\" do\n    assert_compiles <<-'CR'\n      module SomeInterface; end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface, public: true)]\n      class Foo\n        include SomeInterface\n      end\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES.keys == [SomeInterface] }}, \"Expected alias keys to be [SomeInterface]\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface].size == 1 }}, \"Expected SomeInterface alias size to be 1\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][0][\"id\"] == \"foo\" }}, \"Expected SomeInterface alias id to be foo\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][0][\"public\"] == true }}, \"Expected SomeInterface alias public to be true\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][0][\"name\"].nil? }}, \"Expected SomeInterface alias name to be nil\")\n        end\n      end\n    CR\n  end\n\n  it \"uses included interface type as alias name if there is only 1\" do\n    assert_compiles <<-'CR'\n      module SomeInterface; end\n\n      @[ADI::Register]\n      @[ADI::AsAlias]\n      class Foo\n        include SomeInterface\n      end\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES.keys == [SomeInterface] }}, \"Expected alias keys to be [SomeInterface]\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface].size == 1 }}, \"Expected SomeInterface alias size to be 1\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][0][\"id\"] == \"foo\" }}, \"Expected SomeInterface alias id to be foo\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][0][\"public\"] == false }}, \"Expected SomeInterface alias public to be false\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][0][\"name\"].nil? }}, \"Expected SomeInterface alias name to be nil\")\n        end\n      end\n    CR\n  end\n\n  it \"allows aliasing more than one interface\" do\n    assert_compiles <<-'CR'\n      module SomeInterface; end\n      module OtherInterface; end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface)]\n      @[ADI::AsAlias(OtherInterface)]\n      class Foo\n        include SomeInterface\n        include OtherInterface\n      end\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES.keys == [SomeInterface, OtherInterface] }}, \"Expected alias keys to be [SomeInterface, OtherInterface]\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface].size == 1 }}, \"Expected SomeInterface alias size to be 1\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[OtherInterface].size == 1 }}, \"Expected OtherInterface alias size to be 1\")\n        end\n      end\n    CR\n  end\n\n  it \"allows named alias with type\" do\n    assert_compiles <<-'CR'\n      module SomeInterface; end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface, name: \"my_param\")]\n      class Foo\n        include SomeInterface\n      end\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES.keys == [SomeInterface] }}, \"Expected alias keys to be [SomeInterface]\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface].size == 1 }}, \"Expected SomeInterface alias size to be 1\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][0][\"id\"] == \"foo\" }}, \"Expected SomeInterface alias id to be foo\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][0][\"name\"].id == \"my_param\" }}, \"Expected SomeInterface alias name to be my_param\")\n        end\n      end\n    CR\n  end\n\n  it \"allows multiple named aliases for same type\" do\n    assert_compiles <<-'CR'\n      module SomeInterface; end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface, name: \"first\")]\n      class First\n        include SomeInterface\n      end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface, name: \"second\")]\n      class Second\n        include SomeInterface\n      end\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface].size == 2 }}, \"Expected SomeInterface alias size to be 2\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][0][\"name\"].id == \"first\" }}, \"Expected SomeInterface alias[0] name to be first\")\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface][1][\"name\"].id == \"second\" }}, \"Expected SomeInterface alias[1] name to be second\")\n        end\n      end\n    CR\n  end\n\n  it \"allows both named and type-only aliases for same type\" do\n    assert_compiles <<-'CR'\n      module SomeInterface; end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface, name: \"specific\")]\n      class Specific\n        include SomeInterface\n      end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface)]\n      class Default\n        include SomeInterface\n      end\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::ALIASES[SomeInterface].size == 2 }}, \"Expected SomeInterface alias size to be 2\")\n          \\{%\n            named = ADI::ServiceContainer::ALIASES[SomeInterface].find { |a| !a[\"name\"].nil? }\n            type_only = ADI::ServiceContainer::ALIASES[SomeInterface].find { |a| a[\"name\"].nil? }\n          %}\n          ASPEC.compile_time_assert(\\{{ named[\"id\"] == \"specific\" }}, \"Expected named alias id to be specific\")\n          ASPEC.compile_time_assert(\\{{ type_only[\"id\"] == \"default\" }}, \"Expected type-only alias id to be default\")\n        end\n      end\n    CR\n  end\n\n  it \"errors on duplicate type+name combination\" do\n    assert_compile_time_error \"Duplicate alias for type 'SomeInterface' with name 'my_param'\", <<-'CR'\n      module SomeInterface; end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface, name: \"my_param\")]\n      class Foo\n        include SomeInterface\n      end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface, name: \"my_param\")]\n      class Bar\n        include SomeInterface\n      end\n    CR\n  end\n\n  it \"errors on duplicate type-only alias\" do\n    assert_compile_time_error \"Duplicate alias for type 'SomeInterface'. A type-only alias\", <<-'CR'\n      module SomeInterface; end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface)]\n      class Foo\n        include SomeInterface\n      end\n\n      @[ADI::Register]\n      @[ADI::AsAlias(SomeInterface)]\n      class Bar\n        include SomeInterface\n      end\n    CR\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/process_auto_configurations_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\nprivate PARTNER_TAG = \"partner\"\n\nenum Status\n  Active\n  Inactive\nend\n\n@[ADI::Register(_id: 1, name: \"google\", tags: [{name: PARTNER_TAG, priority: 5}])]\n@[ADI::Register(_id: 2, name: \"facebook\", tags: [PARTNER_TAG])]\n@[ADI::Register(_id: 3, name: \"yahoo\", tags: [{name: \"partner\", priority: 10}])]\n@[ADI::Register(_id: 4, name: \"microsoft\", tags: [PARTNER_TAG])]\nstruct FeedPartner\n  getter id\n\n  def initialize(@id : Int32); end\nend\n\n@[ADI::Register(_services: \"!partner\", public: true)]\nclass PartnerClient\n  getter services\n\n  def initialize(@services : Array(FeedPartner))\n  end\nend\n\n@[ADI::Register(_services: \"!partner\", public: true)]\nclass PartnerNamedDefaultClient\n  getter services\n  getter status\n\n  def initialize(\n    @services : Array(FeedPartner),\n    @status : Status = Status::Active,\n  )\n  end\nend\n\n@[ADI::Register(_services: \"!partner\", public: true)]\nrecord ProxyTagClient, services : Array(ADI::Proxy(FeedPartner))\n\nOTHER_TAG = \"foo.bar\"\n\n@[ADI::Autoconfigure(tags: [OTHER_TAG])]\nmodule Test; end\n\n@[ADI::Autoconfigure(public: true)]\nmodule PublicService; end\n\n@[ADI::Register]\n@[ADI::Autoconfigure(tags: [\"config\"])]\nclass MultipleTags\n  include Test\nend\n\n@[ADI::Register]\nclass SingleTag\n  include Test\nend\n\n@[ADI::Register(_services: \"!foo.bar\", public: true)]\nrecord OtherTagClient, services : Array(Test)\n\n@[ADI::Register(_services: \"!config\", public: true)]\nrecord ConfigTagClient, services : Array(MultipleTags)\n\n@[ADI::Register]\nrecord AutoConfiguredPublicService do\n  include PublicService\nend\n\nSTRING_UNUSED_TAG2 = \"string_unused_tag2\"\nSTRING_UNUSED_TAG3 = \"string_unused_tag3\"\nSTRING_UNUSED_TAG4 = \"string_unused_tag4\"\n\n@[ADI::Autoconfigure(tags: [\"unused_tag\"])]\nmodule UnusedInterface; end\n\n@[ADI::Autoconfigure(tags: [STRING_UNUSED_TAG2])]\nmodule UnusedInterface2; end\n\n@[ADI::Autoconfigure(tags: STRING_UNUSED_TAG3)]\nmodule UnusedInterface3; end\n\n@[ADI::Autoconfigure(tags: [{name: STRING_UNUSED_TAG4}])]\nmodule UnusedInterface4; end\n\n@[ADI::Autoconfigure(tags: [{name: \"string_unused_tag5\"}])]\nmodule UnusedInterface5; end\n\n@[ADI::Register(_services: \"!unused_tag\", public: true)]\nrecord UnusedTagClient, services : Array(UnusedInterface)\n\n@[ADI::Autoconfigure(calls: [{\"foo\", {6}}, {\"foo\", {3}}, {\"foo\"}])]\nmodule CallInterface; end\n\n@[ADI::Register(public: true)]\nclass CallAutoconfigureClient\n  include CallInterface\n\n  getter values = [] of Int32\n\n  def foo(value : Int32 = 1)\n    @values << value\n  end\nend\n\n@[ADI::AutoconfigureTag]\nmodule Namespace::FQNTagInterface; end\n\n@[ADI::AutoconfigureTag(\"foo\")]\nmodule Namespace::ExplicitTagInterface; end\n\nBAR_TAG = \"bar\"\n\n@[ADI::AutoconfigureTag(BAR_TAG)]\nmodule Namespace::ExplicitTagConstInterface; end\n\n@[ADI::AutoconfigureTag(\"prioritized_tag\", priority: 10)]\nmodule Namespace::PrioritizedTagInterface; end\n\n@[ADI::Register]\nrecord FQNService1 do\n  include Namespace::FQNTagInterface\n  include Namespace::ExplicitTagConstInterface\nend\n\n@[ADI::Register]\nrecord FQNService2 do\n  include Namespace::FQNTagInterface\n  include Namespace::ExplicitTagInterface\nend\n\n@[ADI::Register]\nrecord PrioritizedService1 do\n  include Namespace::PrioritizedTagInterface\nend\n\n@[ADI::Register(tags: [{name: \"prioritized_tag\", priority: 20}])]\nrecord PrioritizedService2 do\n  include Namespace::PrioritizedTagInterface\nend\n\n@[ADI::Register(public: true, _services: \"!prioritized_tag\")]\nclass PrioritizedTagClient\n  getter services : Array(Namespace::PrioritizedTagInterface)\n\n  def initialize(@services : Array(Namespace::PrioritizedTagInterface)); end\nend\n\n@[ADI::Register(public: true, _services: \"!Namespace::FQNTagInterface\")]\nclass FQNTagClient\n  getter services : Array(Namespace::FQNTagInterface)\n\n  def initialize(@services : Array(Namespace::FQNTagInterface)); end\nend\n\n@[ADI::Register(public: true, _services: \"!foo\")]\nclass FQNTagNamedClient\n  getter services : Array(Namespace::ExplicitTagInterface)\n\n  def initialize(@services : Array(Namespace::ExplicitTagInterface)); end\nend\n\n@[ADI::Register(public: true, _services: \"!bar\")]\nclass FQNTagConstClient\n  getter services : Array(Namespace::ExplicitTagConstInterface)\n\n  def initialize(@services : Array(Namespace::ExplicitTagConstInterface)); end\nend\n\n@[ADI::Register(public: true)]\nclass FQNTaggedIteratorClient\n  getter services\n\n  def initialize(@[ADI::TaggedIterator] @services : Enumerable(Namespace::FQNTagInterface)); end\nend\n\n@[ADI::Register(public: true)]\nclass FQNTaggedIteratorNamedClient\n  getter services\n\n  def initialize(@[ADI::TaggedIterator(\"Namespace::FQNTagInterface\")] @services : Enumerable(Namespace::FQNTagInterface)); end\nend\n\nNAMESPACE_TAG = \"Namespace::FQNTagInterface\"\n\n@[ADI::Register(public: true)]\nclass FQNTaggedIteratorNamedConstClient\n  getter services\n\n  def initialize(@[ADI::TaggedIterator(NAMESPACE_TAG)] @services : Enumerable(Namespace::FQNTagInterface)); end\nend\n\n@[ADI::Autoconfigure(tags: [\"non-service-abstract\"])]\nabstract struct NonServiceAbstract; end\n\n@[ADI::Register]\nrecord NonServiceChild1 < NonServiceAbstract\n\n@[ADI::Register]\nrecord NonServiceChild2 < NonServiceAbstract\n\n@[ADI::Register(public: true, _services: \"!non-service-abstract\")]\nclass NonServiceAbstractClient\n  getter services : Array(NonServiceAbstract)\n\n  def initialize(@services : Array(NonServiceAbstract)); end\nend\n\n@[ADI::Autoconfigure(constructor: \"create\", public: true)]\nabstract class AutoConfigureConstructor; end\n\n@[ADI::Register(public: true)]\nclass ConstructorOne < AutoConfigureConstructor\n  getter num\n\n  def self.create : self\n    new 123\n  end\n\n  private def initialize(@num : Int32); end\nend\n\n@[ADI::Register(_id: 999, public: true)]\nclass ConstructorTwo < AutoConfigureConstructor\n  getter num\n\n  def self.create(id : Int32) : self\n    new id\n  end\n\n  private def initialize(@num : Int32); end\nend\n\nmodule ManualTagInterface; end\n\nmodule ManualPublicInterface; end\n\nmodule ManualCallsInterface; end\n\nabstract class ManualConstructorBase; end\n\nmodule ManualAutoConfigSetup\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          AUTO_CONFIGURATIONS[ManualTagInterface] = {tags: [\"manual_tag\"]}\n          AUTO_CONFIGURATIONS[ManualPublicInterface] = {public: true}\n          AUTO_CONFIGURATIONS[ManualCallsInterface] = {calls: [{\"foo\", {6}}, {\"foo\"}]}\n          AUTO_CONFIGURATIONS[ManualConstructorBase] = {constructor: \"create\", public: true}\n        %}\n      {% end %}\n    end\n  end\nend\n\nADI.add_compiler_pass ManualAutoConfigSetup, priority: 200\n\n@[ADI::Register]\nrecord ManualTagService1 do\n  include ManualTagInterface\nend\n\n@[ADI::Register]\nrecord ManualTagService2 do\n  include ManualTagInterface\nend\n\n@[ADI::Register(public: true, _services: \"!manual_tag\")]\nclass ManualTagClient\n  getter services : Array(ManualTagInterface)\n\n  def initialize(@services : Array(ManualTagInterface)); end\nend\n\n@[ADI::Register]\nrecord ManualPublicService do\n  include ManualPublicInterface\nend\n\n@[ADI::Register(public: true)]\nclass ManualCallsClient\n  include ManualCallsInterface\n\n  getter values = [] of Int32\n\n  def foo(value : Int32 = 1)\n    @values << value\n  end\nend\n\n@[ADI::Register(public: true)]\nclass ManualConstructorChild < ManualConstructorBase\n  getter num\n\n  def self.create : self\n    new 42\n  end\n\n  private def initialize(@num : Int32); end\nend\n\ndescribe ADI::ServiceContainer::ProcessAutoconfigureAnnotations do\n  describe \"compiler errors\", tags: \"compiled\" do\n    describe \"tags\" do\n      it \"errors if the `tags` field is not of a valid type\" do\n        assert_compile_time_error \"'tags' field of service 'foo' must be an 'ArrayLiteral', got 'NumberLiteral'.\", <<-CR\n          @[ADI::Register(tags: 123)]\n          record Foo\n        CR\n      end\n\n      it \"errors if the `tags` field on the auto configuration is not of a valid type\" do\n        assert_compile_time_error \"'tags' field of auto configuration 'Test' must be an 'ArrayLiteral', got 'NumberLiteral'.\", <<-CR\n          @[ADI::Autoconfigure(tags: 123)]\n          module Test; end\n\n          @[ADI::Register]\n          record Foo do\n            include Test\n          end\n        CR\n      end\n\n      it \"errors if not all tags are of the proper type\" do\n        assert_compile_time_error \"Tag must be a 'StringLiteral' or 'NamedTupleLiteral', got 'NumberLiteral'.\", <<-CR\n          @[ADI::Autoconfigure(tags: [123])]\n          module Test; end\n\n          @[ADI::Register]\n          record Foo do\n            include Test\n          end\n        CR\n      end\n\n      it \"errors if not all tags have a name\" do\n        assert_compile_time_error \"Failed to register service 'foo' (Foo). Tag must have a name.\", <<-CR\n          @[ADI::Autoconfigure(tags: [{ name: \"A\" }, { priority: 123 }])]\n          module Test; end\n\n          @[ADI::Register]\n          record Foo do\n            include Test\n          end\n        CR\n      end\n\n      describe ADI::TaggedIterator do\n        it \"errors if used with unsupported collection type\" do\n          assert_compile_time_error \"Failed to register service 'fqn_tagged_iterator_named_client' (FQNTaggedIteratorNamedClient). Collection type must be one of 'Indexable', 'Iterator', or 'Enumerable'. Got 'Set'.\", <<-CR\n            @[ADI::Register]\n            class FQNTaggedIteratorNamedClient\n              getter services\n\n              def initialize(@[ADI::TaggedIterator] @services : Set(String)); end\n            end\n          CR\n        end\n      end\n\n      it \"errors when both an annotation and a manual entry exist for the same type\" do\n        assert_compile_time_error \"Auto configuration for 'ManualConflict' is already defined in 'AUTO_CONFIGURATIONS'. Remove the annotation or the manual entry.\", <<-CR\n          @[ADI::Autoconfigure(tags: [\"conflict_tag\"])]\n          module ManualConflict; end\n\n          module ConflictSetup\n            macro included\n              macro finished\n                {% verbatim do %}\n                  {% AUTO_CONFIGURATIONS[ManualConflict] = {tags: [\"conflict_tag\"]} %}\n                {% end %}\n              end\n            end\n          end\n\n          ADI.add_compiler_pass ConflictSetup, priority: 200\n\n          @[ADI::Register]\n          record Foo do\n            include ManualConflict\n          end\n        CR\n      end\n    end\n\n    describe \"calls\" do\n      it \"errors if the method of a call is empty\" do\n        assert_compile_time_error \"Auto configuration 'Test': 'calls' method name cannot be empty.\", <<-CR\n          @[ADI::Autoconfigure(calls: [{\"\"}])]\n          module Test; end\n\n          @[ADI::Register]\n          record Foo do\n            include Test\n          end\n        CR\n      end\n\n      it \"errors if the method does not exist on the type\" do\n        assert_compile_time_error \"Auto configuration 'Test': 'calls' method does not exist on service 'foo' (Foo).\", <<-CR\n          @[ADI::Autoconfigure(calls: [{\"foo\"}])]\n          module Test; end\n\n          @[ADI::Register]\n          record Foo do\n            include Test\n          end\n        CR\n      end\n    end\n  end\n\n  describe \"tags\" do\n    it \"injects all services with that tag, ordering based on priority\" do\n      services = ADI.container.partner_client.services\n      services[0].id.should eq 3\n      services[1].id.should eq 1\n      services[2].id.should eq 2\n      services[3].id.should eq 4\n    end\n\n    it \"also allows the service to still have defaults after the tagged services argument\" do\n      service = ADI.container.partner_named_default_client\n      service.services.size.should eq 4\n      service.status.should eq Status::Active\n    end\n\n    it \"converts each tagged service into a proxy\" do\n      services = ADI.container.proxy_tag_client.services\n      services[0].id.should eq 3\n      services[1].id.should eq 1\n      services[2].id.should eq 2\n      services[3].id.should eq 4\n    end\n\n    it \"applies tags from auto_configure\" do\n      ADI.container.other_tag_client.services.size.should eq 2\n      ADI.container.config_tag_client.services.size.should eq 1\n    end\n\n    describe ADI::AutoconfigureTag do\n      it \"without tag\" do\n        ADI.container.fqn_tag_client.services.should eq [FQNService1.new, FQNService2.new]\n      end\n\n      it \"with tag name\" do\n        ADI.container.fqn_tag_named_client.services.should eq [FQNService2.new]\n      end\n\n      it \"with tag name const\" do\n        ADI.container.fqn_tag_const_client.services.should eq [FQNService1.new]\n      end\n\n      it \"with named args\" do\n        services = ADI.container.prioritized_tag_client.services\n        services[0].should eq PrioritizedService2.new\n        services[1].should eq PrioritizedService1.new\n      end\n    end\n\n    describe ADI::TaggedIterator do\n      it \"without tag name\" do\n        collection = ADI.container.fqn_tagged_iterator_client.services\n        collection.should be_a Iterator(Namespace::FQNTagInterface)\n        collection.map(&.itself).should eq [FQNService1.new, FQNService2.new]\n      end\n\n      it \"with tag name\" do\n        collection = ADI.container.fqn_tagged_iterator_named_client.services\n        collection.should be_a Iterator(Namespace::FQNTagInterface)\n        collection.map(&.itself).should eq [FQNService1.new, FQNService2.new]\n      end\n\n      it \"with tag name as const\" do\n        collection = ADI.container.fqn_tagged_iterator_named_const_client.services\n        collection.should be_a Iterator(Namespace::FQNTagInterface)\n        collection.map(&.itself).should eq [FQNService1.new, FQNService2.new]\n      end\n    end\n\n    it \"handles a non service abstract parent type with service child types\" do\n      ADI.container.non_service_abstract_client.services.should eq [NonServiceChild1.new, NonServiceChild2.new]\n    end\n\n    it \"provides an empty array if there were no services configured with the desired tag\" do\n      ADI.container.unused_tag_client.services.should be_empty\n    end\n\n    it \"applies tags to manually configured matching services\" do\n      ADI.container.manual_tag_client.services.should eq [ManualTagService1.new, ManualTagService2.new]\n    end\n  end\n\n  describe \"public\" do\n    it \"when wired up via annotation\" do\n      ADI.container.auto_configured_public_service.should be_a AutoConfiguredPublicService\n    end\n\n    it \"when manually wired up\" do\n      ADI.container.manual_public_service.should be_a ManualPublicService\n    end\n  end\n\n  describe \"calls\" do\n    it \"when wired up via annotation\" do\n      ADI.container.call_autoconfigure_client.values.should eq [6, 3, 1]\n    end\n\n    it \"when manually wired up\" do\n      ADI.container.manual_calls_client.values.should eq [6, 1]\n    end\n  end\n\n  describe \"constructor\" do\n    it \"when wired up via annotation\" do\n      ADI.container.constructor_one.num.should eq 123\n      ADI.container.constructor_two.num.should eq 999\n    end\n\n    it \"when manually wired up\" do\n      ADI.container.manual_constructor_child.num.should eq 42\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/process_bindings_spec.cr",
    "content": "require \"../spec_helper\"\n\nADI.bind id, 20\nADI.bind name, \"Fred\"\n\n# Overrides previous value\nADI.bind value, 66\nADI.bind value, 88\n\n# Mixed type\nADI.bind value : Float32, 3.14\nADI.bind value : Float64, 99.99\n\n@[ADI::Register(public: true, _id: 30)]\n@[ADI::Autoconfigure(bind: {id: 10, name: \"Jim\", alive: true})]\nclass BindingsPriorityClient\n  def initialize(\n    id : Int64,    # Ann has highest priority\n    name : String, # Global bind 2nd highest priority\n    alive : Bool,  # Autoconfigure lowest priority\n    value : Float32,\n  )\n    id.should eq 30\n    name.should eq \"Fred\"\n    alive.should be_true\n    value.should eq 3.14_f32\n  end\nend\n\n@[ADI::Register]\nclass SomeClassService\n  getter value : Int32 = 123\n\n  def_equals\nend\n\n@[ADI::Register]\nrecord SomeStructService, name : String = \"Fred\"\n\n@[ADI::Register(\n  public: true,\n  _some_service: \"@some_class_service\",\n  _some_parameter: \"%app.domain%\",\n  _service_hash: {\n    \"class\"  => \"@some_class_service\",\n    \"struct\" => \"@some_struct_service\",\n  },\n  _service_array: [\n    \"@some_class_service\",\n    \"@some_struct_service\",\n  ]\n)]\nclass BindingsClient\n  def initialize(\n    some_service : SomeClassService,\n    some_parameter : String,\n    service_hash : Hash(String, SomeClassService | SomeStructService),\n    service_array : Array(SomeClassService | SomeStructService),\n    value : Float64,\n    proxy_service : ADI::Proxy(SomeStructService),\n  )\n    some_service.value.should eq 123\n    some_parameter.should eq \"google.com\"\n    service_hash.should eq({\"class\" => SomeClassService.new, \"struct\" => SomeStructService.new})\n    service_array.should eq [SomeClassService.new, SomeStructService.new]\n    value.should eq 99.99\n    proxy_service.should eq SomeStructService.new\n  end\nend\n\n@[ADI::Register(public: true)]\nclass Bindings2Client\n  def initialize(\n    value,\n    proxy_service : SomeStructService,\n  )\n    value.should eq 88\n    proxy_service.should eq SomeStructService.new\n  end\nend\n\nalias MyCustomInt = Int32\n\nADI.bind aliased_number : Int32, 123\nADI.bind aliased_number, 456\n\n@[ADI::Register(public: true)]\nrecord AliasedBindingClient, aliased_number : MyCustomInt\n\ndescribe ADI::ServiceContainer do\n  it \"resolves bindings in proper order Annotation > Global > AutoConfigure\" do\n    ADI.container.bindings_priority_client\n  end\n\n  it \"resolves parameter and service references\" do\n    ADI.container.bindings_client\n    ADI.container.bindings2_client\n  end\n\n  it \"resolves typed bindings when types differ\" do\n    ADI.container.aliased_binding_client.aliased_number.should eq 123\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/process_parameters_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ADI::ServiceContainer::ProcessParameters, tags: \"compiled\" do\n  it \"populates parameter information of registered services\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper.cr\"\n\n      @[ADI::Register(_id: 123, public: true)]\n      class SomeService\n        def initialize(@id : Int32); end\n      end\n\n      ADI::ServiceContainer.new\n\n      macro finished\n        macro finished\n          \\{%\n            parameters = ADI::ServiceContainer::SERVICE_HASH[\"some_service\"][\"parameters\"]\n            id = parameters[\"id\"]\n          %}\n          ASPEC.compile_time_assert(\\{{ parameters.size == 1 }}, \"Expected parameters size to be 1\")\n          ASPEC.compile_time_assert(\\{{ id[\"declaration\"].stringify == \"id : Int32\" }}, \"Expected declaration to be id : Int32\")\n          ASPEC.compile_time_assert(\\{{ id[\"name\"] == \"id\" }}, \"Expected name to be id\")\n          ASPEC.compile_time_assert(\\{{ id[\"internal_name\"] == \"id\" }}, \"Expected internal_name to be id\")\n          ASPEC.compile_time_assert(\\{{ id[\"idx\"] == 0 }}, \"Expected idx to be 0\")\n          ASPEC.compile_time_assert(\\{{ id[\"restriction\"].stringify == \"Int32\" }}, \"Expected restriction to be Int32\")\n          ASPEC.compile_time_assert(\\{{ id[\"resolved_restriction\"].stringify == \"Int32\" }}, \"Expected resolved_restriction to be Int32\")\n          ASPEC.compile_time_assert(\\{{ id[\"default_value\"].nil? }}, \"Expected default_value to be nil\")\n          ASPEC.compile_time_assert(\\{{ id[\"value\"] == 123 }}, \"Expected value to be 123\")\n        end\n      end\n    CR\n  end\n\n  it \"does not override value of manually wired up parameters\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper.cr\"\n\n      class SomeService\n        def initialize(@id : Int32); end\n      end\n\n      module MyExtension\n        macro included\n          macro finished\n            {% verbatim do %}\n              {%\n                SERVICE_HASH[\"some_service\"] = {\n                  class: SomeService,\n                  public: true,\n                  parameters: {\n                    id: {value: 999}\n                  }\n                }\n              %}\n            {% end %}\n          end\n        end\n      end\n\n      ADI.add_compiler_pass MyExtension, :before_optimization, 1028\n      ADI::ServiceContainer.new\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::SERVICE_HASH[\"some_service\"][\"parameters\"][\"id\"][\"value\"] == 999 }}, \"Expected value to be 999\")\n        end\n      end\n    CR\n  end\n\n  it \"does not override value of manually wired up parameters with default value\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper.cr\"\n\n      class SomeService\n        def initialize(@id : Int32 = 123); end\n      end\n\n      module MyExtension\n        macro included\n          macro finished\n            {% verbatim do %}\n              {%\n                SERVICE_HASH[\"some_service\"] = {\n                  class: SomeService,\n                  public: true,\n                  parameters: {\n                    id: {value: 999}\n                  }\n                }\n              %}\n            {% end %}\n          end\n        end\n      end\n\n      ADI.add_compiler_pass MyExtension, :before_optimization, 1028\n      ADI::ServiceContainer.new\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::ServiceContainer::SERVICE_HASH[\"some_service\"][\"parameters\"][\"id\"][\"value\"] == 999 }}, \"Expected value to be 999\")\n        end\n      end\n    CR\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/proxy_spec.cr",
    "content": "require \"../spec_helper\"\n\n@[ADI::Register]\nclass ServiceThree\n  class_getter? instantiated : Bool = false\n  getter value = 123\n\n  def initialize\n    @@instantiated = true\n  end\nend\n\n@[ADI::Register]\nclass ServiceTwo\n  getter value = 123\nend\n\n@[ADI::Register]\nrecord Some::Namespace::Service\n\n@[ADI::Register(public: true)]\nclass ServiceOne\n  getter service_two : ADI::Proxy(ServiceTwo)\n  getter service_three : ADI::Proxy(ServiceThree)\n  getter namespaced_service : ADI::Proxy(Some::Namespace::Service)\n  getter service_two_extra : ADI::Proxy(ServiceTwo)\n\n  def initialize(\n    @service_two : ADI::Proxy(ServiceTwo),\n    @service_three : ADI::Proxy(ServiceThree),\n    @namespaced_service : ADI::Proxy(Some::Namespace::Service),\n    @service_two_extra : ADI::Proxy(ServiceTwo),\n  )\n  end\n\n  def test\n    1 + 1\n  end\n\n  def run\n    @service_three.value\n  end\nend\n\ndescribe ADI::ServiceContainer do\n  describe \"with service proxies\" do\n    it \"delays instantiation until the proxy is used\" do\n      service = ADI.container.service_one\n      ServiceThree.instantiated?.should be_false\n      service.test\n      ServiceThree.instantiated?.should be_false\n      service.run.should eq 123\n      ServiceThree.instantiated?.should be_true\n    end\n\n    it \"exposes the service ID and type of the proxied service\" do\n      service = ADI.container.service_one\n      service.service_two_extra.service_id.should eq \"service_two\"\n      service.service_two_extra.service_type.should eq ServiceTwo\n      service.service_two_extra.instantiated?.should be_false\n      service.service_two_extra.value.should eq 123\n      service.service_two_extra.instantiated?.should be_true\n\n      service.namespaced_service.service_id.should eq \"some_namespace_service\"\n      service.namespaced_service.service_type.should eq Some::Namespace::Service\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/register_services_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\n# Happy Path\n@[ADI::Register]\nclass SingleService\n  getter value : Int32 = 1\nend\n\n@[ADI::Register(public: true)]\nclass SingleClient\n  getter service : SingleService\n\n  def initialize(@service : SingleService); end\nend\n\n# Factories\nclass TestFactory\n  def self.create_factory_tuple(value : Int32) : FactoryTuple\n    FactoryTuple.new value * 3\n  end\n\n  def self.create_factory_service(value_provider : ValueProvider) : FactoryService\n    FactoryService.new value_provider.valuee\n  end\nend\n\n@[ADI::Register(_value: 10, public: true, factory: {TestFactory, \"create_factory_tuple\"})]\nclass FactoryTuple\n  getter value : Int32\n\n  def initialize(@value : Int32); end\nend\n\n@[ADI::Register(_value: 10, public: true, factory: \"double\")]\nclass FactoryString\n  getter value : Int32\n\n  def self.double(value : Int32) : self\n    new value * 2\n  end\n\n  def initialize(@value : Int32); end\nend\n\n@[ADI::Register(_value: 50, public: true)]\nclass PseudoFactory\n  getter value : Int32\n\n  @[ADI::Inject]\n  def self.new_instance(value : Int32) : self\n    new value * 2\n  end\n\n  def initialize(@value : Int32); end\nend\n\n@[ADI::Register]\nrecord ValueProvider, valuee : Int32 = 10\n\n@[ADI::Register(public: true, factory: {TestFactory, \"create_factory_service\"})]\nclass FactoryService\n  getter value : Int32\n\n  def initialize(@value : Int32); end\nend\n\n@[ADI::Register(_value: 99, public: true)]\nclass InstanceInjectService\n  getter value : Int32\n\n  def initialize(value : String)\n    @value = value.to_i\n  end\n\n  @[ADI::Inject]\n  def initialize(@value : Int32); end\nend\n\n# Calls\n@[ADI::Register(public: true, calls: [\n  {\"foo\"},\n  {\"foo\", {3}},\n  {\"foo\", {6}},\n])]\nclass CallClient\n  getter values = [] of Int32\n\n  def foo(value : Int32 = 1)\n    @values << value\n  end\nend\n\ndescribe ADI::ServiceContainer::RegisterServices do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"errors if a service has multiple ADI::Register annotations but not all of them have a name\" do\n      assert_compile_time_error \"Failed to auto register services for 'Foo'. Each service must explicitly provide a name when auto registering more than one service based on the same type.\", <<-CR\n        @[ADI::Register(name: \"one\")]\n        @[ADI::Register]\n        record Foo\n      CR\n    end\n\n    it \"errors if the generic service does not have a name.\" do\n      assert_compile_time_error \"Failed to auto register service for 'Foo(T)'. Generic services must explicitly provide a name.\", <<-CR\n        @[ADI::Register]\n        record Foo(T)\n      CR\n    end\n\n    it \"errors if the service is already registered\" do\n      assert_compile_time_error \"Failed to auto register service for 'my_service' (MyService). It is already registered.\", <<-CR\n        @[ADI::Register]\n        record MyService\n\n        module MyExtension\n          macro included\n            macro finished\n              {% verbatim do %}\n                {%\n                  SERVICE_HASH[\"my_service\"] = {\n                    class:      MyService,\n                  }\n                %}\n              {% end %}\n            end\n          end\n        end\n\n        ADI.add_compiler_pass MyExtension, :before_optimization, 1028\n        CR\n    end\n\n    describe \"factory\" do\n      it \"errors if method is an instance method\" do\n        assert_compile_time_error \"Failed to auto register service 'foo'. Factory method 'foo' within 'Foo' is an instance method.\", <<-CR\n        @[ADI::Register(factory: \"foo\")]\n        record Foo do\n          def foo; end\n        end\n      CR\n      end\n\n      it \"errors if the method is missing\" do\n        assert_compile_time_error \"Failed to auto register service 'foo'. Factory method 'foo' within 'Foo' does not exist.\", <<-CR\n        @[ADI::Register(factory: \"foo\")]\n        record Foo\n      CR\n      end\n    end\n\n    describe \"tags\" do\n      it \"errors if not all tags have a `name` field\" do\n        assert_compile_time_error \"Failed to register service 'foo' (Foo). Tag must have a name.\", <<-CR\n          @[ADI::Register(tags: [{priority: 100}])]\n          record Foo\n        CR\n      end\n\n      it \"errors if not all tags are of the proper type\" do\n        assert_compile_time_error \"Tag must be a 'StringLiteral' or 'NamedTupleLiteral', got 'NumberLiteral'.\", <<-CR\n          @[ADI::Register(tags: [100])]\n          record Foo\n        CR\n      end\n    end\n\n    describe \"calls\" do\n      it \"errors if the method of a call is empty\" do\n        assert_compile_time_error \"'calls' field of service 'foo': method name cannot be empty.\", <<-CR\n        @[ADI::Register(calls: [{\"\"}])]\n        record Foo\n      CR\n      end\n\n      it \"errors if the method does not exist on the type\" do\n        assert_compile_time_error \"'calls' field of service 'foo' (Foo): method does not exist.\", <<-CR\n        @[ADI::Register(calls: [{\"foo\"}])]\n        record Foo\n      CR\n      end\n    end\n  end\n\n  describe \"with factory based services\" do\n    it \"supports passing a tuple\" do\n      ADI::ServiceContainer.new.factory_tuple.value.should eq 30\n    end\n\n    it \"supports passing the string method name\" do\n      ADI::ServiceContainer.new.factory_string.value.should eq 20\n    end\n\n    it \"supports auto resolving factory method service dependencies\" do\n      ADI::ServiceContainer.new.factory_service.value.should eq 10\n    end\n\n    describe \"with an ADI:Inject annotation\" do\n      it \"on a class method\" do\n        ADI::ServiceContainer.new.pseudo_factory.value.should eq 100\n      end\n\n      it \"allows specifying which initialize method to use\" do\n        ADI::ServiceContainer.new.instance_inject_service.value.should eq 99\n      end\n    end\n  end\n\n  it \"correctly resolves the service\" do\n    service = ADI.container.single_client.service\n    service.should be_a SingleService\n    service.value.should eq 1\n  end\n\n  it \"registers calls\" do\n    ADI.container.call_client.values.should eq [1, 3, 6]\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/remove_unused_services_spec.cr",
    "content": "require \"../spec_helper\"\n\n# 1. Unused private service - will be removed\n@[ADI::Register]\nclass RemoveUnusedService\nend\n\n# 2. Used private service - should NOT be removed\n@[ADI::Register]\nclass RemoveUsedService\n  def value\n    10\n  end\nend\n\n@[ADI::Register(public: true)]\nclass RemoveUsedClient\n  def initialize(@dep : RemoveUsedService)\n  end\n\n  def value\n    @dep.value\n  end\nend\n\n# 3. Public alias target - should NOT be removed\nmodule RemoveTestInterface; end\n\n@[ADI::Register]\n@[ADI::AsAlias(\"remove_test_alias\", public: true)]\nclass RemoveAliasedService\n  include RemoveTestInterface\n\n  def value\n    20\n  end\nend\n\ndescribe ADI::ServiceContainer::RemoveUnusedServices do\n  it \"keeps referenced private services\" do\n    ADI.container.remove_used_client.value.should eq 10\n  end\n\n  it \"keeps services with public aliases\" do\n    ADI.container.remove_test_alias.value.should eq 20\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/resolve_parameter_placeholders_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\ndescribe ADI::ServiceContainer::ResolveParameterPlaceholders do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"errors if a parameter references another undefined placeholder.\" do\n      assert_compile_time_error \"Parameter 'parameters[app.name]' referenced unknown parameter 'app.version'.\", <<-CR\n        ADI.configure({\n          parameters: {\n            \"app.name\": \"Testing v%app.version%\"\n          }\n        })\n      CR\n    end\n\n    it \"errors if a parameter references another undefined placeholder within a hash.\" do\n      assert_compile_time_error \"Parameter 'parameters[app.settings][\\\"thing\\\"]' referenced unknown parameter 'app.name'.\", <<-CR\n        ADI.configure({\n          parameters: {\n            \"app.settings\": {\n              \"thing\" => \"%app.name%\",\n            }\n          }\n        })\n      CR\n    end\n\n    it \"errors if a parameter references another undefined placeholder within an array.\" do\n      assert_compile_time_error \"Parameter 'parameters[app.settings][0]' referenced unknown parameter 'app.name'.\", <<-CR\n        ADI.configure({\n          parameters: {\n            \"app.settings\": [\"%app.name%\"]\n          }\n        })\n      CR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/resolve_values_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\nmodule ResolveValuePriorityInterface; end\n\n@[ADI::Register]\n@[ADI::AsAlias(\"my_string_alias\")]\nrecord ServicePriorityOne do\n  include ResolveValuePriorityInterface\nend\n\n@[ADI::Register]\nrecord ServicePriorityTwo do\n  include ResolveValuePriorityInterface\nend\n\n@[ADI::Register]\n@[ADI::AsAlias(ResolveValuePriorityInterface)]\nrecord ServicePriorityFour do\n  include ResolveValuePriorityInterface\nend\n\n@[ADI::Register]\nrecord ServicePriorityThree\n\n@[ADI::Register(_ann_bind: 1000, public: true)]\n@[ADI::Autoconfigure(bind: {ann_bind: 800, global_bind: 800, auto_configure_bind: 800})]\nclass ValuePriorityService\n  getter ann_bind, global_bind, auto_configure_bind, default_value, nilable_type\n\n  def initialize(\n    ann_bind : Int32,\n    global_bind : Int32,\n    auto_configure_bind : Int32,\n    nilable_type : Int32?,\n    default_value : Int32 = 700,\n  )\n    ann_bind.should eq 1000\n    global_bind.should eq 900\n    auto_configure_bind.should eq 800\n    nilable_type.should be_nil\n    default_value.should eq 700\n  end\nend\n\nADI.bind ann_bind : Int32, 900\nADI.bind global_bind : Int32, 900\n\n@[ADI::Register(_alias_overridden_by_ann_bind: \"@service_priority_one\", _alias_service_via_string_alias: \"@my_string_alias\", public: true)]\n@[ADI::Autoconfigure(bind: {alias_overridden_by_auto_configure_bind: \"@service_priority_two\"})]\nclass ServiceValuePriorityService\n  getter explicit_auto_wire, interface_service_matches_name, default_alias, alias_overridden_by_ann_bind\n\n  def initialize(\n    explicit_auto_wire : ServicePriorityThree,\n    service_priority_two : ResolveValuePriorityInterface,\n    default_alias : ResolveValuePriorityInterface,\n    alias_overridden_by_ann_bind : ResolveValuePriorityInterface,\n    alias_overridden_by_global_bind : ResolveValuePriorityInterface,\n    alias_overridden_by_auto_configure_bind : ResolveValuePriorityInterface,\n\n    # Validates container rewrites the alias service ID to the real underlying service ID\n    alias_service_via_string_alias : ResolveValuePriorityInterface,\n  )\n    explicit_auto_wire.should be_a ServicePriorityThree\n    service_priority_two.should be_a ServicePriorityTwo\n    default_alias.should be_a ServicePriorityFour\n    alias_overridden_by_ann_bind.should be_a ServicePriorityOne\n    alias_overridden_by_global_bind.should be_a ServicePriorityOne\n    alias_overridden_by_auto_configure_bind.should be_a ServicePriorityTwo\n    alias_service_via_string_alias.should be_a ServicePriorityOne\n  end\nend\n\nADI.bind alias_overridden_by_global_bind : ResolveValuePriorityInterface, \"@service_priority_one\"\n\n@[ADI::Register(_value: false, public: true)]\nrecord BoolExplicitArgumentService, value : Bool\n\ndescribe ADI::ServiceContainer::ResolveValues do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"errors if a service string reference doesn't map to a known service\" do\n      assert_compile_time_error \"Service 'foo' (Foo) references undefined service 'bar'.\", <<-CR\n        @[ADI::Register(_id: \"@bar\")]\n        record Foo, id : Int32\n      CR\n    end\n  end\n\n  it \"resolves the values with the expected priority\" do\n    ADI.container.value_priority_service\n  end\n\n  it \"resolves service references with the expected priority\" do\n    ADI.container.service_value_priority_service\n  end\n\n  it \"allows passing `false` as an argument\" do\n    ADI.container.bool_explicit_argument_service.value.should be_false\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/untyped_with_default_spec.cr",
    "content": "require \"../spec_helper\"\n\nrecord NotAService, id : Int32 = 1234\n\n@[ADI::Register(public: true)]\nclass SomeUntypedService\n  getter service : NotAService\n\n  def initialize(@service = NotAService.new); end\nend\n\ndescribe ADI::ServiceContainer do\n  it \"when the constructor arg is not typed, but has a default\" do\n    ADI::ServiceContainer.new.some_untyped_service.service.id.should eq 1234\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/validate_arguments_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\ndescribe ADI::ServiceContainer::ValidateArguments, tags: \"compiled\" do\n  describe \"compiler errors\" do\n    it \"errors if a expects a string value parameter but it is not of that type\" do\n      assert_compile_time_error \"Service 'foo' (Foo): parameter expects a 'String' but got 'Int32'.\", <<-'CR'\n        @[ADI::Register(_value: 123)]\n        record Foo, value : String\n      CR\n\n      assert_compile_time_error \"Service 'foo' (Foo): parameter expects a 'String' but got 'UInt8'.\", <<-'CR'\n        @[ADI::Register(_value: 123_u8)]\n        record Foo, value : String\n      CR\n\n      assert_compile_time_error \"Service 'foo' (Foo): parameter expects a 'String' but got 'Bool'.\", <<-'CR'\n        @[ADI::Register(_value: true)]\n        record Foo, value : String\n      CR\n\n      assert_compile_time_error \"Service 'foo' (Foo): parameter expects a 'String' but got 'Float64'.\", <<-'CR'\n        @[ADI::Register(_value: 3.14)]\n        record Foo, value : String\n      CR\n\n      assert_compile_time_error \"Service 'foo' (Foo): parameter expects a 'String' but got 'Symbol'.\", <<-'CR'\n        @[ADI::Register(_value: :foo)]\n        record Foo, value : String\n      CR\n    end\n\n    it \"still errors with explicit calls even if they are not of the proper type\" do\n      assert_compile_time_error \"expected argument 'value' to 'Foo.new' to be String, not Int32\", <<-'CR'\n        @[ADI::Register(_value: \"123\".to_i, public: true)]\n        record Foo, value : String\n\n        ADI.container.foo\n      CR\n    end\n\n    it \"errors if a parameter resolves to a service of the incorrect type\" do\n      assert_compile_time_error \"Service 'foo' (Foo): parameter expects 'Int32' but the resolved service 'bar' is of type 'Bar'.\", <<-'CR'\n        @[ADI::Register]\n        record Bar\n\n        @[ADI::Register(_value: \"@bar\", public: true)]\n        record Foo, value : Int32\n      CR\n    end\n\n    describe NamedTuple do\n      it \"errors if configuration is missing a non-nilable property\" do\n        assert_compile_time_error \"Configuration value 'test.connection' is missing required value for 'port' of type 'Int32'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n\n            property connection : NamedTuple(hostname: String, username: String, password: String, port: Int32)\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              connection: {\n                hostname: \"my-db\",\n                username: \"user\",\n                password: \"pass\",\n              },\n            },\n          })\n        CR\n      end\n\n      it \"errors if there is a type mismatch\" do\n        assert_compile_time_error \"Expected configuration value 'test.connection.hostname' to be a 'String', but got 'Int32'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n            property connection : NamedTuple(hostname: String)\n          end\n          ADI.register_extension \"test\", Schema\n          ADI.configure({\n            test: {\n              connection: {\n                hostname: 10,\n              },\n            },\n          })\n        CR\n      end\n\n      it \"errors if there is a type mismatch within an array type\" do\n        assert_compile_time_error \"Expected configuration value 'test.connection.ports[1]' to be a 'Int32', but got 'String'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n            property connection : NamedTuple(ports: Array(Int32))\n          end\n          ADI.register_extension \"test\", Schema\n          ADI.configure({\n            test: {\n              connection: {\n                ports: [\n                  10,\n                  \"blah\"\n                ]\n              },\n            },\n          })\n        CR\n      end\n\n      it \"errors if there is a type mismatch within a nilable array type\" do\n        assert_compile_time_error \"Expected configuration value 'test.connection.ports[1]' to be a 'Int32', but got 'String'.\", <<-'CR'\n          module Schema\n            include ADI::Extension::Schema\n            property connection : NamedTuple(ports: Array(Int32)?)\n          end\n          ADI.register_extension \"test\", Schema\n          ADI.configure({\n            test: {\n              connection: {\n                ports: [\n                  10,\n                  \"blah\"\n                ]\n              },\n            },\n          })\n        CR\n      end\n    end\n\n    describe \"array_of\" do\n      it \"errors on type mismatch in array within array_of object\" do\n        assert_compile_time_error \"Expected configuration value 'test.rules[0].priorities[2]' to be a 'String', but got 'Int32'.\", <<-'CR'\n          require \"../spec_helper\"\n\n          module Schema\n            include ADI::Extension::Schema\n\n            array_of rules,\n              priorities : Array(String)? = nil\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              rules: [\n                {priorities: [\"json\", \"xml\", 2]},\n              ],\n            },\n          })\n        CR\n      end\n    end\n\n    describe \"object_of\" do\n      it \"errors on type mismatch in array within object_of object\" do\n        assert_compile_time_error \"Expected configuration value 'test.rule.priorities[2]' to be a 'String', but got 'Int32'.\", <<-'CR'\n          require \"../spec_helper\"\n\n          module Schema\n            include ADI::Extension::Schema\n\n            object_of rule, priorities : Array(String)? = nil\n          end\n\n          ADI.register_extension \"test\", Schema\n\n          ADI.configure({\n            test: {\n              rule: {priorities: [\"json\", \"xml\", 2]},\n            },\n          })\n        CR\n      end\n    end\n  end\n\n  it \"sets missing NT keys to `nil` if the type is nilable\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        property connection : NamedTuple(hostname: String, username: String, password: String, port: Int32?)\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          connection: {\n            hostname: \"my-db\",\n            username: \"user\",\n            password: \"pass\",\n          },\n        },\n      })\n\n      macro finished\n        macro finished\n          ASPEC.compile_time_assert(\\{{ ADI::CONFIG[\"test\"][\"connection\"][\"port\"].nil? }}, \"Expected port to be nil\")\n        end\n      end\n    CR\n  end\n\n  it \"properly checks type within array of array_of object\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        array_of rules,\n          priorities : Array(String)? = nil\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          rules: [\n            {priorities: [\"json\", \"xml\"]},\n          ],\n        },\n      })\n    CR\n  end\n\n  it \"properly checks type within array of object_of object\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      module Schema\n        include ADI::Extension::Schema\n\n        object_of rule, priorities : Array(String)? = nil\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      ADI.configure({\n        test: {\n          rule: {priorities: [\"json\", \"xml\"]},\n        },\n      })\n    CR\n  end\n\n  it \"allows calls to `String` parameters\" do\n    ASPEC::Methods.assert_compiles <<-'CR'\n      require \"../spec_helper\"\n\n      @[ADI::Register(_value: 123.to_s, public: true)]\n      record Foo, value : String\n\n      ADI.container.foo\n    CR\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/compiler_passes/validate_generics_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"../spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\n@[ADI::Register(Int32, Bool, public: true, name: \"int_service\")]\n@[ADI::Register(Float64, Bool, public: true, name: \"float_service\")]\nstruct GenericServiceBase(T, B)\n  def type\n    {T, B}\n  end\nend\n\ndescribe ADI::ServiceContainer::ValidateGenerics do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"errors if the generic service does not provide the generic arguments.\" do\n      assert_compile_time_error \"Failed to register service 'foo_service'. Generic services must provide the types to use via the 'generics' field.\", <<-CR\n        @[ADI::Register(name: \"foo_service\")]\n        record Foo(T)\n      CR\n    end\n\n    it \"errors if there is a generic argument count mismatch.\" do\n      assert_compile_time_error \"Failed to register service 'foo_service'. Expected 1 generics types got 2.\", <<-CR\n        @[ADI::Register(String, Bool, name: \"foo_service\")]\n        record Foo(T)\n      CR\n    end\n  end\n\n  it \"correctly initializes the service with the given generic arguments\" do\n    ADI.container.int_service.type.should eq({Int32, Bool})\n    ADI.container.float_service.type.should eq({Float64, Bool})\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/extension_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compiles code, line: line, preamble: %(require \"./spec_helper.cr\")\nend\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"./spec_helper.cr\"), postamble: \"ADI::ServiceContainer.new\"\nend\n\ndescribe ADI::Extension, tags: \"compiled\" do\n  it \"happy path\" do\n    assert_compiles <<-'CR'\n      module Schema\n        include ADI::Extension::Schema\n        property id : Int32\n        property name : String = \"Fred\"\n      end\n\n      ADI.register_extension \"test\", Schema\n      ADI.configure({\n        test: {\n          id: 10,\n        },\n      })\n\n      macro finished\n        macro finished\n          \\{%\n             options = Schema::OPTIONS\n          %}\n          ASPEC.compile_time_assert(\\{{ options.size == 2 }}, \"Expected options size to be 2\")\n          ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"id\" }}, \"Expected first option name to be id\")\n          ASPEC.compile_time_assert(\\{{ options[0][\"type\"] == Int32 }}, \"Expected first option type to be Int32\")\n          ASPEC.compile_time_assert(\\{{ options[0][\"default\"].nil? }}, \"Expected first option default to be nil\")\n          ASPEC.compile_time_assert(\\{{ options[1][\"name\"] == \"name\" }}, \"Expected second option name to be name\")\n          ASPEC.compile_time_assert(\\{{ options[1][\"type\"] == String }}, \"Expected second option type to be String\")\n          ASPEC.compile_time_assert(\\{{ options[1][\"default\"] == \"Fred\" }}, \"Expected second option default to be Fred\")\n          ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"id\",\"type\":\"`Int32`\",\"default\":\"``\"}, {\"name\":\"name\",\"type\":\"`String`\",\"default\":\"`Fred`\"}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n        end\n      end\n    CR\n  end\n\n  it \"allows using NoReturn array default to inherit type of the array\" do\n    assert_compiles <<-'CR'\n      module Schema\n        include ADI::Extension::Schema\n        property values : Array(Int32 | String) = [] of NoReturn\n      end\n\n      ADI.register_extension \"test\", Schema\n\n      macro finished\n        macro finished\n          \\{%\n             options = Schema::OPTIONS\n          %}\n          ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n          ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"values\" }}, \"Expected option name to be values\")\n          ASPEC.compile_time_assert(\\{{ options[0][\"type\"] == Array(Int32 | String) }}, \"Expected option type to be Array(Int32 | String)\")\n          ASPEC.compile_time_assert(\\{{ options[0][\"default\"].stringify == \"Array(Int32 | String).new\" }}, \"Expected option default to be Array(Int32 | String).new\")\n          ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"values\",\"type\":\"`Array(Int32 | String)`\",\"default\":\"`Array(Int32 | String).new`\"}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n        end\n      end\n    CR\n  end\n\n  describe \"object_of / object_of?\" do\n    it \"is able to resolve parameters from the object value\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          object_of connection, username : String, password : String, port : Int32 = 1234\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            connection: {\n              username: \"%app.username%\",\n              password: \"abc123\",\n            },\n          },\n          parameters: {\n            \"app.username\": \"addminn\",\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n               config = ADI::CONFIG[\"test\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"username\"] == \"addminn\" }}, \"Expected connection username to be addminn\")\n            ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"password\"] == \"abc123\" }}, \"Expected connection password to be abc123\")\n            ASPEC.compile_time_assert(\\{{ config[\"connection\"][\"port\"] == 1234 }}, \"Expected connection port to be 1234\")\n          end\n        end\n      CR\n    end\n\n    it \"errors if a required configuration value has not been provided\" do\n      assert_compile_time_error \"Configuration value 'test.connection' is missing required value for 'port' of type 'Int32'.\", <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_of connection, username : String, password : String, port : Int32\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            connection: {\n              username: \"admin\",\n              password: \"abc123\",\n            },\n          },\n        })\n      CR\n    end\n\n    it \"errors if a configuration value has been provided a value of the wrong type\" do\n      assert_compile_time_error \"Expected configuration value 'test.connection.port' to be a 'Int32', but got 'Bool'.\", <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_of connection, username : String, password : String, port : Int32\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            connection: {\n              username: \"admin\",\n              password: \"abc123\",\n              port: false,\n            },\n          },\n        })\n      CR\n    end\n\n    it \"object_of\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          object_of rule, id : Int32, stop : Bool = false\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        ADI.configure({\n          test: {\n            rule: {\n              id: 10\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n             options = Schema::OPTIONS\n             members = options[0][\"members\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"rule\" }}, \"Expected option name to be rule\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"NamedTuple(T)\" }}, \"Expected option type to be NamedTuple(T)\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"default\"].nil? }}, \"Expected option default to be nil\")\n            ASPEC.compile_time_assert(\\{{ members.size == 3 }}, \"Expected members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].type.stringify == \"Int32\" }}, \"Expected id type to be Int32\")\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].value.nil? }}, \"Expected id value to be nil\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].type.stringify == \"Bool\" }}, \"Expected stop type to be Bool\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].value == false }}, \"Expected stop value to be false\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"rule\",\"type\":\"`NamedTuple(T)`\",\"default\":\"``\",\"members\":[{\"name\":\"id\",\"type\":\"`Int32`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"stop\",\"type\":\"`Bool`\",\"default\":\"`false`\",\"doc\":\"\"}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n\n    it \"object_of with assign\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          object_of rule = {id: 999}, id : Int32, stop : Bool = false\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n             options = Schema::OPTIONS\n             members = options[0][\"members\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"rule\" }}, \"Expected option name to be rule\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"NamedTuple(T)\" }}, \"Expected option type to be NamedTuple(T)\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"default\"] == {id: 999, stop: false} }}, \"Expected option default to match\")\n            ASPEC.compile_time_assert(\\{{ members.size == 3 }}, \"Expected members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].type.stringify == \"Int32\" }}, \"Expected id type to be Int32\")\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].value.nil? }}, \"Expected id value to be nil\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].type.stringify == \"Bool\" }}, \"Expected stop type to be Bool\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].value == false }}, \"Expected stop value to be false\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"rule\",\"type\":\"`NamedTuple(T)`\",\"default\":\"`{id: 999}`\",\"members\":[{\"name\":\"id\",\"type\":\"`Int32`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"stop\",\"type\":\"`Bool`\",\"default\":\"`false`\",\"doc\":\"\"}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n\n    it \"object_of?\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          object_of? rule, id : Int32, stop : Bool = false\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n             options = Schema::OPTIONS\n             members = options[0][\"members\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"rule\" }}, \"Expected option name to be rule\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"(NamedTuple(T) | Nil)\" }}, \"Expected option type to be (NamedTuple(T) | Nil)\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"default\"].nil? }}, \"Expected option default to be nil\")\n            ASPEC.compile_time_assert(\\{{ members.size == 3 }}, \"Expected members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].type.stringify == \"Int32\" }}, \"Expected id type to be Int32\")\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].value.nil? }}, \"Expected id value to be nil\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].type.stringify == \"Bool\" }}, \"Expected stop type to be Bool\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].value == false }}, \"Expected stop value to be false\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"rule\",\"type\":\"`(NamedTuple(T) | Nil)`\",\"default\":\"`nil`\",\"members\":[{\"name\":\"id\",\"type\":\"`Int32`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"stop\",\"type\":\"`Bool`\",\"default\":\"`false`\",\"doc\":\"\"}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n\n    it \"object_of? with assign\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          object_of? rule = {id: 999}, id : Int32, stop : Bool = false\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n             options = Schema::OPTIONS\n             members = options[0][\"members\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"rule\" }}, \"Expected option name to be rule\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"(NamedTuple(T) | Nil)\" }}, \"Expected option type to be (NamedTuple(T) | Nil)\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"default\"].nil? }}, \"Expected option default to be nil\")\n            ASPEC.compile_time_assert(\\{{ members.size == 3 }}, \"Expected members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].type.stringify == \"Int32\" }}, \"Expected id type to be Int32\")\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].value.nil? }}, \"Expected id value to be nil\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].type.stringify == \"Bool\" }}, \"Expected stop type to be Bool\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].value == false }}, \"Expected stop value to be false\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"rule\",\"type\":\"`(NamedTuple(T) | Nil)`\",\"default\":\"`{id: 999}`\",\"members\":[{\"name\":\"id\",\"type\":\"`Int32`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"stop\",\"type\":\"`Bool`\",\"default\":\"`false`\",\"doc\":\"\"}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n  end\n\n  describe \"array_of / array_of?\" do\n    it \"array_of\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          array_of rules, id : Int32, stop : Bool = false\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n             options = Schema::OPTIONS\n             members = options[0][\"members\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"rules\" }}, \"Expected option name to be rules\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"Array(T)\" }}, \"Expected option type to be Array(T)\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"default\"].stringify == \"[]\" }}, \"Expected option default to be empty array\")\n            ASPEC.compile_time_assert(\\{{ members.size == 3 }}, \"Expected members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].type.stringify == \"Int32\" }}, \"Expected id type to be Int32\")\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].value.nil? }}, \"Expected id value to be nil\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].type.stringify == \"Bool\" }}, \"Expected stop type to be Bool\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].value == false }}, \"Expected stop value to be false\")\n          end\n        end\n      CR\n    end\n\n    it \"array_of with assign\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          array_of rules = [{id: 10}], id : Int32, stop : Bool = false\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n             options = Schema::OPTIONS\n             members = options[0][\"members\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"rules\" }}, \"Expected option name to be rules\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"Array(T)\" }}, \"Expected option type to be Array(T)\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"default\"] == [{id: 10, stop: false}] }}, \"Expected option default to match\")\n            ASPEC.compile_time_assert(\\{{ members.size == 3 }}, \"Expected members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].type.stringify == \"Int32\" }}, \"Expected id type to be Int32\")\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].value.nil? }}, \"Expected id value to be nil\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].type.stringify == \"Bool\" }}, \"Expected stop type to be Bool\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].value == false }}, \"Expected stop value to be false\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"rules\",\"type\":\"`Array(T)`\",\"default\":\"`[{id: 10}]`\",\"members\":[{\"name\":\"id\",\"type\":\"`Int32`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"stop\",\"type\":\"`Bool`\",\"default\":\"`false`\",\"doc\":\"\"}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n\n    it \"array_of?\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          array_of? rules, id : Int32, stop : Bool = false\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n             options = Schema::OPTIONS\n             members = options[0][\"members\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"rules\" }}, \"Expected option name to be rules\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"(Array(T) | Nil)\" }}, \"Expected option type to be (Array(T) | Nil)\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"default\"].nil? }}, \"Expected option default to be nil\")\n            ASPEC.compile_time_assert(\\{{ members.size == 3 }}, \"Expected members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].type.stringify == \"Int32\" }}, \"Expected id type to be Int32\")\n            ASPEC.compile_time_assert(\\{{ members[\"id\"].value.nil? }}, \"Expected id value to be nil\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].type.stringify == \"Bool\" }}, \"Expected stop type to be Bool\")\n            ASPEC.compile_time_assert(\\{{ members[\"stop\"].value == false }}, \"Expected stop value to be false\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"rules\",\"type\":\"`(Array(T) | Nil)`\",\"default\":\"`nil`\",\"members\":[{\"name\":\"id\",\"type\":\"`Int32`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"stop\",\"type\":\"`Bool`\",\"default\":\"`false`\",\"doc\":\"\"}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n  end\n\n  describe \"object_schema\" do\n    it \"stores schema in OBJECT_SCHEMAS\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : String = \"hmac.sha256\"\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n               schemas = Schema::OBJECT_SCHEMAS\n               jwt_schema = schemas[\"JwtConfig\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ schemas.size == 1 }}, \"Expected schemas size to be 1\")\n            ASPEC.compile_time_assert(\\{{ schemas[\"JwtConfig\"] != nil }}, \"Expected JwtConfig schema to exist\")\n            ASPEC.compile_time_assert(\\{{ jwt_schema[\"members\"].size == 3 }}, \"Expected jwt_schema members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ jwt_schema[\"members\"][\"secret\"].type.stringify == \"String\" }}, \"Expected secret type to be String\")\n            ASPEC.compile_time_assert(\\{{ jwt_schema[\"members\"][\"secret\"].value.nil? }}, \"Expected secret value to be nil\")\n            ASPEC.compile_time_assert(\\{{ jwt_schema[\"members\"][\"algorithm\"].type.stringify == \"String\" }}, \"Expected algorithm type to be String\")\n            ASPEC.compile_time_assert(\\{{ jwt_schema[\"members\"][\"algorithm\"].value == \"hmac.sha256\" }}, \"Expected algorithm value to be hmac.sha256\")\n          end\n        end\n      CR\n    end\n\n    it \"supports nested object_schema references\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema InnerConfig,\n            value : String\n\n          object_schema OuterConfig,\n            name : String,\n            inner : InnerConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n               schemas = Schema::OBJECT_SCHEMAS\n               outer_schema = schemas[\"OuterConfig\"]\n               inner_member = outer_schema[\"members\"][\"inner\"]\n            %}\n            # The inner member should have nested members from InnerConfig\n            ASPEC.compile_time_assert(\\{{ inner_member[\"members\"] != nil }}, \"Expected inner member to have members\")\n            ASPEC.compile_time_assert(\\{{ inner_member[\"members\"][\"value\"].type.stringify == \"String\" }}, \"Expected inner value type to be String\")\n            # OuterConfig's members_string should include InnerConfig's nested members\n            ASPEC.compile_time_assert(\\{{ outer_schema[\"members_string\"] == %([{\"name\":\"name\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"inner\",\"type\":\"`InnerConfig`\",\"default\":\"``\",\"doc\":\"\",\"members\":[{\"name\":\"value\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"}]}]) }}, \"Expected members_string to match\")\n          end\n        end\n      CR\n    end\n  end\n\n  describe \"map_of / map_of?\" do\n    it \"map_of\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          map_of hubs, url : String, port : Int32 = 5432\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n               options = Schema::OPTIONS\n               members = options[0][\"members\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"hubs\" }}, \"Expected option name to be hubs\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"Hash(K, V)\" }}, \"Expected option type to be Hash(K, V)\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"default\"].stringify == \"{__nil: nil}\" }}, \"Expected option default to be {__nil: nil}\")\n            ASPEC.compile_time_assert(\\{{ members.size == 3 }}, \"Expected members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ members[\"url\"].type.stringify == \"String\" }}, \"Expected url type to be String\")\n            ASPEC.compile_time_assert(\\{{ members[\"url\"].value.nil? }}, \"Expected url value to be nil\")\n            ASPEC.compile_time_assert(\\{{ members[\"port\"].type.stringify == \"Int32\" }}, \"Expected port type to be Int32\")\n            ASPEC.compile_time_assert(\\{{ members[\"port\"].value == 5432 }}, \"Expected port value to be 5432\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"hubs\",\"type\":\"`Hash(K, V)`\",\"default\":\"`{__nil: nil}`\",\"members\":[{\"name\":\"url\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"port\",\"type\":\"`Int32`\",\"default\":\"`5432`\",\"doc\":\"\"}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n\n    it \"map_of?\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          map_of? hubs, url : String, port : Int32 = 5432\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n               options = Schema::OPTIONS\n               members = options[0][\"members\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"hubs\" }}, \"Expected option name to be hubs\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"(Hash(K, V) | Nil)\" }}, \"Expected option type to be (Hash(K, V) | Nil)\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"default\"].nil? }}, \"Expected option default to be nil\")\n            ASPEC.compile_time_assert(\\{{ members.size == 3 }}, \"Expected members size to be 3\") # Account for __nil\n            ASPEC.compile_time_assert(\\{{ members[\"url\"].type.stringify == \"String\" }}, \"Expected url type to be String\")\n            ASPEC.compile_time_assert(\\{{ members[\"port\"].type.stringify == \"Int32\" }}, \"Expected port type to be Int32\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"hubs\",\"type\":\"`(Hash(K, V) | Nil)`\",\"default\":\"`nil`\",\"members\":[{\"name\":\"url\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"port\",\"type\":\"`Int32`\",\"default\":\"`5432`\",\"doc\":\"\"}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n\n    it \"map_of with object_schema reference\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : String = \"hmac.sha256\"\n\n          map_of hubs,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n               options = Schema::OPTIONS\n               jwt_member = options[0][\"members\"][\"jwt\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ jwt_member[\"members\"] != nil }}, \"Expected jwt member to have members\")\n            ASPEC.compile_time_assert(\\{{ jwt_member[\"members\"][\"secret\"].type.stringify == \"String\" }}, \"Expected secret type to be String\")\n            ASPEC.compile_time_assert(\\{{ jwt_member[\"members\"][\"algorithm\"].value == \"hmac.sha256\" }}, \"Expected algorithm value to be hmac.sha256\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"hubs\",\"type\":\"`Hash(K, V)`\",\"default\":\"`{__nil: nil}`\",\"members\":[{\"name\":\"url\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"jwt\",\"type\":\"`JwtConfig`\",\"default\":\"``\",\"doc\":\"\",\"members\":[{\"name\":\"secret\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"algorithm\",\"type\":\"`String`\",\"default\":\"`hmac.sha256`\",\"doc\":\"\"}]}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n\n    it \"map_of with custom default using assignment syntax\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n          map_of hubs = {default: {url: \"localhost\", port: 8080}}, url : String, port : Int32 = 5432\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n               options = Schema::OPTIONS\n               default = options[0][\"default\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"name\"] == \"hubs\" }}, \"Expected option name to be hubs\")\n            ASPEC.compile_time_assert(\\{{ options[0][\"type\"].stringify == \"Hash(K, V)\" }}, \"Expected option type to be Hash(K, V)\")\n            # Custom default should be preserved\n            ASPEC.compile_time_assert(\\{{ default[\"default\"][\"url\"] == \"localhost\" }}, \"Expected default url to be localhost\")\n            ASPEC.compile_time_assert(\\{{ default[\"default\"][\"port\"] == 8080 }}, \"Expected default port to be 8080\")\n          end\n        end\n      CR\n    end\n  end\n\n  describe \"object_schema in array_of\" do\n    it \"array_of with object_schema reference\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : String = \"hmac.sha256\"\n\n          array_of items,\n            name : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n               options = Schema::OPTIONS\n               jwt_member = options[0][\"members\"][\"jwt\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ jwt_member[\"members\"] != nil }}, \"Expected jwt member to have members\")\n            ASPEC.compile_time_assert(\\{{ jwt_member[\"members\"][\"secret\"].type.stringify == \"String\" }}, \"Expected secret type to be String\")\n            ASPEC.compile_time_assert(\\{{ jwt_member[\"members\"][\"algorithm\"].value == \"hmac.sha256\" }}, \"Expected algorithm value to be hmac.sha256\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"items\",\"type\":\"`Array(T)`\",\"default\":\"`[]`\",\"members\":[{\"name\":\"name\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"jwt\",\"type\":\"`JwtConfig`\",\"default\":\"``\",\"doc\":\"\",\"members\":[{\"name\":\"secret\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"algorithm\",\"type\":\"`String`\",\"default\":\"`hmac.sha256`\",\"doc\":\"\"}]}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n  end\n\n  describe \"object_schema in object_of\" do\n    it \"object_of with object_schema reference\" do\n      assert_compiles <<-'CR'\n        module Schema\n          include ADI::Extension::Schema\n\n          object_schema JwtConfig,\n            secret : String,\n            algorithm : String = \"hmac.sha256\"\n\n          # Use object_of? since we're not providing config - just testing OPTIONS structure\n          object_of? connection,\n            url : String,\n            jwt : JwtConfig\n        end\n\n        ADI.register_extension \"test\", Schema\n\n        macro finished\n          macro finished\n            \\{%\n               options = Schema::OPTIONS\n               jwt_member = options[0][\"members\"][\"jwt\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ options.size == 1 }}, \"Expected options size to be 1\")\n            ASPEC.compile_time_assert(\\{{ jwt_member[\"members\"] != nil }}, \"Expected jwt member to have members\")\n            ASPEC.compile_time_assert(\\{{ jwt_member[\"members\"][\"secret\"].type.stringify == \"String\" }}, \"Expected secret type to be String\")\n            ASPEC.compile_time_assert(\\{{ jwt_member[\"members\"][\"algorithm\"].value == \"hmac.sha256\" }}, \"Expected algorithm value to be hmac.sha256\")\n            ASPEC.compile_time_assert(\\{{ Schema::CONFIG_DOCS.stringify == %([{\"name\":\"connection\",\"type\":\"`(NamedTuple(T) | Nil)`\",\"default\":\"`nil`\",\"members\":[{\"name\":\"url\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"jwt\",\"type\":\"`JwtConfig`\",\"default\":\"``\",\"doc\":\"\",\"members\":[{\"name\":\"secret\",\"type\":\"`String`\",\"default\":\"``\",\"doc\":\"\"},{\"name\":\"algorithm\",\"type\":\"`String`\",\"default\":\"`hmac.sha256`\",\"doc\":\"\"}]}]}] of Nil) }}, \"Expected CONFIG_DOCS to match\")\n          end\n        end\n      CR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"../src/athena-dependency_injection\"\n\nrequire \"athena-spec\"\nrequire \"../src/spec\"\n\nADI.configure({\n  parameters: {\n    \"app.mapping\": {\n      10 => \"%app.domain%\",\n      20 => \"%app.placeholder%\", # Resolves recursively out of order\n    },\n    \"app.nested_mapping\": {\n      \"string\"       => \"%app.domain%\",\n      \"array\"        => \"%app.array%\",\n      \"nested_array\" => \"%app.nested_array%\",\n      \"bool\"         => \"%app.enable_v2_protocol%\",\n      \"escaped\"      => \"foo%%bar\", # Escape `%` in hash\n    },\n    \"app.nested_array\": [\n      \"%app.array%\",\n      \"%app.domain%\",\n      \"foo%%bar\", # Escape `%` in array\n    ],\n    \"app.array\": [\n      \"%app.domain%\",\n      \"%app.placeholder%\",\n      \"%app.with_percent%\",\n      \"%app.with_percent_placeholder%\",\n    ],\n    \"app.domain\":                   \"google.com\",\n    \"app.with_percent\":             \"foo%%bar\", # Escape `%`\n    \"app.with_percent_placeholder\": \"https://%app.domain%/path/t%%o/thing\",\n    \"app.enable_v2_protocol\":       false,\n    \"app.full_url\":                 \"Visit: %app.placeholder%!\", # String that contains a placeholder to a yet to be defined parameter that'll need re-processed\n    \"app.placeholder\":              \"https://%app.domain%/path/to/thing\",\n    \"app.placeholders\":             \"https://%app.domain%/path/to/%app.enable_v2_protocol%\",\n    \"app.empty\":                    \"\",\n    \"app.empty.reference\":          \"%app.empty%\",\n    \"app.empty.reference.nested\":   \"%app.empty.reference%\",\n  },\n})\n"
  },
  {
    "path": "src/components/dependency_injection/spec/spec_spec.cr",
    "content": "require \"./spec_helper\"\n\nmodule TransformerInterface\n  abstract def transform\nend\n\nclass FakeTransformer\n  include TransformerInterface\n\n  def transform\n  end\nend\n\nclass ADI::Spec::MockableServiceContainer\n  property reverse_transformer : TransformerInterface?\nend\n\ndescribe ADI::Spec::MockableServiceContainer do\n  it \"allows mocking services\" do\n    mock_container = ADI::Spec::MockableServiceContainer.new\n\n    mock_container.reverse_transformer = FakeTransformer.new\n\n    mock_container.reverse_transformer.should be_a FakeTransformer\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/abstract_bundle.cr",
    "content": "module Athena::DependencyInjection\n  # :nodoc:\n  abstract struct AbstractBundle\n    PASSES = [] of Nil\n  end\n\n  # Registers the provided *bundle*.\n  #\n  # See the [Getting Started](/getting_started/configuration) docs for more information.\n  macro register_bundle(bundle)\n    {%\n      resolved_bundle = bundle.resolve\n\n      unless resolved_bundle <= Athena::DependencyInjection::AbstractBundle\n        bundle.raise \"The provided bundle '#{bundle}' be inherit from 'ADI::AbstractBundle'.\"\n      end\n\n      ann = resolved_bundle.annotation Athena::DependencyInjection::Bundle\n\n      unless name = ann[0] || ann[\"name\"]\n        bundle.raise \"Unable to determine extension name. It was not provided as the first positional argument nor via the 'name' field.\"\n      end\n    %}\n\n    ADI.register_extension {{name}}, {{\"#{bundle.resolve.id}::Schema\".id}}\n    ADI.add_compiler_pass {{\"#{bundle.resolve.id}::Extension\".id}}, :before_optimization, 1028\n\n    {% for pass in resolved_bundle.constant(\"PASSES\") %}\n      ADI.add_compiler_pass {{pass.splat}}\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/annotation_configurations.cr",
    "content": "module Athena::DependencyInjection\n  # :nodoc:\n  CUSTOM_ANNOTATIONS = [] of Nil\n\n  # Registers a configuration annotation with the provided *name*.\n  # Defines a configuration record with the provided *args*, if any, that represents the possible arguments that the annotation accepts.\n  # May also be used with a block to add custom methods to the configuration record.\n  #\n  # ### Example\n  #\n  # ```\n  # # Defines an annotation without any arguments.\n  # ADI.configuration_annotation Secure\n  #\n  # # Defines annotation with a required and optional argument.\n  # # The default value will be used if that key isn't supplied in the annotation.\n  # ADI.configuration_annotation SomeAnn, id : Int32, debug : Bool = true\n  #\n  # # A block can be used to define custom methods on the configuration object.\n  # ADI.configuration_annotation CustomAnn, first_name : String, last_name : String do\n  #   def name : String\n  #     \"#{@first_name} #{@last_name}\"\n  #   end\n  # end\n  # ```\n  #\n  # NOTE: The logic to actually do the resolution of the annotations must be handled in the owning shard.\n  # `Athena::DependencyInjection` only defines the common logic that each implementation can use.\n  # See `ADI::AnnotationConfigurations` for more information.\n  macro configuration_annotation(name, *args, &)\n    annotation {{name.id}}; end\n\n    # :nodoc:\n    record {{name.id}}Configuration < ADI::AnnotationConfigurations::ConfigurationBase{% unless args.empty? %}, {{args.splat}}{% end %} do\n      {{yield}}\n    end\n\n    {% CUSTOM_ANNOTATIONS << name %}\n  end\n\n  # Wraps a hash of configuration annotations applied to a given type, method, or instance variable.\n  # Provides the logic to access each annotation's configuration in a type safe manner.\n  #\n  # Implementations using this type must define the logic to provide the annotation hash manually;\n  # this would most likely just be something like:\n  #\n  # ```\n  # # Define a hash to store the configurations.\n  # {% custom_configurations = {} of Nil => Nil %}\n  #\n  # # Iterate over the stored annotation classes.\n  # {% for ann_class in ADI::CUSTOM_ANNOTATIONS %}\n  #    {% ann_class = ann_class.resolve %}\n  #\n  #    # Define an array to store the annotation configurations of this type.\n  #    {% annotations = [] of Nil %}\n  #\n  #    # Iterate over each annotation of this type on the given type, method, or instance variable.\n  #    {% for ann in type_method_instance_variable.annotations ann_class %}\n  #      # Add a new instance of the annotations configuration to the array.\n  #      # Add the annotation's positional arguments first, if any, then named arguments.\n  #      {% annotations << \"#{ann_class}Configuration.new(#{ann.args.empty? ? \"\".id : \"#{ann.args.splat},\".id}#{ann.named_args.double_splat})\".id %}\n  #    {% end %}\n  #\n  #    # Update the configuration hash with the annotation class and configuration objects, but only if there was at least one.\n  #    {% custom_configurations[ann_class] = \"(#{annotations} of ADI::AnnotationConfigurations::ConfigurationBase)\".id unless annotations.empty? %}\n  #  {% end %}\n  #\n  # # ...\n  #\n  # # Use the built hash to instantiate a new `ADI::AnnotationConfigurations` instance.\n  # ADI::AnnotationConfigurations.new({{custom_configurations}} of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase)),\n  # ```\n  #\n  # TODO: Centralize the hash resolution logic once [this issue](https://github.com/crystal-lang/crystal/issues/8835) is resolved.\n  struct AnnotationConfigurations\n    # :inherit:\n    def inspect(io : IO) : Nil\n      io << \"#<ADI::AnnotationConfigurations>\"\n    end\n\n    # Base type of annotation configuration objects registered via `Athena::DependencyInjection.configuration_annotation`.\n    abstract struct ConfigurationBase; end\n\n    # :nodoc:\n    #\n    # Used to type the `#annotation_hash` when there are no user defined annotations.\n    annotation Placeholder; end\n\n    macro finished\n      # A union representing the possible annotation classes that could be applied to a type, method, or instance variable.\n      alias Classes = {{%(Union(#{ADI::CUSTOM_ANNOTATIONS.empty? ? \"Placeholder.class\".id : ADI::CUSTOM_ANNOTATIONS.map { |t| \"#{t}.class\".id }.splat})).id}}\n\n      # The Hash type that will store the annotation configurations.\n      alias AnnotationHash = Hash(ADI::AnnotationConfigurations::Classes, Array(ADI::AnnotationConfigurations::ConfigurationBase))\n\n      def initialize(@annotation_hash : AnnotationHash = AnnotationHash.new); end\n\n      {% for ann_class in ADI::CUSTOM_ANNOTATIONS %}\n        # Returns the `{{ann_class}}` configuration instance for the provided *ann_class* at the provided *index*.\n        #\n        # Returns the last configuration instance by default.\n        def [](ann_class : {{ann_class}}.class, index : Int32 = -1) : {{ann_class}}Configuration\n          self.[]?(ann_class, index) || raise KeyError.new \"No annotations of type '#{ann_class}' were found.\"\n        end\n\n        # Returns the `{{ann_class}}` configuration instance for the provided *ann_class* at the provided *index*,\n        # or `nil` if no annotations of that type were found.\n        #\n        # Returns the last configuration instance by default.\n        def []?(ann_class : {{ann_class}}.class, index : Int32 = -1) : {{ann_class}}Configuration?\n          @annotation_hash[ann_class]?.try(&.[index]).as {{ann_class}}Configuration?\n        end\n\n        # Returns an array of `{{ann_class}}` configuration instances for the provided *ann_class*.\n        def fetch_all(ann_class : {{ann_class}}.class) : Array(ADI::AnnotationConfigurations::ConfigurationBase)\n          @annotation_hash[ann_class]? || Array(ADI::AnnotationConfigurations::ConfigurationBase).new\n        end\n      {% end %}\n\n      # Returns `true` if there are annotations of the provided *ann_class*, otherwise `false`.\n      def has?(ann_class : ADI::AnnotationConfigurations::Classes) : Bool\n        @annotation_hash.has_key? ann_class\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/annotations.cr",
    "content": "module Athena::DependencyInjection\n  # Allows defining an alternative name to identify a service.\n  # This helps solve two primary use cases:\n  #\n  # 1. Defining a default service to use when a parameter is typed as an interface\n  # 1. Decoupling a service from its ID to more easily allow customizing it.\n  #\n  # ### Default Service\n  #\n  # This annotation may be applied to a service that includes one or more interface(s).\n  # The annotation can then be provided the interface to alias as the first positional argument.\n  # If the service only includes one interface (module ending with `Interface`), the annotation argument can be omitted.\n  # Multiple annotations may be applied if it includes more than one.\n  #\n  # ```\n  # module SomeInterface; end\n  #\n  # module OtherInterface; end\n  #\n  # module BlahInterface; end\n  #\n  # # `Foo` is implicitly aliased to `SomeInterface` since it only includes the one.\n  # @[ADI::Register]\n  # @[ADI::AsAlias] # SomeInterface is assumed\n  # class Foo\n  #   include SomeInterface\n  # end\n  #\n  # # Alias `Bar` to both included interfaces.\n  # @[ADI::Register]\n  # @[ADI::AsAlias(BlahInterface)]\n  # @[ADI::AsAlias(OtherInterface)]\n  # class Bar\n  #   include BlahInterface\n  #   include OtherInterface\n  # end\n  # ```\n  #\n  # In this example, anytime a parameter restriction for `SomeInterface` is encountered, `Foo` will be injected.\n  # Similarly, anytime a parameter restriction of `BlahInterface` or `OtherInterface` is encountered, `Bar` will be injected.\n  # This can be especially useful for when you want to define a default service to use when there are multiple implementations of an interface.\n  #\n  # ### String Keys\n  #\n  # The use case for string keys is you can do something like this:\n  #\n  # ```\n  # @[ADI::Register(name: \"default_service\")]\n  # @[ADI::AsAlias(\"my_service\")]\n  # class SomeService\n  # end\n  # ```\n  # The idea being, have a service with an internal `default_service` id, but alias it to a more general `my_service` id.\n  # Dependencies could then be wired up to depend upon the `\"@my_service\"` implementation.\n  # This enabled the user/other logic to override the `my_service` alias to their own implementation (assuming it implements same API/interface(s)).\n  # This should allow everything to propagate and use the custom type without having to touch the original `default_service`.\n  #\n  # ### Named Aliases\n  #\n  # When multiple implementations of an interface need to be injected into the same service,\n  # the `name` parameter specifies which constructor parameter should receive which implementation.\n  # The name matches the constructor parameter name.\n  #\n  # ```\n  # module LoggerInterface; end\n  #\n  # @[ADI::Register]\n  # @[ADI::AsAlias(LoggerInterface, name: \"file_logger\")]\n  # class FileLogger\n  #   include LoggerInterface\n  # end\n  #\n  # @[ADI::Register]\n  # @[ADI::AsAlias(LoggerInterface, name: \"console_logger\")]\n  # class ConsoleLogger\n  #   include LoggerInterface\n  # end\n  #\n  # @[ADI::Register(public: true)]\n  # class MyService\n  #   # file_logger -> FileLogger, console_logger -> ConsoleLogger\n  #   def initialize(@file_logger : LoggerInterface, @console_logger : LoggerInterface)\n  #   end\n  # end\n  # ```\n  #\n  # Named aliases take precedence over type-only aliases. A type-only alias can still be defined\n  # as a fallback for parameters whose names don't match any named alias.\n  #\n  # NOTE: Named aliases cannot be accessed directly via `container.get(Interface)`.\n  # Only type-only aliases support the `public` parameter for direct container access.\n  annotation AsAlias; end\n\n  # Applies the provided configuration to any registered service of the type the annotation is applied to.\n  # E.g. a module interface, or a parent type.\n  #\n  # The following values may be auto-configured:\n  #\n  # * `tags : Array(String | NamedTuple(name: String, priority: Int32?))` - The [tags](/DependencyInjection/Register/#Athena::DependencyInjection::Register--tagging-services) to apply.\n  # * `calls : Array(Tuple(String, Tuple(T)))` - [Service calls](/DependencyInjection/Register/#Athena::DependencyInjection::Register--service-calls) that should be made on the service after its instantiated.\n  # * `bind : NamedTuple(*)` - A named tuple of values that should be available to the constructors\n  # * `public : Bool` - If the services should be accessible directly from the container\n  # * `constructor : String` - Name of a class method to use as the [service factory](/DependencyInjection/Register/#Athena::DependencyInjection::Register--factories)\n  #\n  # TIP: Checkout `ADI::AutoconfigureTag` and `ADI::TaggedIterator` for a simpler way of handling tags.\n  #\n  # ### Example\n  #\n  # ```\n  # @[ADI::Autoconfigure(bind: {id: 123}, public: true)]\n  # module SomeInterface; end\n  #\n  # @[ADI::Register]\n  # record One do\n  #   include SomeInterface\n  # end\n  #\n  # @[ADI::Register]\n  # record Two, id : Int32 do\n  #   include SomeInterface\n  # end\n  #\n  # # The services are only accessible like this since they were auto-configured to be public.\n  # ADI.container.one # => One()\n  #\n  # # `123` is used as it was bound to all services that include `SomeInterface`.\n  # ADI.container.two # => Two(@id=123)\n  # ```\n  annotation Autoconfigure; end\n\n  # Similar to `ADI::Autoconfigure` but specialized for easily configuring [tags](/DependencyInjection/Register/#Athena::DependencyInjection::Register--tagging-services).\n  # Accepts an optional tag name as the first positional parameter, otherwise defaults to the FQN of the type.\n  # Named arguments may also be provided that'll be added to the tag as attributes.\n  #\n  # TIP: This type is best used in conjunction with `ADI::TaggedIterator`.\n  #\n  # ### Example\n  #\n  # ```\n  # # All services including `SomeInterface` will be tagged with `\"some-tag\"`.\n  # @[ADI::AutoconfigureTag(\"some-tag\")]\n  # module SomeInterface; end\n  #\n  # # All services including `OtherInterface` will be tagged with `\"OtherInterface\"`.\n  # @[ADI::AutoconfigureTag]\n  # module OtherInterface; end\n  # ```\n  annotation AutoconfigureTag; end\n\n  # Can be applied to a collection parameter to provide all the services with a specific tag.\n  # Supported collection types include: `Indexable`, `Enumerable`, and `Iterator`.\n  # Accepts an optional tag name as the first positional parameter, otherwise defaults to the FQN of the type within the collection type's generic.\n  #\n  # TIP: This type is best used in conjunction with `ADI::AutoconfigureTag`.\n  #\n  # The provided type lazily initializes the provided services as they are accessed.\n  #\n  # ### Example\n  #\n  # ```\n  # @[ADI::Register]\n  # class Foo\n  #   # Inject all services tagged with `\"some-tag\"`.\n  #   def initialize(@[ADI::TaggedIterator(\"some-tag\")] @services : Enumerable(SomeInterface)); end\n  # end\n  #\n  # @[ADI::Register]\n  # class Bar\n  #   # Inject all services tagged with `\"SomeInterface\"`.\n  #   def initialize(@[ADI::TaggedIterator] @services : Enumerable(SomeInterface)); end\n  # end\n  # ```\n  annotation TaggedIterator; end\n\n  # Automatically registers a service based on the type the annotation is applied to.\n  #\n  # The type of the service affects how it behaves within the container.  When a `struct` service is retrieved or injected into a type, it will be a copy of the one in the SC (passed by value).\n  # This means that changes made to it in one type, will _NOT_ be reflected in other types.  A `class` service on the other hand will be a reference to the one in the SC.  This allows it\n  # to share state between services.\n  #\n  # ## Optional Arguments\n  #\n  # In most cases, the annotation can be applied without additional arguments.  However, the annotation accepts a handful of optional arguments to fine tune how the service is registered.\n  #\n  # * `name : String`- The name of the service.  Should be unique.  Defaults to the type's FQN snake cased.\n  # * `factory : String | Tuple(T, String)` - Use a factory type/method to create the service.  See the [Factories](#factories) section.\n  # * `public : Bool` - If the service should be directly accessible from the container.  Defaults to `false`.\n  # * `alias : Array(T)` - Injects `self` when any of these types are used as a type restriction.  See the Aliasing Services example for more information.\n  # * `tags : Array(String | NamedTuple(name: String, priority: Int32?))` - Tags that should be assigned to the service.  Defaults to an empty array.  See the [Tagging Services][Athena::DependencyInjection::Register--tagging-services] example for more information.\n  # * `calls : Array(Tuple(String, Tuple(T)))` - Calls that should be made on the service after its instantiated.\n  #\n  # ## Examples\n  #\n  # ### Basic Usage\n  #\n  # The simplest usage involves only applying the `ADI::Register` annotation to a type.  If the type does not have any arguments, then it is simply registered as a service as is.  If the type _does_ have arguments, then an attempt is made to register the service by automatically resolving dependencies based on type restrictions.\n  #\n  # ```\n  # @[ADI::Register]\n  # # Register a service without any dependencies.\n  # struct ShoutTransformer\n  #   def transform(value : String) : String\n  #     value.upcase\n  #   end\n  # end\n  #\n  # @[ADI::Register(public: true)]\n  # # The ShoutTransformer is injected based on the type restriction of the `transformer` argument.\n  # struct SomeAPIClient\n  #   def initialize(@transformer : ShoutTransformer); end\n  #\n  #   def send(message : String)\n  #     message = @transformer.transform message\n  #\n  #     # ...\n  #   end\n  # end\n  #\n  # ADI.container.some_api_client.send \"foo\" # => FOO\n  # ```\n  #\n  # ### Aliasing Services\n  #\n  # An important part of DI is building against interfaces as opposed to concrete types.\n  # This allows a type to depend upon abstractions rather than a specific implementation of the interface.\n  # Or in other words, prevents a singular implementation from being tightly coupled with another type.\n  #\n  # The `ADI::AsAlias` annotation can be used to define a default implementation for an interface.\n  # Checkout the annotation's docs for more information.\n  #\n  # ### Scalar Arguments\n  #\n  # The auto registration logic as shown in previous examples only works on service dependencies.  Scalar arguments, such as Arrays, Strings, NamedTuples, etc, must be defined manually.\n  # This is achieved by using the argument's name prefixed with a `_` symbol as named arguments within the annotation.\n  #\n  # ```\n  # @[ADI::Register(_shell: ENV[\"SHELL\"], _config: {id: 12_i64, active: true}, public: true)]\n  # struct ScalarClient\n  #   def initialize(@shell : String, @config : NamedTuple(id: Int64, active: Bool)); end\n  # end\n  #\n  # ADI.container.scalar_client # => ScalarClient(@config={id: 12, active: true}, @shell=\"/bin/bash\")\n  # ```\n  # Arrays can also include references to services by prefixing the name of the service with an `@` symbol.\n  #\n  # ```\n  # module Interface; end\n  #\n  # @[ADI::Register]\n  # struct One\n  #   include Interface\n  # end\n  #\n  # @[ADI::Register]\n  # struct Two\n  #   include Interface\n  # end\n  #\n  # @[ADI::Register]\n  # struct Three\n  #   include Interface\n  # end\n  #\n  # @[ADI::Register(_services: [\"@one\", \"@three\"], public: true)]\n  # struct ArrayClient\n  #   def initialize(@services : Array(Interface)); end\n  # end\n  #\n  # ADI.container.array_client # => ArrayClient(@services=[One(), Three()])\n  # ```\n  #\n  # While scalar arguments cannot be auto registered by default, the `Athena::DependencyInjection.bind` macro can be used to support it.  For example: `ADI.bind shell, \"bash\"`.\n  # This would now inject the string `\"bash\"` whenever an argument named `shell` is encountered.\n  #\n  # ### Tagging Services\n  #\n  # Services can also be tagged.\n  # Service tags allows another service to have all services with a specific tag injected as a dependency.\n  # A tag consists of a name, and additional metadata related to the tag.\n  #\n  # TIP: Checkout `ADI::AutoconfigureTag` for an easy way to tag services.\n  #\n  # ```\n  # PARTNER_TAG = \"partner\"\n  #\n  # @[ADI::Register(_id: 1, name: \"google\", tags: [{name: PARTNER_TAG, priority: 5}])]\n  # @[ADI::Register(_id: 2, name: \"facebook\", tags: [PARTNER_TAG])]\n  # @[ADI::Register(_id: 3, name: \"yahoo\", tags: [{name: \"partner\", priority: 10}])]\n  # @[ADI::Register(_id: 4, name: \"microsoft\", tags: [PARTNER_TAG])]\n  # # Register multiple services based on the same type.  Each service must give define a unique name.\n  # record FeedPartner, id : Int32\n  #\n  # @[ADI::Register(public: true)]\n  # class PartnerClient\n  #   getter services : Enumerable(FeedPartner)\n  #\n  #   def initialize(@[ADI::TaggedIterator(PARTNER_TAG)] @services : Enumerable(FeedPartner)); end\n  # end\n  #\n  # ADI.container.partner_client.services.to_a # =>\n  # # [FeedPartner(@id=3),\n  # #  FeedPartner(@id=1),\n  # #  FeedPartner(@id=2),\n  # #  FeedPartner(@id=4)]\n  # ```\n  #\n  # The `ADI::TaggedIterator` annotation provides an easy way to inject services with a specific tag to a specific parameter.\n  #\n  # ### Service Calls\n  #\n  # Service calls can be defined that will call a specific method on the service, with a set of arguments.\n  # Use cases for this are generally not all that common, but can sometimes be useful.\n  #\n  # ```\n  # @[ADI::Register(public: true, calls: [\n  #   {\"foo\"},\n  #   {\"foo\", {3}},\n  #   {\"foo\", {6}},\n  # ])]\n  # class CallClient\n  #   getter values = [] of Int32\n  #\n  #   def foo(value : Int32 = 1)\n  #     @values << value\n  #   end\n  # end\n  #\n  # ADI.container.call_client.values # => [1, 3, 6]\n  # ```\n  #\n  # ### Service Proxies\n  #\n  # In some cases, it may be a bit \"heavy\" to instantiate a service that may only be used occasionally.\n  # To solve this, a proxy of the service could be injected instead.\n  # The instantiation of proxied services are deferred until a method is called on it.\n  #\n  # A service is proxied by changing the type signature of the service to be of the `ADI::Proxy(T)` type, where `T` is the service to be proxied.\n  #\n  # ```\n  # @[ADI::Register]\n  # class ServiceTwo\n  #   getter value = 123\n  #\n  #   def initialize\n  #     pp \"new s2\"\n  #   end\n  # end\n  #\n  # @[ADI::Register(public: true)]\n  # class ServiceOne\n  #   getter service_two : ADI::Proxy(ServiceTwo)\n  #\n  #   # Tells `ADI` that a proxy of `ServiceTwo` should be injected.\n  #   def initialize(@service_two : ADI::Proxy(ServiceTwo))\n  #     pp \"new s1\"\n  #   end\n  #\n  #   def run\n  #     # At this point service_two hasn't been initialized yet.\n  #     pp \"before value\"\n  #\n  #     # First method interaction with the proxy instantiates the service and forwards the method to it.\n  #     pp @service_two.value\n  #   end\n  # end\n  #\n  # ADI.container.service_one.run\n  # # \"new s1\"\n  # # \"before value\"\n  # # \"new s2\"\n  # # 123\n  # ```\n  #\n  # #### Tagged Services Proxies\n  #\n  # Tagged services may also be injected as an array of proxy objects.\n  # This can be useful as an easy way to manage a collection of services where only one (or a small amount) will be used at a time.\n  #\n  # ```\n  # @[ADI::Register(_services: \"!some_tag\")]\n  # class SomeService\n  #   def initialize(@services : Array(ADI::Proxy(ServiceType)))\n  #   end\n  # end\n  # ```\n  #\n  # #### Proxy Metadata\n  #\n  # The `ADI::Proxy` object also exposes some metadata related to the proxied object; such as its name, type, and if it has been instantiated yet.\n  #\n  # For example, using `ServiceTwo`:\n  #\n  # ```\n  # # Assume this returns a `ADI::Proxy(ServiceTwo)`.\n  # proxy = ADI.container.service_two\n  #\n  # proxy.service_id    # => \"service_two\"\n  # proxy.service_type  # => ServiceTwo\n  # proxy.instantiated? # => false\n  # proxy.value         # => 123\n  # proxy.instantiated? # => true\n  # ```\n  #\n  # ### Parameters\n  #\n  # Reusable configuration [parameters](/getting_started/configuration#parameters) can be injected directly into services using the same syntax as when used within `ADI.configure`.\n  # Parameters may be supplied either via `Athena::DependencyInjection.bind` or an explicit service argument.\n  #\n  # ```\n  # ADI.configure({\n  #   parameters: {\n  #     \"app.name\":              \"My App\",\n  #     \"app.database.username\": \"administrator\",\n  #   },\n  # })\n  #\n  # ADI.bind db_username, \"%app.database.username%\"\n  #\n  # @[ADI::Register(_app_name: \"%app.name%\", public: true)]\n  # record SomeService, app_name : String, db_username : String\n  #\n  # service = ADI.container.some_service\n  # service.app_name    # => \"My App\"\n  # service.db_username # => \"USERNAME\"\n  # ```\n  #\n  # ### Optional Services\n  #\n  # Services defined with a nillable type restriction are considered to be optional.  If no service could be resolved from the type, then `nil` is injected instead.\n  # Similarly, if the argument has a default value, that value would be used instead.\n  #\n  # ```\n  # struct OptionalMissingService\n  # end\n  #\n  # @[ADI::Register]\n  # struct OptionalExistingService\n  # end\n  #\n  # @[ADI::Register(public: true)]\n  # class OptionalClient\n  #   getter service_missing, service_existing, service_default\n  #\n  #   def initialize(\n  #     @service_missing : OptionalMissingService?,\n  #     @service_existing : OptionalExistingService?,\n  #     @service_default : OptionalMissingService | Int32 | Nil = 12,\n  #   ); end\n  # end\n  #\n  # ADI.container.optional_client\n  # # #<OptionalClient:0x7fe7de7cdf40\n  # #  @service_default=12,\n  # #  @service_existing=OptionalExistingService(),\n  # #  @service_missing=nil>\n  # ```\n  #\n  # ### Generic Services\n  #\n  # Generic arguments can be provided as positional arguments within the `ADI::Register` annotation.\n  #\n  # !!!note\n  #     Services based on generic types _MUST_ explicitly provide a name via the `name` field within the `ADI::Register` annotation\n  #     since there wouldn't be a way to tell them apart from the class name alone.\n  #\n  # ```\n  # @[ADI::Register(Int32, Bool, name: \"int_service\", public: true)]\n  # @[ADI::Register(Float64, Bool, name: \"float_service\", public: true)]\n  # struct GenericService(T, B)\n  #   def type\n  #     {T, B}\n  #   end\n  # end\n  #\n  # ADI.container.int_service.type   # => {Int32, Bool}\n  # ADI.container.float_service.type # => {Float64, Bool}\n  # ```\n  #\n  # ### Factories\n  #\n  # In some cases it may be necessary to use the [factory design pattern](https://en.wikipedia.org/wiki/Factory_%28object-oriented_programming%29)\n  # to handle creating an object as opposed to creating the object directly.  In this case the `factory` argument can be used.\n  #\n  # Factory methods are class methods defined on some type; either the service itself or a different type.\n  # Arguments to the factory method are provided as they would if the service was being created directly.\n  # This includes auto resolved service dependencies, and scalar underscore based arguments included within the `ADI::Register` annotation.\n  #\n  # #### Same Type\n  #\n  # A `String` `factory` value denotes the method name that should be called on the service itself to create the service.\n  #\n  # ```\n  # # Calls `StringFactoryService.double` to create the service.\n  # @[ADI::Register(_value: 10, public: true, factory: \"double\")]\n  # class StringFactoryService\n  #   getter value : Int32\n  #\n  #   def self.double(value : Int32) : self\n  #     new value * 2\n  #   end\n  #\n  #   def initialize(@value : Int32); end\n  # end\n  #\n  # ADI.container.string_factory_service.value # => 20\n  # ```\n  #\n  # Using the `ADI::Inject` annotation on a class method is equivalent to providing that method's name as the `factory` value.\n  # For example, this is the same as the previous example:\n  #\n  # ```\n  # @[ADI::Register(_value: 10, public: true)]\n  # class StringFactoryService\n  #   getter value : Int32\n  #\n  #   @[ADI::Inject]\n  #   def self.double(value : Int32) : self\n  #     new value * 2\n  #   end\n  #\n  #   def initialize(@value : Int32); end\n  # end\n  #\n  # ADI.container.string_factory_service.value # => 20\n  # ```\n  #\n  # #### Different Type\n  #\n  # A `Tuple` can also be provided as the `factory` value to allow using an external type's factory method to create the service.\n  # The first item represents the factory type to use, and the second item represents the method that should be called.\n  #\n  # ```\n  # class TestFactory\n  #   def self.create_tuple_service(value : Int32) : TupleFactoryService\n  #     TupleFactoryService.new value * 3\n  #   end\n  # end\n  #\n  # # Calls `TestFactory.create_tuple_service` to create the service.\n  # @[ADI::Register(_value: 10, public: true, factory: {TestFactory, \"create_tuple_service\"})]\n  # class TupleFactoryService\n  #   getter value : Int32\n  #\n  #   def initialize(@value : Int32); end\n  # end\n  #\n  # ADI.container.tuple_factory_service.value # => 30\n  # ```\n  annotation Register; end\n\n  # Specifies which constructor should be used for injection.\n  #\n  # ```\n  # @[ADI::Register(_value: 2, public: true)]\n  # class SomeService\n  #   @active : Bool = false\n  #\n  #   def initialize(value : String, @active : Bool)\n  #     @value = value.to_i\n  #   end\n  #\n  #   @[ADI::Inject]\n  #   def initialize(@value : Int32); end\n  # end\n  #\n  # ADI.container.some_service # => #<SomeService:0x7f51a77b1eb0 @active=false, @value=2>\n  # SomeService.new \"1\", true  # => #<SomeService:0x7f51a77b1e90 @active=true, @value=1>\n  # ```\n  #\n  # Without the `ADI::Inject` annotation, the first initializer would be used, which would fail since we are not providing a value for the `active` argument.\n  # `ADI::Inject` allows telling the service container that it should use the second constructor when registering this service.  This allows a constructor overload\n  # specific to DI to be used while still allowing the type to be used outside of DI via other constructors.\n  #\n  # Using the `ADI::Inject` annotation on a class method also acts a shortcut for defining a service [factory][Athena::DependencyInjection::Register--factories].\n  annotation Inject; end\n\n  # :nodoc:\n  annotation Bundle; end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/athena-dependency_injection.cr",
    "content": "require \"./abstract_bundle\"\nrequire \"./annotations\"\nrequire \"./annotation_configurations\"\nrequire \"./extension\"\nrequire \"./proxy\"\nrequire \"./service_container\"\n\n# :nodoc:\nclass Fiber\n  property container : ADI::ServiceContainer { ADI::ServiceContainer.new }\nend\n\n# Convenience alias to make referencing `Athena::DependencyInjection` types easier.\nalias ADI = Athena::DependencyInjection\n\n# Robust dependency injection service container framework.\nmodule Athena::DependencyInjection\n  VERSION = \"0.4.5\"\n\n  private BINDINGS            = {} of Nil => Nil\n  private AUTO_CONFIGURATIONS = {} of Nil => Nil\n  private EXTENSIONS          = {} of Nil => Nil\n\n  # :nodoc:\n  CONFIG = {parameters: {__nil: nil}} # Ensure this type is a NamedTupleLiteral\n\n  private CONFIGS = [] of Nil\n\n  # Allows binding a *value* to a *key* in order to enable auto registration of that value.\n  #\n  # Bindings allow scalar values, or those that could not otherwise be handled via [service aliases][Athena::DependencyInjection::Register--aliasing-services], to be auto registered.\n  # This allows those arguments to be defined once and reused, as opposed to using named arguments to manually specify them for each service.\n  #\n  # Bindings can also be declared with a type restriction to allow taking the type restriction of the argument into account.\n  # Typed bindings are always checked first as the most specific type is always preferred.\n  # If no typed bindings match the argument's type, then the last defined untyped bindings is used.\n  #\n  # ### Example\n  #\n  # ```\n  # module ValueInterface; end\n  #\n  # @[ADI::Register(_value: 1, name: \"value_one\")]\n  # @[ADI::Register(_value: 2, name: \"value_two\")]\n  # @[ADI::Register(_value: 3, name: \"value_three\")]\n  # record ValueService, value : Int32 do\n  #   include ValueInterface\n  # end\n  #\n  # # Untyped bindings\n  # ADI.bind api_key, ENV[\"API_KEY\"]\n  # ADI.bind config, {id: 12_i64, active: true}\n  # ADI.bind static_value, 123\n  # ADI.bind odd_values, [\"@value_one\", \"@value_three\"]\n  # ADI.bind value_arr, [true, true, false]\n  #\n  # # Typed bindings\n  # ADI.bind value_arr : Array(Int32), [1, 2, 3]\n  # ADI.bind value_arr : Array(Float64), [1.0, 2.0, 3.0]\n  #\n  # @[ADI::Register(public: true)]\n  # record BindingClient,\n  #   api_key : String,\n  #   config : NamedTuple(id: Int64, active: Bool),\n  #   static_value : Int32,\n  #   odd_values : Array(ValueInterface)\n  #\n  # @[ADI::Register(public: true)]\n  # record IntArr, value_arr : Array(Int32)\n  #\n  # @[ADI::Register(public: true)]\n  # record FloatArr, value_arr : Array(Float64)\n  #\n  # @[ADI::Register(public: true)]\n  # record BoolArr, value_arr : Array(Bool)\n  #\n  # ADI.container.binding_client # =>\n  # # BindingClient(\n  # #  @api_key=\"123ABC\",\n  # #  @config={id: 12, active: true},\n  # #  @static_value=123,\n  # #  @odd_values=[ValueService(@value=1), ValueService(@value=3)])\n  #\n  # ADI.container.int_arr   # => IntArr(@value_arr=[1, 2, 3])\n  # ADI.container.float_arr # => FloatArr(@value_arr=[1.0, 2.0, 3.0])\n  # ADI.container.bool_arr  # => BoolArr(@value_arr=[true, true, false])\n  # ```\n  macro bind(key, value)\n    {% BINDINGS[key] = value %}\n  end\n\n  # Returns the `ADI::ServiceContainer` for the current fiber.\n  def self.container : ADI::ServiceContainer\n    Fiber.current.container\n  end\n\n  # Namespace for DI extension related types.\n  module Extension; end\n\n  # Primary entrypoint for configuring `ADI::Extension::Schema`s.\n  macro configure(config)\n    {%\n      CONFIGS << config\n    %}\n  end\n\n  # Adds a compiler *pass*, optionally of a specific *type* and *priority* (default `0`).\n  #\n  # Valid types include:\n  #\n  # * `:before_optimization` (default)\n  # * `:optimization`\n  # * `:before_removing`\n  # * `:after_removing`\n  # * `:removing`\n  #\n  # EXPERIMENTAL: This feature is intended for internal/advanced use and, for now, comes with limited public documentation.\n  macro add_compiler_pass(pass, type = nil, priority = nil)\n    {%\n      pass_type = pass.resolve\n\n      unless pass_type.module?\n        pass.raise \"Pass type must be a module.\"\n      end\n\n      type = type || :before_optimization\n      priority = priority || 0\n\n      if hash = ADI::ServiceContainer::PASS_CONFIG[type]\n        hash[priority] = [] of Nil if hash[priority] == nil\n\n        hash[priority] << pass_type.id\n      else\n        type.raise \"Invalid compiler pass type: '#{type}'.\"\n      end\n    %}\n  end\n\n  # Registers an extension `ADI::Extension::Schema` with the provided *name*.\n  macro register_extension(name, schema)\n    {% ADI::ServiceContainer::EXTENSIONS[name.id.stringify] = schema %}\n  end\n\n  # :nodoc:\n  macro service_iterator(for name, services)\n    {% begin %}\n      private struct {{name.camelcase.id}}(T, S)\n        include Iterator(T)\n        include Indexable(T)\n\n        @offset = 0\n\n        def initialize(@container : ADI::ServiceContainer); end\n\n        def next : T | Iterator::Stop\n          return stop if @offset == S\n\n          self\n            .unsafe_fetch(@offset)\n            .tap { @offset += 1 }\n        end\n\n        def size : Int32\n          S\n        end\n\n        def rewind : Nil\n          @offset = 0\n        end\n\n        # :nodoc:\n        def unsafe_fetch(index : Int) : T\n          case index\n            {% for service_id, idx in services %}\n              when {{idx}} then @container.{{service_id.id}}\\\n            {% end %}\n          else\n            raise \"\"\n          end\n        end\n      end\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/analyze_service_references.cr",
    "content": "# :nodoc:\n#\n# Builds a reference graph tracking which services are used and how many times.\n# Populates SERVICE_REFERENCES with reference counts for use by optimization passes.\nmodule Athena::DependencyInjection::ServiceContainer::AnalyzeServiceReferences\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          __nil = nil\n\n          # Initialize reference counts for all services\n          SERVICE_HASH.each do |service_id, definition|\n            if definition != nil\n              SERVICE_REFERENCES[service_id] = {\n                count:         0,\n                public:        definition[\"public\"] == true,\n                referenced_by: [] of Nil,\n              }\n            end\n          end\n\n          # Analyze references\n          SERVICE_HASH.each do |service_id, definition|\n            if definition != nil\n              # 1. Check parameter values for service references\n              if parameters = definition[\"parameters\"]\n                parameters.each do |_, param|\n                  value = param[\"value\"]\n\n                  # Direct service reference (bare identifier after ResolveValues)\n                  if value && SERVICE_HASH[value.stringify] != nil\n                    ref_id = value.stringify\n                    SERVICE_REFERENCES[ref_id][\"count\"] += 1\n                    SERVICE_REFERENCES[ref_id][\"referenced_by\"] << service_id\n                  end\n\n                  # Array of service references\n                  if value.is_a?(ArrayLiteral)\n                    value.each do |v|\n                      if SERVICE_HASH[v.stringify] != nil\n                        ref_id = v.stringify\n                        SERVICE_REFERENCES[ref_id][\"count\"] += 1\n                        SERVICE_REFERENCES[ref_id][\"referenced_by\"] << service_id\n                      end\n                    end\n                  end\n                end\n              end\n\n              # 2. Check calls array for service references\n              if calls = definition[\"calls\"]\n                calls.each do |call|\n                  method, args = call\n                  if args\n                    args.each do |arg|\n                      if SERVICE_HASH[arg.stringify] != nil\n                        ref_id = arg.stringify\n                        SERVICE_REFERENCES[ref_id][\"count\"] += 1\n                        SERVICE_REFERENCES[ref_id][\"referenced_by\"] << service_id\n                      end\n                    end\n                  end\n                end\n              end\n\n              # 3. Check explicit referenced_services metadata\n              if referenced_services = definition[\"referenced_services\"]\n                referenced_services.each do |ref_id|\n                  ref_id_str = ref_id.id.stringify\n                  if SERVICE_HASH[ref_id_str] != nil\n                    SERVICE_REFERENCES[ref_id_str][\"count\"] += 1\n                    SERVICE_REFERENCES[ref_id_str][\"referenced_by\"] << service_id\n                  end\n                end\n              end\n            end\n          end\n\n          # 4. Count public aliases as references to their target services\n          # Only type-only aliases (name is nil) can be public\n          ALIASES.each do |alias_name, alias_entries|\n            type_only_alias = alias_entries.find(&.[\"name\"].nil?)\n            if type_only_alias && type_only_alias[\"public\"] == true\n              target_id = type_only_alias[\"id\"].id.stringify\n              SERVICE_REFERENCES.keys.each do |key|\n                if key.id.stringify == target_id\n                  old_info = SERVICE_REFERENCES[key]\n                  SERVICE_REFERENCES[key] = {\n                    count:         old_info[\"count\"] + 1,\n                    public:        old_info[\"public\"],\n                    referenced_by: old_info[\"referenced_by\"] << \"alias:#{alias_name.id}\",\n                  }\n                end\n              end\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/auto_wire.cr",
    "content": "# :nodoc:\nmodule Athena::DependencyInjection::ServiceContainer::AutoWire\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          printed = false\n\n          SERVICE_HASH.each do |_, definition|\n            definition[\"parameters\"].each do |name, param|\n              param_resolved_restriction = param[\"resolved_restriction\"]\n              resolved_services = [] of Nil\n\n              # Gather a list of services that are compatible with the parameter's type restriction.\n              SERVICE_HASH.each do |id, s_metadata|\n                if (type = param_resolved_restriction) &&\n                   (\n                     s_metadata[\"class\"] <= type ||\n                     (type < ADI::Proxy && s_metadata[\"class\"] <= type.type_vars.first.resolve)\n                   )\n                  resolved_services << id\n                end\n              end\n\n              # If only one service was resolved use it, but only if the parameter is typed as a non-module.\n              # This prevents parameters typed as an interface from being resolved if there is only a single implementation.\n              #\n              # These services should be wired up as aliases to prevent errors if/when another implementation is added.\n              resolved_service = if resolved_services.size == 1 && !param_resolved_restriction.module?\n                                   resolved_services[0]\n\n                                   # If there are more than one, try and match the parameter's name to a service ID.\n                                 elsif rs = resolved_services.find(&.==(name.id))\n                                   rs\n\n                                   # Otherwise see if any aliases explicitly match the parameter's type restriction.\n                                 elsif a = ALIASES.keys.find { |k| k == param_resolved_restriction }\n                                   aliases_for_type = ALIASES[a]\n\n                                   # Try named alias first (more specific match by parameter name)\n                                   named_alias = aliases_for_type.find { |entry| entry[\"name\"] && entry[\"name\"].id == name.id }\n\n                                   if named_alias\n                                     named_alias[\"id\"]\n                                   else\n                                     # Fall back to type-only alias\n                                     type_only_alias = aliases_for_type.find(&.[\"name\"].nil?)\n                                     type_only_alias ? type_only_alias[\"id\"] : nil\n                                   end\n                                 end\n\n              if resolved_service\n                if param[\"resolved_restriction\"] < ADI::Proxy\n                  param[\"value\"] = \"ADI::Proxy.new(#{resolved_service}, ->#{resolved_service.id})\".id\n                  # Track proxy references to ensure getters are generated\n                  definition[\"referenced_services\"] << resolved_service\n                else\n                  param[\"value\"] = resolved_service.id\n                end\n              end\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/define_getters.cr",
    "content": "# :nodoc:\nmodule Athena::DependencyInjection::ServiceContainer::DefineGetters\n  macro included\n    macro finished\n      {% verbatim do %}\n        {% for service_id, metadata in SERVICE_HASH %}\n          {% if metadata != nil && !metadata[\"inlined\"] %}\n            # String literal primarily represents internal services created during container construction.\n            {% service_name = metadata[:class].is_a?(StringLiteral) ? metadata[:class].id : metadata[:class].name(generic_args: false) %}\n            {% generics_type = \"#{service_name}(#{metadata[:generics].splat})\".id %}\n\n            {% service = metadata[:generics].empty? ? metadata[:class].id : generics_type.id %}\n            {% ivar_type = metadata[:generics].empty? ? metadata[:class].id : generics_type.id %}\n            {% fq_prefix = metadata[:class].is_a?(StringLiteral) ? \"\".id : \"::\".id %}\n\n            {% constructor_service = service %}\n            {% constructor_method = \"new\" %}\n\n            {% if factory = metadata[:factory] %}\n              {% constructor_service, constructor_method = factory %}\n            {% end %}\n\n            {%\n              __nil = nil\n\n              # Collect inline setup code from inlined dependencies\n              inline_setups = [] of Nil\n              metadata[\"parameters\"].each do |_, param|\n                value = param[\"value\"]\n\n                # Check direct service reference\n                value_str = value.id.stringify\n                dep = SERVICE_HASH[value_str]\n                if dep && dep[\"inlined\"] && dep[\"inline_setup\"]\n                  inline_setups << dep[\"inline_setup\"]\n                end\n\n                # Check array elements for service references\n                if value.is_a?(ArrayLiteral)\n                  value.each do |v|\n                    v_str = v.id.stringify\n                    dep = SERVICE_HASH[v_str]\n                    if dep && dep[\"inlined\"] && dep[\"inline_setup\"]\n                      inline_setups << dep[\"inline_setup\"]\n                    end\n                  end\n                end\n              end\n\n              # Collect inline setups from call arguments\n              if calls = metadata[\"calls\"]\n                calls.each do |call|\n                  method, args = call\n                  if args\n                    args.each do |arg|\n                      arg_dep = SERVICE_HASH[arg.stringify]\n                      if arg_dep && arg_dep[\"inlined\"] && arg_dep[\"inline_setup\"]\n                        inline_setups << arg_dep[\"inline_setup\"]\n                      end\n                    end\n                  end\n                end\n              end\n            %}\n\n            {% if !metadata[:public] %}protected {% end %}getter {{service_id.id}} : {{fq_prefix}}{{ivar_type}} do\n              {% for setup in inline_setups %}\n                {{setup.id}}\n              {% end %}\n\n              instance = {{fq_prefix}}{{constructor_service}}.{{constructor_method.id}}({{\n                                                                                          metadata[\"parameters\"].map do |name, param|\n                                                                                            value = param[\"value\"]\n                                                                                            value_str = value.id.stringify\n\n                                                                                            # Check if this parameter references an inlined service\n                                                                                            dep = SERVICE_HASH[value_str]\n                                                                                            if dep && dep[\"inlined\"] && dep[\"inline_var\"]\n                                                                                              \"#{name.id}: #{dep[\"inline_var\"].id}\".id\n                                                                                            elsif value.is_a?(ArrayLiteral)\n                                                                                              # Handle array with potential inlined services\n                                                                                              elements = value.map do |v|\n                                                                                                v_str = v.id.stringify\n                                                                                                v_dep = SERVICE_HASH[v_str]\n                                                                                                if v_dep && v_dep[\"inlined\"] && v_dep[\"inline_var\"]\n                                                                                                  v_dep[\"inline_var\"].id\n                                                                                                else\n                                                                                                  v\n                                                                                                end\n                                                                                              end\n                                                                                              str = \"#{name.id}: [#{elements.splat}]\"\n                                                                                              if (resolved_restriction = param[\"resolved_restriction\"]) && resolved_restriction <= Array && (value.of.is_a?(Nop) || elements.empty?)\n                                                                                                str += \" of Union(#{resolved_restriction.type_vars.splat})\"\n                                                                                              end\n                                                                                              str.id\n                                                                                            else\n                                                                                              \"#{name.id}: #{value}\".id\n                                                                                            end\n                                                                                          end.splat\n                                                                                        }})\n\n              {% for call in metadata[:calls] %}\n                {% method, args = call %}\n                {%\n                  transformed_args = args.map do |arg|\n                    arg_dep = SERVICE_HASH[arg.stringify]\n                    if arg_dep && arg_dep[\"inlined\"] && arg_dep[\"inline_var\"]\n                      arg_dep[\"inline_var\"].id\n                    else\n                      arg\n                    end\n                  end\n                %}\n                instance.{{method.id}}({{transformed_args.splat}})\n              {% end %}\n\n              instance\n            end\n\n            {% if metadata[:public] %}\n              def get(service : {{fq_prefix}}{{service}}.class) : {{fq_prefix}}{{service.id}}\n                {{service_id.id}}\n              end\n            {% end %}\n          {% end %}\n        {% end %}\n\n        {% for alias_name, alias_entries in ALIASES %}\n          # Find type-only alias (name is nil) for public access\n          {% type_only_alias = alias_entries.find(&.[\"name\"].nil?) %}\n          {% if type_only_alias && type_only_alias[\"public\"] %}\n            # String alias maps to a service => service alias so we just need a method with the alias' name.\n            {% if alias_name.is_a?(StringLiteral) %}\n              {% aliased_class = SERVICE_HASH[type_only_alias[\"id\"]][\"class\"] %}\n              {% alias_fq_prefix = aliased_class.is_a?(StringLiteral) ? \"\".id : \"::\".id %}\n              def {{alias_name.id}} : {{alias_fq_prefix}}{{aliased_class.id}}\n                {{type_only_alias[\"id\"].id}}\n              end\n            # TypeNode alias maps to an interface => service alias, so we need an override of `#get` pinned to the interface type.\n            {% else %}\n              def get(service : {{alias_name.id}}.class) : {{alias_name.id}}\n                {{type_only_alias[\"id\"].id}}\n              end\n            {% end %}\n          {% end %}\n        {% end %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/inline_service_definitions.cr",
    "content": "# :nodoc:\n#\n# Marks private single-use services for inlining into their consumers.\n# Precomputes inline setup code and variable names which DefineGetters uses.\n# Supports nested inlining where service A depends on service B, both single-use.\nmodule Athena::DependencyInjection::ServiceContainer::InlineServiceDefinitions\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          __nil = nil\n\n          # Build set of services that are targets of public aliases\n          # Only type-only aliases (name is nil) can be public\n          alias_targets = {} of Nil => Nil\n          ALIASES.each do |alias_name, alias_entries|\n            type_only_alias = alias_entries.find(&.[\"name\"].nil?)\n            if type_only_alias && type_only_alias[\"public\"] == true\n              alias_targets[type_only_alias[\"id\"].id.stringify] = true\n            end\n          end\n\n          # Build set of services that are referenced via Proxy (need getters for proc references)\n          proxy_targets = {} of Nil => Nil\n          SERVICE_HASH.each do |_, definition|\n            if definition != nil && definition[\"referenced_services\"]\n              definition[\"referenced_services\"].each do |ref_id|\n                proxy_targets[ref_id.id.stringify] = true\n              end\n            end\n          end\n\n          # First pass: mark ALL single-use private services for inlining\n          SERVICE_REFERENCES.each do |service_id, ref_info|\n            definition = SERVICE_HASH[service_id]\n\n            # Only inline if: not nil, not public, exactly one reference, not a public alias target, AND not a proxy target\n            sid_str = service_id.id.stringify\n            if definition != nil && ref_info[\"public\"] == false && ref_info[\"count\"] == 1 && !alias_targets[sid_str] && !proxy_targets[sid_str]\n              definition[\"inlined\"] = true\n              # Pre-compute the variable name (service_id with dashes replaced)\n              definition[\"inline_var\"] = service_id.gsub(/-/, \"_\")\n            end\n          end\n\n          # Second pass: compute inline setup code in dependency order\n          # Uses a queue pattern - services whose deps aren't ready get pushed back for later processing\n          to_process = [] of Nil\n          SERVICE_HASH.each do |service_id, definition|\n            if definition != nil && definition[\"inlined\"]\n              to_process << service_id\n            end\n          end\n\n          to_process.each do |service_id|\n            definition = SERVICE_HASH[service_id]\n\n            if !definition[\"inline_setup\"]\n              # Check if all inlined dependencies have their setup computed\n              can_compute = true\n              if params = definition[\"parameters\"]\n                params.each do |_, param|\n                  value = param[\"value\"]\n\n                  # Check direct service reference\n                  value_str = value.id.stringify\n                  dep = SERVICE_HASH[value_str]\n                  if dep && dep[\"inlined\"] && !dep[\"inline_setup\"]\n                    can_compute = false\n                  end\n\n                  # Check array elements\n                  if value.is_a?(ArrayLiteral)\n                    value.each do |v|\n                      v_str = v.id.stringify\n                      v_dep = SERVICE_HASH[v_str]\n                      if v_dep && v_dep[\"inlined\"] && !v_dep[\"inline_setup\"]\n                        can_compute = false\n                      end\n                    end\n                  end\n                end\n              end\n\n              # Check call arguments for inlined dependencies\n              if calls = definition[\"calls\"]\n                calls.each do |call|\n                  method, args = call\n                  if args\n                    args.each do |arg|\n                      arg_dep = SERVICE_HASH[arg.stringify]\n                      if arg_dep && arg_dep[\"inlined\"] && !arg_dep[\"inline_setup\"]\n                        can_compute = false\n                      end\n                    end\n                  end\n                end\n              end\n\n              if can_compute\n                service_name = definition[\"class\"].is_a?(StringLiteral) ? definition[\"class\"].id : definition[\"class\"].name(generic_args: false)\n                generics_type = \"#{service_name}(#{definition[\"generics\"].splat})\".id\n                service = definition[\"generics\"].empty? ? definition[\"class\"].id : generics_type.id\n\n                constructor_service = service\n                constructor_method = \"new\"\n\n                if factory = definition[\"factory\"]\n                  constructor_service, constructor_method = factory\n                end\n\n                fq_prefix = definition[\"class\"].is_a?(StringLiteral) ? \"\".id : \"::\".id\n                var_name = definition[\"inline_var\"]\n                setup_lines = [] of Nil\n\n                # First, include setup code from all inlined dependencies in parameters\n                if params = definition[\"parameters\"]\n                  params.each do |_, param|\n                    value = param[\"value\"]\n\n                    # Check direct service reference\n                    value_str = value.id.stringify\n                    dep = SERVICE_HASH[value_str]\n                    if dep && dep[\"inlined\"] && dep[\"inline_setup\"]\n                      setup_lines << dep[\"inline_setup\"]\n                    end\n\n                    # Check array elements\n                    if value.is_a?(ArrayLiteral)\n                      value.each do |v|\n                        v_str = v.id.stringify\n                        v_dep = SERVICE_HASH[v_str]\n                        if v_dep && v_dep[\"inlined\"] && v_dep[\"inline_setup\"]\n                          setup_lines << v_dep[\"inline_setup\"]\n                        end\n                      end\n                    end\n                  end\n                end\n\n                # Include setup code from inlined dependencies in call arguments\n                if calls = definition[\"calls\"]\n                  calls.each do |call|\n                    method, args = call\n                    if args\n                      args.each do |arg|\n                        arg_dep = SERVICE_HASH[arg.stringify]\n                        if arg_dep && arg_dep[\"inlined\"] && arg_dep[\"inline_setup\"]\n                          setup_lines << arg_dep[\"inline_setup\"]\n                        end\n                      end\n                    end\n                  end\n                end\n\n                # Build parameter list, using inline_var for inlined deps\n                param_strs = [] of Nil\n                if params = definition[\"parameters\"]\n                  params.each do |name, param|\n                    value = param[\"value\"]\n                    value_str = value.id.stringify\n                    dep = SERVICE_HASH[value_str]\n\n                    if dep && dep[\"inlined\"] && dep[\"inline_var\"]\n                      param_strs << \"#{name.id}: #{dep[\"inline_var\"].id}\"\n                    elsif value.is_a?(ArrayLiteral)\n                      # Handle array with potential inlined services\n                      elements = value.map do |v|\n                        v_str = v.id.stringify\n                        v_dep = SERVICE_HASH[v_str]\n                        if v_dep && v_dep[\"inlined\"] && v_dep[\"inline_var\"]\n                          v_dep[\"inline_var\"].id\n                        else\n                          v\n                        end\n                      end\n                      str = \"#{name.id}: [#{elements.splat}]\"\n                      # Always add type annotation for arrays (needed for empty arrays)\n                      if (resolved_restriction = param[\"resolved_restriction\"]) && resolved_restriction <= Array\n                        str += \" of Union(#{resolved_restriction.type_vars.splat})\"\n                      end\n                      param_strs << str\n                    else\n                      param_strs << \"#{name.id}: #{value}\"\n                    end\n                  end\n                end\n\n                # Add this service's instantiation\n                setup_lines << \"#{var_name.id} = #{fq_prefix}#{constructor_service}.#{constructor_method.id}(#{param_strs.join(\", \").id})\"\n\n                # Add any calls on this service, transforming inlined service args\n                if calls = definition[\"calls\"]\n                  calls.each do |call|\n                    method, args = call\n                    transformed_args = args.map do |arg|\n                      arg_dep = SERVICE_HASH[arg.stringify]\n                      if arg_dep && arg_dep[\"inlined\"] && arg_dep[\"inline_var\"]\n                        arg_dep[\"inline_var\"].id\n                      else\n                        arg\n                      end\n                    end\n                    setup_lines << \"#{var_name.id}.#{method.id}(#{transformed_args.splat})\"\n                  end\n                end\n\n                definition[\"inline_setup\"] = setup_lines.join(\"\\n\")\n              else\n                # Dependencies not ready yet, push back for later processing\n                to_process << service_id\n              end\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/merge_configs.cr",
    "content": "# :nodoc:\n#\n# Merges successive calls to `ADI.configure`, with the last ones winning.\nmodule Athena::DependencyInjection::ServiceContainer::MergeConfigs\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          to_process = [] of Nil\n\n          CONFIGS.each do |c|\n            c.to_a.each do |tup|\n              to_process << {tup[0], tup[1], c, [tup[0]], CONFIG}\n            end\n          end\n\n          to_process.each do |(k, v, h, stack, root)|\n            if v.is_a?(NamedTupleLiteral)\n              v.to_a.each do |(sk, sv)|\n                to_process << {sk, sv, v, stack + [sk], root}\n              end\n            else\n              stack[..-2].each_with_index do |sk, idx|\n                if root[sk] == nil\n                  root[sk] = {__nil: nil} # Ensure this is a NamedTupleLiteral\n                end\n\n                root = root[sk]\n              end\n\n              root[k] = v\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/merge_extension_config.cr",
    "content": "# :nodoc:\n#\n# Compiler pass that merges user-provided configuration with extension schema defaults.\n#\n# This pass handles two main scenarios for each schema property:\n#   1. User provided a value: Transform it as needed (e.g., resolve enums, fill in default values for missing object members)\n#   2. User didn't provide a value: Use the schema's default value\n#\n# Key concepts:\n#   - CONFIG: User-provided configuration (from ADI.configure)\n#   - OPTIONS: Schema property definitions (from extension.cr macros)\n#   - member_map: For array_of/object_of/map_of, describes the structure of each element.\n#     Members can be TypeDeclaration (simple) or NamedTupleLiteral (object_schema ref).\n#   - map_of properties use `prop[\"type\"] <= Hash` as their identifying marker\nmodule Athena::DependencyInjection::ServiceContainer::MergeExtensionConfig\n  private EXTENSION_SCHEMA_PROPERTIES_MAP = {} of Nil => Nil\n\n  macro included\n    macro finished\n      {% verbatim do %}\n\n        # In order to keep extensions local to the DI component, they must be registered via a dedicated macro call.\n        # This includes the name of the extension and its schema.\n        # If the extension has any compiler passes (including the extension itself), those must be registered via a dedicated macro call as well.\n        # This setup keeps things pretty de-coupled; allowing use of extensions/compiler passes if used outside of the Framework.\n\n        {%\n          _nil = nil\n\n          # Array of tuples representing all of the extension types that are to be processed.\n          # 0 : String - name of the extension\n          # 1 : Array(String) - represents the path from root of the extension to this child extension type\n          # 2 : TypeNode - extension type\n          extensions_to_process = [] of Nil\n\n          extension_schema_map = {} of Nil => Nil\n\n          # For each extension type, register its base type\n          ADI::ServiceContainer::EXTENSIONS.each do |name, ext|\n            extensions_to_process << {name.id, [] of Nil, ext.resolve}\n          end\n\n          # For each base type, determine all child extension types\n          extensions_to_process.each do |(ext_name, ext_path, ext)|\n            ext.constants.reject(&.==(\"OPTIONS\")).each do |sub_ext|\n              t = parse_type(\"::#{ext}::#{sub_ext}\").resolve\n\n              # We only want to process sub extension modules, not just any primitive constant defined within these types\n              if t.is_a? TypeNode\n                extensions_to_process << {ext_name, ext_path + [sub_ext.stringify.underscore.downcase.id], t}\n              end\n            end\n          end\n\n          # For each extension to register, build out a schema hash consisting of the schema types related to each extension\n          extensions_to_process.each do |(ext_name, ext_path, ext)|\n            if extension_schema_map[ext_name] == nil\n              ext_options = extension_schema_map[ext_name] = {__nil: nil} # Ensure this is a NamedTupleLiteral\n              EXTENSION_SCHEMA_PROPERTIES_MAP[ext_name] = [] of Nil\n            end\n\n            ext.constant(\"OPTIONS\").each do |o|\n              obj = extension_schema_map[ext_name]\n\n              if ext_path.empty?\n                obj[o[\"name\"]] = o\n                EXTENSION_SCHEMA_PROPERTIES_MAP[ext_name] << {o, ext_path}\n              else\n                ext_path.each_with_index do |k, idx|\n                  obj[k] = {} of Nil => Nil if obj[k] == nil\n                  obj = obj[k]\n\n                  if idx == ext_path.size - 1\n                    obj[o[\"name\"]] = o\n                    EXTENSION_SCHEMA_PROPERTIES_MAP[ext_name] << {o, ext_path}\n                  end\n                end\n              end\n            end\n          end\n\n          # Validate there is no configuration for un-registered extensions\n          extra_keys = CONFIG.keys.reject { |k| k == \"parameters\".id || k == \"__nil\".id } - extension_schema_map.keys\n\n          unless extra_keys.empty?\n            CONFIG[extra_keys.first].raise \"Extension '#{extra_keys.first.id}' is configured, but no extension with that name has been registered.\"\n          end\n\n          EXTENSION_SCHEMA_PROPERTIES_MAP.each do |ext_name, schema_properties|\n            # Ensure the root CONFIG obj has a key for each extension\n            unless extension_config = CONFIG[ext_name]\n              extension_config = CONFIG[ext_name] = {__nil: nil} # Ensure this is a NamedTupleLiteral\n            end\n\n            # Iterate over each schema property to process them\n            schema_properties.each do |(prop, ext_path)|\n              extension_schema_for_current_property = extension_schema_map[ext_name]\n              extension_config_for_current_property = extension_config\n\n              ext_path.each do |p|\n                extension_schema_for_current_property = extension_schema_for_current_property[p] if extension_schema_for_current_property\n\n                # Ensure this is a NamedTupleLiteral, expand user provided value to include defaults from schema if not provided\n                extension_config_for_current_property[p] = {__nil: nil} if extension_config_for_current_property[p] == nil\n                extension_config_for_current_property = extension_config_for_current_property[p]\n              end\n\n              # If the user provided configuration, check for unexpected keys\n              extra_keys = extension_config_for_current_property.keys.reject { |k| k == \"__nil\".id } - extension_schema_for_current_property.keys\n\n              unless extra_keys.empty?\n                extra_key_value = extension_config_for_current_property[extra_keys.first]\n\n                extra_key_value.raise \"Unexpected property '#{([ext_name] + ext_path).join('.').id}.#{extra_keys.first.id}'.\"\n              end\n\n              # Then handle any light transformations needed to get the configuration value into the expected format/type\n              if (config_value = extension_config_for_current_property[prop[\"name\"]]) != nil\n                config_value = config_value.is_a?(Path) ? config_value.resolve : config_value\n\n                resolved_value = if config_value.is_a?(SymbolLiteral) && (type = prop[\"type\"]) <= ::Enum\n                                   config_value.raise \"Unknown '#{type}' enum member for property '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}'.\" unless type.constants.any?(&.downcase.id.==(config_value.id))\n\n                                   # Resolve symbol literals to enum members\n                                   config_value = \"#{prop[\"global\"] ? \"::\".id : \"\".id}#{type}.new(#{config_value})\".id\n                                 elsif config_value.is_a?(NumberLiteral) && (type = prop[\"type\"]) <= ::Enum\n                                   # Resolve enum value to enum members\n                                   config_value = \"#{prop[\"global\"] ? \"::\".id : \"\".id}#{type}.new(#{config_value})\".id\n                                 elsif config_value.is_a?(ArrayLiteral)\n                                   # If there is an array literal and the prop has `members`,\n                                   # assume it is an `array_of` schema property array and fill in unprovided fields.\n                                   if member_map = prop[\"members\"]\n                                     config_value.each do |cfv|\n                                       provided_keys = cfv.keys\n\n                                       # Convert user-provided enum values for object_schema members\n                                       # Skip nested object_schema references (NamedTupleLiteral with members key)\n                                       provided_keys.reject { |k| k.stringify == \"__nil\" }.each do |k|\n                                         if (decl = member_map[k]) && (user_value = cfv[k]) && !(decl.is_a?(NamedTupleLiteral) && decl[\"members\"])\n                                           decl_type = decl.is_a?(TypeDeclaration) ? decl.type : decl[\"type\"]\n                                           decl_global = decl_type.is_a?(Path) && decl_type.global?\n                                           resolved_decl_type = decl_type.resolve\n\n                                           if user_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                             user_value.raise \"Unknown '#{resolved_decl_type}' enum member for '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(user_value.id))\n                                             cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{user_value})\".id\n                                           elsif user_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                             cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{user_value})\".id\n                                           end\n                                         end\n                                       end\n\n                                       # We only want to add in missing default values, so reject any properties that were provided, even if they may be incorrect.\n                                       member_map.keys.reject { |k| k.stringify == \"__nil\" || provided_keys.includes? k }.each do |k|\n                                         decl = member_map[k]\n\n                                         # Handle both TypeDeclaration and NamedTupleLiteral (for nested object_schema)\n                                         decl_value = decl.is_a?(TypeDeclaration) ? decl.value : decl[\"value\"]\n                                         # Skip setting required values so that it results in a missing error vs type mismatch error.\n                                         unless decl_value.is_a?(Nop)\n                                           # Skip enum conversion for nested object_schema references\n                                           if decl.is_a?(NamedTupleLiteral) && decl[\"members\"]\n                                             cfv[k] = decl_value\n                                           else\n                                             # Convert symbol/number to enum if applicable\n                                             decl_type = decl.is_a?(TypeDeclaration) ? decl.type : decl[\"type\"]\n                                             decl_global = decl_type.is_a?(Path) && decl_type.global?\n                                             resolved_decl_type = decl_type.resolve\n\n                                             if decl_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                               decl_value.raise \"Unknown '#{resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(decl_value.id))\n                                               cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                             elsif decl_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                               cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                             else\n                                               cfv[k] = decl_value\n                                             end\n                                           end\n                                         end\n                                       end\n\n                                       # Recursively fill in defaults for nested object_schema members\n                                       member_map.keys.reject { |k| k.stringify == \"__nil\" }.each do |k|\n                                         decl = member_map[k]\n                                         if decl.is_a?(NamedTupleLiteral) && (nested_members = decl[\"members\"]) && (nested_cfv = cfv[k])\n                                           nested_provided_keys = nested_cfv.keys\n\n                                           # Convert user-provided enum values for nested object_schema members\n                                           # Skip nested object_schema references\n                                           nested_provided_keys.reject { |nk| nk.stringify == \"__nil\" }.each do |nk|\n                                             if (nested_decl = nested_members[nk]) && (nested_user_value = nested_cfv[nk]) && !(nested_decl.is_a?(NamedTupleLiteral) && nested_decl[\"members\"])\n                                               nested_decl_type = nested_decl.is_a?(TypeDeclaration) ? nested_decl.type : nested_decl[\"type\"]\n                                               nested_decl_global = nested_decl_type.is_a?(Path) && nested_decl_type.global?\n                                               nested_resolved_decl_type = nested_decl_type.resolve\n\n                                               if nested_user_value.is_a?(SymbolLiteral) && nested_resolved_decl_type <= ::Enum\n                                                 nested_user_value.raise \"Unknown '#{nested_resolved_decl_type}' enum member for '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}.#{nk.id}'.\" unless nested_resolved_decl_type.constants.any?(&.downcase.id.==(nested_user_value.id))\n                                                 nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_user_value})\".id\n                                               elsif nested_user_value.is_a?(NumberLiteral) && nested_resolved_decl_type <= ::Enum\n                                                 nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_user_value})\".id\n                                               end\n                                             end\n                                           end\n\n                                           nested_members.keys.reject { |nk| nk.stringify == \"__nil\" || nested_provided_keys.includes? nk }.each do |nk|\n                                             nested_decl = nested_members[nk]\n                                             nested_decl_value = nested_decl.is_a?(TypeDeclaration) ? nested_decl.value : nested_decl[\"value\"]\n                                             unless nested_decl_value.is_a?(Nop)\n                                               # Skip enum conversion for nested object_schema references\n                                               if nested_decl.is_a?(NamedTupleLiteral) && nested_decl[\"members\"]\n                                                 nested_cfv[nk] = nested_decl_value\n                                               else\n                                                 # Convert symbol/number to enum if applicable\n                                                 nested_decl_type = nested_decl.is_a?(TypeDeclaration) ? nested_decl.type : nested_decl[\"type\"]\n                                                 nested_decl_global = nested_decl_type.is_a?(Path) && nested_decl_type.global?\n                                                 nested_resolved_decl_type = nested_decl_type.resolve\n\n                                                 if nested_decl_value.is_a?(SymbolLiteral) && nested_resolved_decl_type <= ::Enum\n                                                   nested_decl_value.raise \"Unknown '#{nested_resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}.#{nk.id}'.\" unless nested_resolved_decl_type.constants.any?(&.downcase.id.==(nested_decl_value.id))\n                                                   nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_decl_value})\".id\n                                                 elsif nested_decl_value.is_a?(NumberLiteral) && nested_resolved_decl_type <= ::Enum\n                                                   nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_decl_value})\".id\n                                                 else\n                                                   nested_cfv[nk] = nested_decl_value\n                                                 end\n                                               end\n                                             end\n                                           end\n                                         end\n                                       end\n                                     end\n                                   end\n\n                                   config_value\n                                 elsif config_value.is_a?(NamedTupleLiteral)\n                                   # NamedTupleLiteral handles three cases:\n                                   #   1. map_of values: {key1: {members...}, key2: {members...}}\n                                   #   2. object_of values: {member1: val, member2: val}\n                                   #   3. Inline NamedTuple type properties\n                                   #\n                                   # Check if this is a map_of property (type is Hash and has members).\n                                   if prop[\"type\"] <= Hash && (member_map = prop[\"members\"])\n                                     config_value.each do |hash_key, cfv|\n                                       if hash_key != \"__nil\"\n                                         provided_keys = cfv.keys\n\n                                         # Convert user-provided enum values for object_schema members\n                                         # Skip nested object_schema references (NamedTupleLiteral with members key)\n                                         provided_keys.reject { |k| k.stringify == \"__nil\" }.each do |k|\n                                           if (decl = member_map[k]) && (user_value = cfv[k]) && !(decl.is_a?(NamedTupleLiteral) && decl[\"members\"])\n                                             decl_type = decl.is_a?(TypeDeclaration) ? decl.type : decl[\"type\"]\n                                             decl_global = decl_type.is_a?(Path) && decl_type.global?\n                                             resolved_decl_type = decl_type.resolve\n\n                                             if user_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                               user_value.raise \"Unknown '#{resolved_decl_type}' enum member for '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{hash_key.id}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(user_value.id))\n                                               cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{user_value})\".id\n                                             elsif user_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                               cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{user_value})\".id\n                                             end\n                                           end\n                                         end\n\n                                         member_map.keys.reject { |k| k.stringify == \"__nil\" || provided_keys.includes? k }.each do |k|\n                                           decl = member_map[k]\n\n                                           # Handle both TypeDeclaration and NamedTupleLiteral (for nested object_schema)\n                                           decl_value = decl.is_a?(TypeDeclaration) ? decl.value : decl[\"value\"]\n                                           # Skip setting required values so that it results in a missing error vs type mismatch error.\n                                           unless decl_value.is_a?(Nop)\n                                             # Skip enum conversion for nested object_schema references\n                                             if decl.is_a?(NamedTupleLiteral) && decl[\"members\"]\n                                               cfv[k] = decl_value\n                                             else\n                                               # Convert symbol/number to enum if applicable\n                                               decl_type = decl.is_a?(TypeDeclaration) ? decl.type : decl[\"type\"]\n                                               decl_global = decl_type.is_a?(Path) && decl_type.global?\n                                               resolved_decl_type = decl_type.resolve\n\n                                               if decl_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                                 decl_value.raise \"Unknown '#{resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{hash_key.id}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(decl_value.id))\n                                                 cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                               elsif decl_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                                 cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                               else\n                                                 cfv[k] = decl_value\n                                               end\n                                             end\n                                           end\n                                         end\n\n                                         # Recursively fill in defaults for nested object_schema members\n                                         member_map.keys.reject { |k| k.stringify == \"__nil\" }.each do |k|\n                                           decl = member_map[k]\n                                           if decl.is_a?(NamedTupleLiteral) && (nested_members = decl[\"members\"]) && (nested_cfv = cfv[k])\n                                             nested_provided_keys = nested_cfv.keys\n\n                                             # Convert user-provided enum values for nested object_schema members\n                                             # Skip nested object_schema references\n                                             nested_provided_keys.reject { |nk| nk.stringify == \"__nil\" }.each do |nk|\n                                               if (nested_decl = nested_members[nk]) && (nested_user_value = nested_cfv[nk]) && !(nested_decl.is_a?(NamedTupleLiteral) && nested_decl[\"members\"])\n                                                 nested_decl_type = nested_decl.is_a?(TypeDeclaration) ? nested_decl.type : nested_decl[\"type\"]\n                                                 nested_decl_global = nested_decl_type.is_a?(Path) && nested_decl_type.global?\n                                                 nested_resolved_decl_type = nested_decl_type.resolve\n\n                                                 if nested_user_value.is_a?(SymbolLiteral) && nested_resolved_decl_type <= ::Enum\n                                                   nested_user_value.raise \"Unknown '#{nested_resolved_decl_type}' enum member for '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{hash_key.id}.#{k.id}.#{nk.id}'.\" unless nested_resolved_decl_type.constants.any?(&.downcase.id.==(nested_user_value.id))\n                                                   nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_user_value})\".id\n                                                 elsif nested_user_value.is_a?(NumberLiteral) && nested_resolved_decl_type <= ::Enum\n                                                   nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_user_value})\".id\n                                                 end\n                                               end\n                                             end\n\n                                             nested_members.keys.reject { |nk| nk.stringify == \"__nil\" || nested_provided_keys.includes? nk }.each do |nk|\n                                               nested_decl = nested_members[nk]\n                                               nested_decl_value = nested_decl.is_a?(TypeDeclaration) ? nested_decl.value : nested_decl[\"value\"]\n                                               unless nested_decl_value.is_a?(Nop)\n                                                 # Skip enum conversion for nested object_schema references\n                                                 if nested_decl.is_a?(NamedTupleLiteral) && nested_decl[\"members\"]\n                                                   nested_cfv[nk] = nested_decl_value\n                                                 else\n                                                   # Convert symbol/number to enum if applicable\n                                                   nested_decl_type = nested_decl.is_a?(TypeDeclaration) ? nested_decl.type : nested_decl[\"type\"]\n                                                   nested_decl_global = nested_decl_type.is_a?(Path) && nested_decl_type.global?\n                                                   nested_resolved_decl_type = nested_decl_type.resolve\n\n                                                   if nested_decl_value.is_a?(SymbolLiteral) && nested_resolved_decl_type <= ::Enum\n                                                     nested_decl_value.raise \"Unknown '#{nested_resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{hash_key.id}.#{k.id}.#{nk.id}'.\" unless nested_resolved_decl_type.constants.any?(&.downcase.id.==(nested_decl_value.id))\n                                                     nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_decl_value})\".id\n                                                   elsif nested_decl_value.is_a?(NumberLiteral) && nested_resolved_decl_type <= ::Enum\n                                                     nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_decl_value})\".id\n                                                   else\n                                                     nested_cfv[nk] = nested_decl_value\n                                                   end\n                                                 end\n                                               end\n                                             end\n                                           end\n                                         end\n                                       end\n                                     end\n\n                                     config_value\n                                     # Fill in `nil` values to missing nilable NT keys\n                                   elsif member_map = prop[\"members\"]\n                                     provided_keys = config_value.keys\n\n                                     # Convert user-provided enum values for object_schema members\n                                     # Skip nested object_schema references (NamedTupleLiteral with members key)\n                                     provided_keys.reject { |k| k.stringify == \"__nil\" }.each do |k|\n                                       if (decl = member_map[k]) && (user_value = config_value[k]) && !(decl.is_a?(NamedTupleLiteral) && decl[\"members\"])\n                                         decl_type = decl.is_a?(TypeDeclaration) ? decl.type : decl[\"type\"]\n                                         decl_global = decl_type.is_a?(Path) && decl_type.global?\n                                         resolved_decl_type = decl_type.resolve\n\n                                         if user_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                           user_value.raise \"Unknown '#{resolved_decl_type}' enum member for '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(user_value.id))\n                                           config_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{user_value})\".id\n                                         elsif user_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                           config_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{user_value})\".id\n                                         end\n                                       end\n                                     end\n\n                                     # We only want to add in missing default values, so reject any properties that were provided, even if they may be incorrect.\n                                     member_map.keys.reject { |k| k.stringify == \"__nil\" || provided_keys.includes? k }.each do |k|\n                                       decl = member_map[k]\n\n                                       # Handle both TypeDeclaration and NamedTupleLiteral (for nested object_schema)\n                                       if decl.is_a?(TypeDeclaration)\n                                         # If the value has a default, use it.\n                                         # Otherwise skip setting required values so that it results in a missing error vs type mismatch error.\n                                         if !decl.value.is_a?(Nop)\n                                           decl_value = decl.value\n                                           # Convert symbol/number to enum if applicable\n                                           decl_global = decl.type.is_a?(Path) && decl.type.global?\n                                           resolved_decl_type = decl.type.resolve\n\n                                           if decl_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                             decl_value.raise \"Unknown '#{resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(decl_value.id))\n                                             config_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                           elsif decl_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                             config_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                           else\n                                             config_value[k] = decl_value\n                                           end\n                                         elsif decl.type.resolve.nilable?\n                                           config_value[k] = nil\n                                         end\n                                       elsif decl.is_a?(NamedTupleLiteral)\n                                         # Nested object_schema reference\n                                         decl_value = decl[\"value\"]\n                                         unless decl_value.is_a?(Nop)\n                                           # Convert symbol/number to enum if applicable\n                                           decl_type = decl[\"type\"]\n                                           decl_global = decl_type.is_a?(Path) && decl_type.global?\n                                           resolved_decl_type = decl_type.resolve\n\n                                           if decl_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                             decl_value.raise \"Unknown '#{resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(decl_value.id))\n                                             config_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                           elsif decl_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                             config_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                           else\n                                             config_value[k] = decl_value\n                                           end\n                                         end\n                                       end\n                                     end\n\n                                     # Recursively fill in defaults for nested object_schema members\n                                     member_map.keys.reject { |k| k.stringify == \"__nil\" }.each do |k|\n                                       decl = member_map[k]\n                                       if decl.is_a?(NamedTupleLiteral) && (nested_members = decl[\"members\"]) && (nested_cfv = config_value[k])\n                                         nested_provided_keys = nested_cfv.keys\n\n                                         # Convert user-provided enum values for nested object_schema members\n                                         # Skip nested object_schema references\n                                         nested_provided_keys.reject { |nk| nk.stringify == \"__nil\" }.each do |nk|\n                                           if (nested_decl = nested_members[nk]) && (nested_user_value = nested_cfv[nk]) && !(nested_decl.is_a?(NamedTupleLiteral) && nested_decl[\"members\"])\n                                             nested_decl_type = nested_decl.is_a?(TypeDeclaration) ? nested_decl.type : nested_decl[\"type\"]\n                                             nested_decl_global = nested_decl_type.is_a?(Path) && nested_decl_type.global?\n                                             nested_resolved_decl_type = nested_decl_type.resolve\n\n                                             if nested_user_value.is_a?(SymbolLiteral) && nested_resolved_decl_type <= ::Enum\n                                               nested_user_value.raise \"Unknown '#{nested_resolved_decl_type}' enum member for '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}.#{nk.id}'.\" unless nested_resolved_decl_type.constants.any?(&.downcase.id.==(nested_user_value.id))\n                                               nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_user_value})\".id\n                                             elsif nested_user_value.is_a?(NumberLiteral) && nested_resolved_decl_type <= ::Enum\n                                               nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_user_value})\".id\n                                             end\n                                           end\n                                         end\n\n                                         nested_members.keys.reject { |nk| nk.stringify == \"__nil\" || nested_provided_keys.includes? nk }.each do |nk|\n                                           nested_decl = nested_members[nk]\n                                           nested_decl_value = nested_decl.is_a?(TypeDeclaration) ? nested_decl.value : nested_decl[\"value\"]\n                                           unless nested_decl_value.is_a?(Nop)\n                                             # Skip enum conversion for nested object_schema references\n                                             if nested_decl.is_a?(NamedTupleLiteral) && nested_decl[\"members\"]\n                                               nested_cfv[nk] = nested_decl_value\n                                             else\n                                               # Convert symbol/number to enum if applicable\n                                               nested_decl_type = nested_decl.is_a?(TypeDeclaration) ? nested_decl.type : nested_decl[\"type\"]\n                                               nested_decl_global = nested_decl_type.is_a?(Path) && nested_decl_type.global?\n                                               nested_resolved_decl_type = nested_decl_type.resolve\n\n                                               if nested_decl_value.is_a?(SymbolLiteral) && nested_resolved_decl_type <= ::Enum\n                                                 nested_decl_value.raise \"Unknown '#{nested_resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}.#{nk.id}'.\" unless nested_resolved_decl_type.constants.any?(&.downcase.id.==(nested_decl_value.id))\n                                                 nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_decl_value})\".id\n                                               elsif nested_decl_value.is_a?(NumberLiteral) && nested_resolved_decl_type <= ::Enum\n                                                 nested_cfv[nk] = \"#{nested_decl_global ? \"::\".id : \"\".id}#{nested_resolved_decl_type}.new(#{nested_decl_value})\".id\n                                               else\n                                                 nested_cfv[nk] = nested_decl_value\n                                               end\n                                             end\n                                           end\n                                         end\n                                       end\n                                     end\n                                   else\n                                     p = prop[\"type\"]\n\n                                     p.keys.each do |k|\n                                       t = p[k]\n\n                                       if config_value[k] == nil && t.nilable?\n                                         config_value[k] = nil\n                                       end\n                                     end\n                                   end\n\n                                   config_value\n                                 else\n                                   config_value\n                                 end\n              else\n                # Otherwise fall back on the default value of the property\n                resolved_value = if prop[\"default\"].is_a?(Nop)\n                                   nil\n                                 elsif prop[\"default\"].is_a?(Path)\n                                   prop[\"default\"].resolve\n                                 else\n                                   default_value = prop[\"default\"]\n\n                                   # Resolve symbol literals to enum members\n                                   if default_value.is_a?(SymbolLiteral) && (type = prop[\"type\"]) <= ::Enum\n                                     prop[\"default\"].raise \"Unknown '#{type}' enum member for default value of property '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}'.\" unless type.constants.any?(&.downcase.id.==(default_value.id))\n\n                                     # Resolve symbol literals to enum members\n                                     default_value = \"#{prop[\"global\"] ? \"::\".id : \"\".id}#{type}.new(#{default_value})\".id\n                                   elsif default_value.is_a?(ArrayLiteral)\n                                     # If there is an array literal and the prop has `members`,\n                                     # assume it is an `array_of` schema property array and fill in unprovided fields.\n                                     if member_map = prop[\"members\"]\n                                       default_value.each do |cfv|\n                                         provided_keys = cfv.keys\n\n                                         # We only want to add in missing default values, so reject any properties that were provided, even if they may be incorrect.\n                                         member_map.keys.reject { |k| k.stringify == \"__nil\" || provided_keys.includes? k }.each do |k|\n                                           decl = member_map[k]\n\n                                           # Skip setting required values so that it results in a missing error vs type mismatch error.\n                                           unless decl.value.is_a?(Nop)\n                                             decl_value = decl.value\n                                             # Convert symbol/number to enum if applicable\n                                             decl_global = decl.type.is_a?(Path) && decl.type.global?\n                                             resolved_decl_type = decl.type.resolve\n\n                                             if decl_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                               decl_value.raise \"Unknown '#{resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(decl_value.id))\n                                               cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                             elsif decl_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                               cfv[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                             else\n                                               cfv[k] = decl_value\n                                             end\n                                           end\n                                         end\n                                       end\n                                     end\n\n                                     default_value\n                                   elsif default_value.is_a?(NamedTupleLiteral)\n                                     # Fill in `nil` values to missing nilable NT keys\n                                     # Skip for map_of properties - the empty map default shouldn't have member defaults filled in\n                                     if !(prop[\"type\"] <= Hash) && (member_map = prop[\"members\"])\n                                       provided_keys = default_value.keys\n\n                                       # We only want to add in missing default values, so reject any properties that were provided, even if they may be incorrect.\n                                       member_map.keys.reject { |k| k.stringify == \"__nil\" || provided_keys.includes? k }.each do |k|\n                                         decl = member_map[k]\n\n                                         # Handle both TypeDeclaration and NamedTupleLiteral (for nested object_schema)\n                                         if decl.is_a?(TypeDeclaration)\n                                           # If the value has a default, use it.\n                                           # Otherwise skip setting required values so that it results in a missing error vs type mismatch error.\n                                           if !decl.value.is_a?(Nop)\n                                             decl_value = decl.value\n                                             # Convert symbol/number to enum if applicable\n                                             decl_global = decl.type.is_a?(Path) && decl.type.global?\n                                             resolved_decl_type = decl.type.resolve\n\n                                             if decl_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                               decl_value.raise \"Unknown '#{resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(decl_value.id))\n                                               default_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                             elsif decl_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                               default_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                             else\n                                               default_value[k] = decl_value\n                                             end\n                                           elsif decl.type.resolve.nilable?\n                                             default_value[k] = nil\n                                           end\n                                         elsif decl.is_a?(NamedTupleLiteral)\n                                           # Nested object_schema reference\n                                           decl_value = decl[\"value\"]\n                                           unless decl_value.is_a?(Nop)\n                                             # Convert symbol/number to enum if applicable\n                                             decl_type = decl[\"type\"]\n                                             decl_global = decl_type.is_a?(Path) && decl_type.global?\n                                             resolved_decl_type = decl_type.resolve\n\n                                             if decl_value.is_a?(SymbolLiteral) && resolved_decl_type <= ::Enum\n                                               decl_value.raise \"Unknown '#{resolved_decl_type}' enum member for default value of '#{([ext_name] + ext_path).join('.').id}.#{prop[\"name\"]}.#{k.id}'.\" unless resolved_decl_type.constants.any?(&.downcase.id.==(decl_value.id))\n                                               default_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                             elsif decl_value.is_a?(NumberLiteral) && resolved_decl_type <= ::Enum\n                                               default_value[k] = \"#{decl_global ? \"::\".id : \"\".id}#{resolved_decl_type}.new(#{decl_value})\".id\n                                             else\n                                               default_value[k] = decl_value\n                                             end\n                                           end\n                                         end\n                                       end\n                                     end\n                                   end\n\n                                   default_value\n                                 end\n              end\n\n              extension_config_for_current_property[prop[\"name\"]] = resolved_value\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/normalize_definitions.cr",
    "content": "# :nodoc:\n#\n# Runs after extensions to normalize the manually wired up services.\n# Ensures required keys are present and with proper defaults if not specified.\nmodule Athena::DependencyInjection::ServiceContainer::NormalizeDefinitions\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          SERVICE_HASH.each do |service_id, definition|\n            definition_keys = definition.keys.map &.stringify\n\n            unless definition_keys.includes? \"class\"\n              definition.raise \"Service '#{service_id.id}' is missing required 'class' property.\"\n            end\n\n            unless definition_keys.includes? \"public\"\n              definition[\"public\"] = false\n            end\n\n            unless definition_keys.includes? \"shared\"\n              definition[\"shared\"] = definition[\"class\"].class?\n            end\n\n            unless definition_keys.includes? \"calls\"\n              definition[\"calls\"] = [] of Nil\n            end\n\n            unless definition_keys.includes? \"tags\"\n              definition[\"tags\"] = [] of Nil\n            end\n\n            unless definition_keys.includes? \"bindings\"\n              definition[\"bindings\"] = {} of Nil => Nil\n            end\n\n            unless definition_keys.includes? \"parameters\"\n              definition[\"parameters\"] = {} of Nil => Nil\n            end\n\n            unless definition_keys.includes? \"generics\"\n              definition[\"generics\"] = [] of Nil\n            end\n\n            unless definition_keys.includes? \"referenced_services\"\n              definition[\"referenced_services\"] = [] of Nil\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/process_aliases.cr",
    "content": "# :nodoc:\nmodule Athena::DependencyInjection::ServiceContainer::ProcessAliases\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          SERVICE_HASH.each do |service_id, definition|\n            interface_modules = definition[\"class\"].ancestors.select &.name.ends_with? \"Interface\"\n            default_alias = 1 == interface_modules.size ? interface_modules[0] : nil\n\n            definition[\"class\"].annotations(ADI::AsAlias).each do |ann|\n              alias_id = if alias_type = ann[0]\n                           alias_type.is_a?(Path) ? alias_type.resolve : alias_type\n                         else\n                           default_alias\n                         end\n\n              unless alias_id\n                ann.raise <<-TXT\n                Alias cannot be automatically determined for '#{service_id.id}' (#{definition[\"class\"]}). \\\n                If the type includes multiple interfaces, provide the interface to alias as the first positional argument to `@[ADI::AsAlias]`.\n                TXT\n              end\n\n              param_name = ann[\"name\"]\n\n              # Initialize the array for this alias type if needed\n              ALIASES[alias_id] = [] of Nil if ALIASES[alias_id].nil?\n\n              # Check for duplicate type+name combination\n              if ALIASES[alias_id].any? { |a| a[\"name\"] == param_name }\n                if param_name\n                  ann.raise \"Duplicate alias for type '#{alias_id}' with name '#{param_name.id}'. \" \\\n                            \"An alias with this type and name combination is already registered.\"\n                else\n                  ann.raise \"Duplicate alias for type '#{alias_id}'. \" \\\n                            \"A type-only alias for this type is already registered.\"\n                end\n              end\n\n              ALIASES[alias_id] << {\n                id:     service_id,\n                public: ann[\"public\"] == true,\n                name:   param_name,\n              }\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/process_annotation_bindings.cr",
    "content": "# :nodoc:\n#\n# Applies bindings from the register annotation.\nmodule Athena::DependencyInjection::ServiceContainer::ProcessAnnotationBindings\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          SERVICE_HASH.each do |_, definition|\n            annotations = definition[\"class\"].annotations ADI::Register\n\n            # If there is only 1 ann, it's going to be this def,\n            # otherwise we need to extract the name off the annotation to update the proper definition.\n            # Depends on the `RegisterServices` logic that ensures there is a `name` property on all annotations when there is more than one.\n            if 1 == annotations.size\n              annotations.first.named_args.each do |k, v|\n                if k.starts_with? '_'\n                  definition[\"bindings\"][k[1..-1]] = v\n                end\n              end\n            else\n              annotations.each do |ann|\n                ann.named_args.each do |k, v|\n                  if k.starts_with? '_'\n                    SERVICE_HASH[ann[\"name\"]][\"bindings\"][k[1..-1]] = v\n                  end\n                end\n              end\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/process_autoconfigure_annotations.cr",
    "content": "# :nodoc:\n#\n# Processes `@[ADI::Autoconfigure]` annotations and `AUTO_CONFIGURATIONS` entries\nmodule Athena::DependencyInjection::ServiceContainer::ProcessAutoconfigureAnnotations\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          __nil = nil\n\n          # Unified hash of auto configurations keyed by resolved type.\n          # Each value is a named tuple: {tags, calls, bind, public, constructor}\n          auto_configs = AUTO_CONFIGURATIONS\n\n          # Build out a list of interfaces, and types that can be used to autoconfigure other services\n          #\n          # Array(TypeNode)\n          types_to_process = [] of Nil\n\n          # Use `Object.all_subclasses` since some autoconfigured types may not be services themselves.\n          Object.all_subclasses.each do |sc|\n            # Used interface modules (this only captures modules that would be included in at least 1 used type)\n            sc.ancestors.each do |a|\n              # TODO: Use `#private?` once available.\n              if a.module? && (m = parse_type(a.name(generic_args: false).stringify).resolve?) && (m.annotation(ADI::Autoconfigure) || m.annotation(ADI::AutoconfigureTag))\n                types_to_process << m\n              end\n            end\n\n            # Non module types that may be the parent type of a service.\n            types_to_process << sc if sc.annotation(ADI::Autoconfigure) || sc.annotation(ADI::AutoconfigureTag)\n          end\n\n          # Don't process types more than once.\n          types_to_process = types_to_process.uniq\n\n          types_to_process.each do |t|\n            if auto_configs[t]\n              t.raise \"Auto configuration for '#{t.id}' is already defined in 'AUTO_CONFIGURATIONS'. Remove the annotation or the manual entry.\"\n            end\n\n            tags = [] of Nil\n\n            if at = t.annotation(ADI::AutoconfigureTag)\n              tag_name = if n = at[0]\n                           if n.is_a?(Path)\n                             n.resolve\n                           else\n                             n\n                           end\n                         else\n                           t.stringify\n                         end\n\n              tag = {name: tag_name}\n\n              at.named_args.each do |k, v|\n                tag[k.id.stringify] = v\n              end\n\n              tags << tag\n            end\n\n            ann = t.annotation ADI::Autoconfigure\n\n            if ann && (v = ann[\"tags\"]) != nil\n              unless v.is_a? ArrayLiteral\n                v.raise \"'tags' field of auto configuration '#{t.id}' must be an 'ArrayLiteral', got '#{v.class_name.id}'.\"\n              end\n\n              v.each do |tag|\n                tags << tag\n              end\n            end\n\n            auto_configs[t] = {\n              tags:        tags,\n              calls:       ann ? ann[\"calls\"] : nil,\n              bind:        ann ? ann[\"bind\"] : nil,\n              public:      ann ? ann[\"public\"] : nil,\n              constructor: ann ? ann[\"constructor\"] : nil,\n            }\n          end\n\n          SERVICE_HASH.each do |service_id, definition|\n            klass = definition[\"class\"]\n\n            auto_configs.each do |t, config|\n              if klass <= t\n                if (v = config[\"constructor\"]) != nil\n                  definition[\"factory\"] = {klass, v}\n                end\n\n                if (v = config[\"bind\"]) != nil\n                  v.each do |k, v|\n                    definition[\"bindings\"][k] = v\n                  end\n                end\n\n                if (v = config[\"public\"]) != nil\n                  definition[\"public\"] = v\n                end\n\n                if (v = config[\"calls\"]) != nil\n                  calls = [] of Nil\n\n                  v.each do |call|\n                    method = call[0]\n                    args = call[1] || nil\n\n                    if method.empty?\n                      method.raise \"Auto configuration '#{t.id}': 'calls' method name cannot be empty.\"\n                    end\n\n                    unless klass.resolve.has_method?(method)\n                      method.raise \"Auto configuration '#{t.id}': 'calls' method does not exist on service '#{service_id.id}' (#{klass}).\"\n                    end\n\n                    calls << {method, args || [] of Nil}\n                  end\n\n                  definition[\"calls\"] = calls\n                end\n\n                # Append raw tags - will be normalized by ProcessTags pass\n                if tags = config[\"tags\"]\n                  tags.each do |tag|\n                    definition[\"tags\"] << tag\n                  end\n                end\n              end\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/process_bindings.cr",
    "content": "# :nodoc:\n#\n# Service bindings overrides those defined globally, but both override autoconfigured bindings.\nmodule Athena::DependencyInjection::ServiceContainer::ProcessBindings\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          SERVICE_HASH.each do |_, definition|\n            definition[\"parameters\"].each do |name, param|\n              set_value = false\n\n              # Typed binding\n              BINDINGS.keys.select(&.is_a?(TypeDeclaration)).each do |key|\n                if key.var.id == param[\"name\"].id && (type = param[\"resolved_restriction\"]) && key.type.resolve >= type\n                  set_value = true\n                  definition[\"bindings\"][name.id] = BINDINGS[key]\n                end\n              end\n\n              # Untyped binding\n              BINDINGS.keys.select(&.!.is_a?(TypeDeclaration)).each do |key|\n                if key.id == param[\"name\"].id && !set_value\n                  # Only set a value if one was not already set via a typed binding\n                  definition[\"bindings\"][name.id] = BINDINGS[key]\n                end\n              end\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/process_parameters.cr",
    "content": "# :nodoc:\n#\n# Processes each service definition to determine their constructor parameters.\n# Also ensures manually wired up services have full and proper initializer information\nmodule Athena::DependencyInjection::ServiceContainer::ProcessParameters\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          SERVICE_HASH.each do |service_id, definition|\n            klass = definition[\"class\"]\n\n            initializer = if f = definition[\"factory\"]\n                            f.first.class.methods.find(&.name.==(f[1]))\n                          elsif specific_initializer = klass.methods.find(&.annotation(ADI::Inject))\n                            specific_initializer\n                          else\n                            klass.methods.find(&.name.==(\"initialize\"))\n                          end\n\n            # If no initializer was resolved, assume it's the default argless constructor.\n            initializer_args = (i = initializer) ? i.args : [] of Nil\n\n            parameters = definition[\"parameters\"]\n\n            initializer_args.each_with_index do |initializer_arg, idx|\n              param_name = initializer_arg.name.id.stringify\n              default_value = nil\n\n              # Inherit value if it was already configured on the param\n              value = if (p = parameters[param_name]) && p.keys.map(&.stringify).includes?(\"value\")\n                        p[\"value\"]\n                      else\n                        nil\n                      end\n\n              # Set the default value is there is one.\n              if !(dv = initializer_arg.default_value).is_a?(Nop)\n                default_value = dv\n              end\n\n              parameters[initializer_arg.name.id.stringify] = {\n                declaration:          initializer_arg,\n                name:                 initializer_arg.name.stringify,\n                idx:                  idx,\n                internal_name:        initializer_arg.internal_name.stringify,\n                restriction:          initializer_arg.restriction,\n                resolved_restriction: ((r = initializer_arg.restriction).is_a?(Nop) ? nil : r.resolve),\n                default_value:        default_value,\n                value:                value.nil? ? default_value : value,\n              }\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/process_tags.cr",
    "content": "# :nodoc:\n#\n# Normalizes service tags and populates TAG_HASH.\n# Runs after RegisterServices and ProcessAutoconfigureAnnotations.\nmodule Athena::DependencyInjection::ServiceContainer::ProcessTags\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          SERVICE_HASH.each do |service_id, definition|\n            klass = definition[\"class\"]\n            normalized_tags = {} of Nil => Nil\n\n            (definition[\"tags\"] || [] of Nil).each do |tag|\n              name, attributes = if tag.is_a?(StringLiteral)\n                                   {tag, {} of Nil => Nil}\n                                 elsif tag.is_a?(Path)\n                                   {tag.resolve.id.stringify, {} of Nil => Nil}\n                                 elsif tag.is_a?(NamedTupleLiteral) || tag.is_a?(HashLiteral)\n                                   unless tag[:name]\n                                     tag.raise \"Failed to register service '#{service_id.id}' (#{klass}). Tag must have a name.\"\n                                   end\n\n                                   # Resolve a constant to its value if used as a tag name\n                                   if tag[\"name\"].is_a? Path\n                                     tag[\"name\"] = tag[\"name\"].resolve\n                                   end\n\n                                   # TODO: Replace this with `#delete` if/when it's ever released\n                                   # https://github.com/crystal-lang/crystal/pull/9837\n                                   attributes = {} of Nil => Nil\n\n                                   tag.each do |k, v|\n                                     attributes[k.id.stringify] = v unless k.id.stringify == \"name\"\n                                   end\n\n                                   {tag[\"name\"], attributes}\n                                 else\n                                   tag.raise \"Tag must be a 'StringLiteral' or 'NamedTupleLiteral', got '#{tag.class_name.id}'.\"\n                                 end\n\n              normalized_tags[name] = [] of Nil if normalized_tags[name] == nil\n              normalized_tags[name] << attributes\n              normalized_tags[name] = normalized_tags[name].uniq\n\n              TAG_HASH[name] = [] of Nil if TAG_HASH[name] == nil\n              TAG_HASH[name] << {service_id, attributes}\n              TAG_HASH[name] = TAG_HASH[name].uniq\n            end\n\n            definition[\"tags\"] = normalized_tags\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/register_services.cr",
    "content": "# :nodoc:\n#\n# Automatically registers types with an `ADI::Register` annotation.\nmodule Athena::DependencyInjection::ServiceContainer::RegisterServices\n  macro included\n    macro finished\n      {% verbatim do %}\n        # Register each service in the hash along with some related metadata.\n        {% for klass in Object.all_subclasses.select &.annotation(ADI::Register) %}\n          {% if (annotations = klass.annotations(ADI::Register)) && !annotations.empty? && !klass.abstract? %}\n            # Raise a compile time exception if multiple services are based on this type, and not all of them specify a `name`.\n            {% if annotations.size > 1 && !annotations.all? &.[:name] %}\n              {% klass.raise \"Failed to auto register services for '#{klass}'. Each service must explicitly provide a name when auto registering more than one service based on the same type.\" %}\n            {% end %}\n\n            {% for ann in annotations %}\n              {% ann = ann %}\n              {% klass = klass %}\n\n              # Use the service name defined within the annotation, otherwise fallback on FQN snake cased\n              {% id_key = ann[:name] || klass.name.gsub(/::/, \"_\").underscore %}\n              {% service_id = id_key.is_a?(StringLiteral) ? id_key : id_key.stringify %}\n\n              {%\n                factory = if factory_ann = ann[:factory]\n                            if factory_ann.is_a? StringLiteral\n                              {klass.resolve, factory_ann}\n                            elsif factory_ann.is_a? TupleLiteral\n                              {factory_ann[0].resolve, factory_ann[1]}\n                            end\n                          elsif (class_initializer = klass.class.methods.find(&.annotation(ADI::Inject))) && (class_initializer.name.stringify != \"new\")\n                            # Class methods with ADI::Inject should act as a factory.\n                            # But only those not named `\"new\"`, as that's the default and we can't know about overloads of `initialize` at this point.\n                            {klass.resolve, class_initializer.name.stringify}\n                          else\n                            nil\n                          end\n\n                # Validate the factory method exists and is a class method if one was found.\n                if factory\n                  factory_class, factory_method = factory\n\n                  if factory_class.instance.has_method? factory_method\n                    raise \"Failed to auto register service '#{service_id.id}'. Factory method '#{factory_method.id}' within '#{factory_class}' is an instance method.\"\n                  end\n\n                  unless factory_class.class.has_method? factory_method\n                    raise \"Failed to auto register service '#{service_id.id}'. Factory method '#{factory_method.id}' within '#{factory_class}' does not exist.\"\n                  end\n                end\n              %}\n\n              {% # Store raw tags - will be normalized by ProcessTags pass\n\n tags = ann[\"tags\"] || [] of Nil\n\n unless tags.is_a? ArrayLiteral\n   ann[\"tags\"].raise \"'tags' field of service '#{service_id.id}' must be an 'ArrayLiteral', got '#{tags.class_name.id}'.\"\n end %}\n\n              # Generic services are somewhat coupled to the annotation, so do a check here in addition to those in `ResolveGenerics`.\n              {%\n                if !klass.type_vars.empty? && !ann[\"name\"]\n                  ann.raise \"Failed to auto register service for '#{klass}'. Generic services must explicitly provide a name.\"\n                end\n              %}\n\n              # Apply calls to the underlying service, validating they exist.\n              {%\n                calls = [] of Nil\n\n                if ann_calls = ann[\"calls\"]\n                  ann_calls.each do |call|\n                    method = call[0]\n                    args = call[1] || nil\n\n                    if method.empty?\n                      method.raise \"'calls' field of service '#{service_id.id}': method name cannot be empty.\"\n                    end\n\n                    unless klass.resolve.has_method?(method)\n                      method.raise \"'calls' field of service '#{service_id.id}' (#{klass}): method does not exist.\"\n                    end\n\n                    calls << {method, args || [] of Nil}\n                  end\n                end\n              %}\n\n              {%\n                unless SERVICE_HASH[service_id].nil?\n                  ann.raise \"Failed to auto register service for '#{service_id.id}' (#{klass}). It is already registered.\"\n                end\n              %}\n\n              {%\n                SERVICE_HASH[service_id] = {\n                  class:               klass.resolve,\n                  factory:             factory,\n                  shared:              klass.class?,\n                  calls:               calls,\n                  configurator:        nil,\n                  tags:                tags,\n                  public:              ann[\"public\"] == true,\n                  decorated_service:   nil,\n                  bindings:            {} of Nil => Nil,\n                  generics:            ann.args,\n                  parameters:          {} of Nil => Nil,\n                  referenced_services: [] of Nil,\n                }\n              %}\n            {% end %}\n          {% end %}\n        {% end %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/remove_unused_services.cr",
    "content": "# :nodoc:\n#\n# Removes services that are never used (not public and zero references).\nmodule Athena::DependencyInjection::ServiceContainer::RemoveUnusedServices\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          SERVICE_REFERENCES.each do |service_id, ref_info|\n            # Only remove if: not public AND no references\n            if ref_info[\"public\"] == false && ref_info[\"count\"] == 0\n              SERVICE_HASH[service_id] = nil\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/resolve_parameter_placeholders.cr",
    "content": "# :nodoc:\nmodule Athena::DependencyInjection::ServiceContainer::ResolveParameterPlaceholders\n  macro included\n    macro finished\n      {% verbatim do %}\n\n        # Resolves `%parameter%` placeholders within configuration values.\n        # E.g. `\"https://%app.domain%/\"` => `\"https://example.com/\"`.\n        #\n        # It is assumed that any user added parameters via another module have already happened.\n        # Parameters added after this module will not be resolved.\n        #\n        # ## Processing strategy\n        #\n        # `to_process` is an array of `{key, value, collection, stack}` tuples. Since arrays are reference types,\n        # we can push new items while iterating to achieve pseudo-recursion without actual recursion (which macros don't support).\n        #\n        # Each tuple tracks:\n        # - `key`: the key within the collection to update\n        # - `value`: the current value to inspect/resolve\n        # - `collection`: the parent collection (CONFIG, a sub-hash, etc.) so we can write back resolved values\n        # - `stack`: path segments for error messages (e.g., `[\"parameters\", \"app.name\"]`)\n        #\n        # ## Supported value types\n        #\n        # * `StringLiteral` containing `%%` (escaped `%`) or `%param.name%` placeholders\n        # * `HashLiteral` — each value is checked for placeholders\n        #   * NOTE: NamedTuple literals are _NOT_ supported as a terminal value, use a HashLiteral instead\n        # * `ArrayLiteral`/`TupleLiteral` — each element is checked for placeholders\n        # * `NamedTupleLiteral` — recursively expanded into `to_process` for its children\n        #\n        # ## Placeholder resolution\n        #\n        # `StringLiteral#gsub` with a block replaces each `%param%` with its resolved value and `%%` with a literal `%`.\n        #\n        # When the entire string is a single placeholder (e.g., `\"%app.debug%\"`), the resolved value is looked up\n        # directly from CONFIG rather than using the gsub result. This is critical for two reasons:\n        # 1. It preserves non-string types (a `BoolLiteral` stays a `BoolLiteral`, not `\"false\"`)\n        # 2. It preserves reference semantics for collections — if `%app.array%` resolves to an `ArrayLiteral`\n        #    whose elements haven't been resolved yet, keeping the reference means those elements will be\n        #    updated in-place when they're resolved later in the loop.\n        #\n        # If a resolved value still contains placeholders (e.g., because it references another parameter that\n        # hasn't been resolved yet), it is pushed back into `to_process` for another pass.\n        #\n        # For hash/array values, the re-process entry pushes the whole sub-collection (`h[k]`) as the value,\n        # which matches the assignment path (`h[k][sk]` / `h[k][a_idx]`) minus the sub-key/index.\n\n        {%\n          to_process = CONFIG.to_a.map { |tup| {tup[0], tup[1], CONFIG, [tup[0]]} }\n\n          to_process.each do |(k, v, h, stack)|\n            if v.is_a?(NamedTupleLiteral)\n              v.to_a.each do |(sk, sv)|\n                to_process << {sk, sv, v, stack + [sk]}\n              end\n            else\n              if v.is_a?(StringLiteral) && v =~ /%%|%([^%\\s]++)%/\n                # gsub replaces each %param% with its resolved value, and %% with a literal %.\n                # matches[1] is the captured parameter name, or nil for %% matches.\n                new_value = v.gsub /%%|%([^%\\s]++)%/ do |str, matches|\n                  if param_name = matches[1]\n                    resolved_value = CONFIG[\"parameters\"][param_name]\n                    if resolved_value == nil\n                      path = \"#{stack[0]}\"\n\n                      stack[1..].each do |p|\n                        path += \"[#{p}]\"\n                      end\n\n                      param_name.raise \"#{stack[0] == \"parameters\" ? \"Parameter\".id : \"Configuration value\".id} '#{path.id}' referenced unknown parameter '#{param_name.id}'.\"\n                    end\n\n                    # gsub always returns a StringLiteral, so non-string values must be stringified here.\n                    # The actual type is preserved below for single-placeholder values.\n                    if resolved_value.is_a?(StringLiteral)\n                      resolved_value\n                    else\n                      resolved_value.stringify\n                    end\n                  else\n                    '%'\n                  end\n                end\n\n                # When the entire value is a single placeholder (e.g., \"%app.debug%\"), replace the gsub\n                # result with a direct lookup. This preserves non-string types (BoolLiteral, NumberLiteral,\n                # etc.) and, critically, reference semantics for collections — an ArrayLiteral whose elements\n                # haven't been resolved yet will be updated in-place when the loop processes them later.\n                if v =~ /^%([^%\\s]++)%$/\n                  new_value = CONFIG[\"parameters\"][v.gsub(/%/, \"\")]\n                end\n\n                # If fully resolved, assign it. Otherwise push back for another pass.\n                if !new_value.is_a?(StringLiteral) || (new_value.is_a?(StringLiteral) && !(new_value =~ /%%|%([^%\\s]++)%/))\n                  h[k] = new_value\n                else\n                  to_process << {k, new_value, h, stack}\n                end\n              elsif v.is_a?(HashLiteral)\n                # Same placeholder resolution as above, applied to each hash value.\n                v.each do |sk, sv|\n                  if sv.is_a?(StringLiteral) && sv =~ /%%|%([^%\\s]++)%/\n                    new_value = sv.gsub /%%|%([^%\\s]++)%/ do |str, matches|\n                      if param_name = matches[1]\n                        resolved_value = CONFIG[\"parameters\"][param_name]\n                        if resolved_value == nil\n                          path = \"#{stack[0]}\"\n\n                          stack[1..].each do |p|\n                            path += \"[#{p}]\"\n                          end\n\n                          param_name.raise \"#{stack[0] == \"parameters\" ? \"Parameter\".id : \"Configuration value\".id} '#{path.id}[#{sk}]' referenced unknown parameter '#{param_name.id}'.\"\n                        end\n\n                        if resolved_value.is_a?(StringLiteral)\n                          resolved_value\n                        else\n                          resolved_value.stringify\n                        end\n                      else\n                        '%'\n                      end\n                    end\n\n                    # See single-placeholder comment above — same type/reference preservation applies.\n                    if sv =~ /^%([^%\\s]++)%$/\n                      new_value = CONFIG[\"parameters\"][sv.gsub(/%/, \"\")]\n                    end\n\n                    if !new_value.is_a?(StringLiteral) || (new_value.is_a?(StringLiteral) && !(new_value =~ /%%|%([^%\\s]++)%/))\n                      h[k][sk] = new_value\n                    else\n                      # Re-process the whole hash, not just the single value, since h[k][sk] is the assignment path.\n                      to_process << {k, h[k], h, stack}\n                    end\n                  end\n                end\n              elsif v.is_a?(ArrayLiteral) || v.is_a?(TupleLiteral)\n                # Same placeholder resolution as above, applied to each array/tuple element.\n                v.each_with_index do |av, a_idx|\n                  if av.is_a?(StringLiteral) && av =~ /%%|%([^%\\s]++)%/\n                    new_value = av.gsub /%%|%([^%\\s]++)%/ do |str, matches|\n                      if param_name = matches[1]\n                        resolved_value = CONFIG[\"parameters\"][param_name]\n                        if resolved_value == nil\n                          path = \"#{stack[0]}\"\n\n                          stack[1..].each do |p|\n                            path += \"[#{p}]\"\n                          end\n\n                          param_name.raise \"#{stack[0] == \"parameters\" ? \"Parameter\".id : \"Configuration value\".id} '#{path.id}[#{a_idx}]' referenced unknown parameter '#{param_name.id}'.\"\n                        end\n\n                        if resolved_value.is_a?(StringLiteral)\n                          resolved_value\n                        else\n                          resolved_value.stringify\n                        end\n                      else\n                        '%'\n                      end\n                    end\n\n                    # See single-placeholder comment above — same type/reference preservation applies.\n                    if av =~ /^%([^%\\s]++)%$/\n                      new_value = CONFIG[\"parameters\"][av.gsub(/%/, \"\")]\n                    end\n\n                    if !new_value.is_a?(StringLiteral) || (new_value.is_a?(StringLiteral) && !(new_value =~ /%%|%([^%\\s]++)%/))\n                      h[k][a_idx] = new_value\n                    else\n                      to_process << {k, h[k], h, [] of Nil}\n                    end\n                  end\n                end\n              end\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/resolve_tagged_iterators.cr",
    "content": "# :nodoc:\n#\n# Sets the value of parameters with the `ADI::TaggedIterator` annotation automatically.\n# See also DefineTaggedIterators for how `Iterator` types are handled.\nmodule Athena::DependencyInjection::ServiceContainer::ResolveTaggedIterators\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          iterator_service_map = {} of Nil => Nil\n\n          SERVICE_HASH.each do |service_id, definition|\n            definition[\"parameters\"].each do |_, param|\n              if ann = param[\"declaration\"].annotation ADI::TaggedIterator\n                param_type = param[\"resolved_restriction\"]\n                base_collection_type = param_type.name(generic_args: false).stringify\n\n                unless {\"Enumerable\", \"Iterator\", \"Indexable\"}.includes? base_collection_type\n                  param[\"declaration\"].raise <<-TEXT\n                  Failed to register service '#{service_id.id}' (#{definition[\"class\"]}). \\\n                  Collection type must be one of 'Indexable', 'Iterator', or 'Enumerable'. Got '#{param_type.name(generic_args: false).id}'.\n                  TEXT\n                end\n\n                enumerable_type = param_type.type_vars.first\n\n                # If no tag name was explicitly provided, assume its the FQN of the enumerable type\n                tag_name = if name = ann[0]\n                             if name.is_a?(Path)\n                               name.resolve\n                             else\n                               name\n                             end\n                           elsif enumerable_type.union?\n                             ann.raise \"Unable to support unions\"\n                           else\n                             enumerable_type.stringify\n                           end\n\n                iterator_service_map[iterator_id = \"#{service_id.id}_iterator\"] = {\n                  type:     enumerable_type,\n                  services: (TAG_HASH[tag_name] || [] of Nil)\n                    .sort_by { |(_tmp, attributes)| -(attributes[\"priority\"] || 0) }\n                    .map(&.first.id),\n                }\n\n                param[\"value\"] = \"@#{iterator_id.id}\"\n              end\n            end\n          end\n        %}\n\n        # Define iterator types\n        {% for name, metadata in iterator_service_map %}\n          ADI.service_iterator({{name}}, {{metadata[\"services\"]}})\n        {% end %}\n\n        # Register iterator services\n        {%\n          iterator_service_map.each do |name, metadata|\n            SERVICE_HASH[name] = {\n              class:               name.camelcase,\n              bindings:            {} of Nil => Nil,\n              generics:            [metadata[\"type\"], metadata[\"services\"].size] of Nil,\n              calls:               [] of Nil,\n              referenced_services: metadata[\"services\"],\n              parameters:          {\n                container: {value: \"self\".id},\n              },\n            }\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/resolve_values.cr",
    "content": "# :nodoc:\nmodule Athena::DependencyInjection::ServiceContainer::ResolveValues\n  macro included\n    macro finished\n      {% verbatim do %}\n\n          # Resolves the constructor arguments for each service in the container.\n          # The values should be provided in priority order:\n          #\n          # 1. Explicit value on annotation => _id\n          # 2. Bindings (typed and untyped) => `ADI.bind` > `ADI::Autoconfigure`\n          # 3. Autowire => By direct type, or parameter name\n          # 4. Service Alias => Service registered with `AsAlias` of a specific interface\n          # 5. Default value => some_value : Int32 = 123\n          # 6. Nilable Type => nil\n\n        {%\n          SERVICE_HASH.each do |service_id, definition|\n            # Use a dedicated array var such that we can use the pseudo recursion trick\n            parameters = definition[\"parameters\"].map { |_tmp, param| {param[\"value\"], param, nil} }\n\n            parameters.each do |(unresolved_value, param, reference)|\n              # Parameter reference\n              if unresolved_value.is_a?(StringLiteral) && unresolved_value.starts_with?('%') && unresolved_value.ends_with?('%')\n                resolved_value = CONFIG[\"parameters\"][unresolved_value[1..-2]]\n\n                # Service reference\n              elsif unresolved_value.is_a?(StringLiteral) && unresolved_value.starts_with?('@')\n                service_name = unresolved_value[1..-1]\n\n                # Resolve the alias ID to its underlying ID\n                # For explicit @service_name references, take the first alias entry\n                if aliases = ALIASES[service_name]\n                  service_name = aliases.first[\"id\"]\n                end\n\n                if SERVICE_HASH[service_name].nil?\n                  unresolved_value.raise \"Service '#{service_id.id}' (#{definition[\"class\"]}) references undefined service '#{service_name.id}'.\"\n                end\n\n                resolved_value = service_name.id\n\n                # Tagged services\n              elsif unresolved_value.is_a?(StringLiteral) && unresolved_value.starts_with?('!')\n                tag_name = unresolved_value[1..]\n\n                # Sort based on tag priority.  Services without a priority will be last in order of definition\n                tagged_services = (TAG_HASH[tag_name] || [] of Nil).sort_by { |(_tmp, attributes)| -(attributes[\"priority\"] || 0) }\n\n                if param[\"resolved_restriction\"].type_vars.first.resolve < ADI::Proxy\n                  # Track proxy references to ensure getters are generated\n                  definition[\"referenced_services\"] = [] of Nil unless definition[\"referenced_services\"]\n                  tagged_services.each do |(id, attributes)|\n                    definition[\"referenced_services\"] << id\n                  end\n                  tagged_services = tagged_services.map do |(id, attributes)|\n                    {\"ADI::Proxy.new(#{id}, ->#{id.id})\".id}\n                  end\n                end\n\n                resolved_value = tagged_services.map &.first.id\n\n                # Array, could contain nested references\n              elsif unresolved_value.is_a?(ArrayLiteral) || unresolved_value.is_a?(TupleLiteral)\n                # Pseudo recurse over each array element\n                resolved_value = unresolved_value\n\n                unresolved_value.each_with_index do |v, idx|\n                  parameters << {v, param, {type: \"array\", key: idx, value: resolved_value}}\n                end\n\n                # Hash, could contain nested references\n              elsif unresolved_value.is_a?(HashLiteral)\n                # Pseudo recurse over each key/value pair\n                resolved_value = unresolved_value\n\n                unresolved_value.each do |k, v|\n                  parameters << {v, param, {type: \"hash\", key: k, value: resolved_value}}\n                end\n                # Bound value, only apply if value was not already resolved\n                # Value is re-processed to resolve the underlying value, use the reference value to know not to do it again\n              elsif (bv = definition[\"bindings\"][param[\"name\"].id]) != nil && !reference\n                resolved_value = nil\n\n                parameters << {bv, param, {type: \"scalar\"}}\n\n                # Scalar value\n              else\n                resolved_value = unresolved_value\n              end\n\n              if reference && (\"array\" == reference[\"type\"] || \"hash\" == reference[\"type\"])\n                reference[\"value\"][reference[:key]] = resolved_value\n              else\n                param[\"value\"] = resolved_value\n              end\n\n              # Clear temp vars to avoid confusion\n              resolved_value = nil\n              unresolved_value = nil\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/validate_arguments.cr",
    "content": "# :nodoc:\n#\n# Compiler pass that validates user-provided configuration against extension schemas.\n#\n# Uses a queue-based approach (values_to_resolve) to handle nested validation:\n#   - Start with the top-level config value\n#   - For array_of/map_of/object_of, queue each element/member for validation\n#   - Process until queue is empty\n#\n# Key concepts:\n#   - prop_type: Can be TypeNode (for type checking) or NamedTupleLiteral (member map for nested objects)\n#   - schema_member_map_prop_cache: Prevents re-processing the same property's members (since queued items share the same `prop`, we only want to expand members once)\n#   - map_of properties identified by `prop[\"type\"] <= Hash`\n#   - Member entries can be TypeDeclaration (simple) or NamedTupleLiteral (object_schema ref)\nmodule Athena::DependencyInjection::ServiceContainer::ValidateArguments\n  macro included\n    macro finished\n      {% verbatim do %}\n        # Validate the user provided configuration against the defined schema first,\n        # before validating service arguments. This ensures config validation errors\n        # are reported before service parameter errors that may result from bad config.\n        {%\n          _nil = nil\n\n          # This is mostly copied from `MergeExtensionConfig` code,\n          # ideally would be nice to be able to not share state like this but :shrug: this works for now.\n          #\n          # That would be easiest with some macros defs to share the macro logic of building out this map.\n          EXTENSION_SCHEMA_PROPERTIES_MAP.each do |ext_name, schema_properties|\n            user_provided_extension_config = CONFIG[ext_name]\n\n            schema_member_map_prop_cache = {} of Nil => Nil\n\n            schema_properties.each do |(prop, ext_path)|\n              user_provided_extension_config_for_current_property = user_provided_extension_config\n\n              ext_path.each do |p|\n                user_provided_extension_config_for_current_property = user_provided_extension_config_for_current_property[p] if user_provided_extension_config_for_current_property\n              end\n\n              # If this schema property maps to an actual property, and the user provided some configuration value for that property, move onto validating the provided value's validity.\n              # Otherwise, if that property was not provided, and is required, raise an exception.\n\n              if prop\n                # If the configuration property was not provided and is required, throw an error\n                if !user_provided_extension_config_for_current_property\n                  if prop[\"default\"].is_a?(Nop) && !prop[\"type\"].resolve.nilable?\n                    path = [ext_name]\n\n                    unless ext_path.empty?\n                      ext_path.each do |p|\n                        path << if p.is_a?(NumberLiteral)\n                          \"[#{p}]\"\n                        else\n                          \"#{p}\"\n                        end\n                      end\n                    end\n\n                    prop[\"root\"].raise \"Required configuration property '#{path.join('.').id}.#{prop[\"name\"]} : #{prop[\"type\"]}' must be provided.\"\n                  end\n                else\n                  if (config_value = user_provided_extension_config_for_current_property[prop[\"name\"]]) != nil\n                    config_value = config_value.is_a?(Path) ? config_value.resolve : config_value\n\n                    # Tuple of:\n                    # 0 - type of the property in the schema\n                    # 1 - the value\n                    # 2 - an array representing the path to this property in the schema\n                    values_to_resolve = [{prop[\"type\"], config_value, ext_path + [prop[\"name\"]]}]\n\n                    values_to_resolve.each_with_index do |(prop_type, cfv, stack), idx|\n                      resolved_type = if cfv.nil?\n                                        Nil\n                                      elsif cfv.is_a?(BoolLiteral)\n                                        Bool\n                                      elsif cfv.is_a?(StringLiteral)\n                                        String\n                                      elsif cfv.is_a?(SymbolLiteral)\n                                        Symbol\n                                      elsif cfv.is_a?(RegexLiteral)\n                                        Regex\n                                      elsif cfv.is_a?(ArrayLiteral)\n                                        # Because each value to resolve has the same `prop`, we only want to process the prop's members once.\n                                        # Otherwise next iterations cfv will be correct, but the prop_type will be a named tuple literal.\n                                        if schema_member_map_prop_cache[prop[\"name\"]] == nil && (member_map = prop[\"members\"])\n                                          schema_member_map_prop_cache[prop[\"name\"]] = true\n                                          cfv.each_with_index do |v, v_idx|\n                                            values_to_resolve << {member_map, v, stack + [v_idx]}\n                                          end\n\n                                          Array\n                                        else\n                                          # If the type is a union, extract the first non-nilable type.\n                                          # Then fallback on the type of the property if no type could be extracted/was provided\n                                          non_nilable_prop_type = prop_type.union? ? prop_type.union_types.reject(&.nilable?).first : prop_type\n                                          array_type = (cfv.of || cfv.type) || non_nilable_prop_type.type_vars.first\n\n                                          cfv.each_with_index do |v, v_idx|\n                                            values_to_resolve << {array_type.resolve, v, stack + [v_idx]}\n                                          end\n\n                                          parse_type(\"Array(#{array_type})\").resolve\n                                        end\n                                      elsif cfv.is_a?(NumberLiteral)\n                                        kind = cfv.kind\n\n                                        if kind.starts_with? 'i'\n                                          parse_type(\"Int#{kind[1..].id}\").resolve\n                                        elsif kind.starts_with? 'u'\n                                          parse_type(\"UInt#{kind[1..].id}\").resolve\n                                        elsif kind.starts_with? 'f'\n                                          parse_type(\"Float#{kind[1..].id}\").resolve\n                                        else\n                                          cfv.raise \"BUG: Unexpected number literal value\"\n                                        end\n                                      elsif cfv.is_a?(TypeNode)\n                                        cfv\n                                      elsif cfv.is_a?(NamedTupleLiteral)\n                                        # NamedTupleLiteral handles: map_of values, object_of values, and inline NamedTuples.\n                                        #\n                                        # Check if this is a map_of property (type is Hash and has members)\n                                        if prop[\"type\"] <= Hash && schema_member_map_prop_cache[prop[\"name\"]] == nil && (member_map = prop[\"members\"])\n                                          schema_member_map_prop_cache[prop[\"name\"]] = true\n                                          cfv.each do |hash_key, v|\n                                            if hash_key != \"__nil\"\n                                              values_to_resolve << {member_map, v, stack + [hash_key]}\n                                            end\n                                          end\n\n                                          Hash\n                                        else\n                                          # Because each value to resolve has the same `prop`, we only want to process the prop's members once.\n                                          # Otherwise next iterations cfv will be correct, but the prop_type will be a named tuple literal.\n                                          if schema_member_map_prop_cache[prop[\"name\"]] == nil && (member_map = prop[\"members\"])\n                                            schema_member_map_prop_cache[prop[\"name\"]] = true\n                                            prop_type = member_map\n                                          end\n\n                                          cfv.each do |k, v|\n                                            nt_key_type = prop_type[k]\n\n                                            if nt_key_type == nil && k != \"__nil\"\n                                              path = \"#{stack[0]}\"\n\n                                              stack[1..].each do |p|\n                                                path += if p.is_a?(NumberLiteral)\n                                                          \"[#{p}]\"\n                                                        else\n                                                          \".#{p}\"\n                                                        end\n                                              end\n\n                                              # Filter out internal __nil key for cleaner error message\n                                              display_type = \"{#{prop_type.keys.reject { |dk| dk.stringify == \"__nil\" }.map { |dk| \"#{dk}: #{prop_type[dk]}\" }.join(\", \").id}}\"\n                                              cfv.raise \"Expected configuration value '#{ext_name.id}.#{path.id}' to be a '#{display_type.id}', but encountered unexpected key '#{k}'.\"\n                                            elsif k == \"__nil\"\n                                              # no-op\n                                            else\n                                              type = if nt_key_type.is_a?(TypeDeclaration)\n                                                       nt_key_type.type.resolve\n                                                     elsif nt_key_type.is_a?(NamedTupleLiteral)\n                                                       # Nested object_schema reference - pass the members map\n                                                       nt_key_type[\"members\"]\n                                                     else\n                                                       nt_key_type.resolve\n                                                     end\n\n                                              values_to_resolve << {type, v, stack + [k]}\n                                            end\n                                          end\n\n                                          missing_keys = prop_type.keys.reject { |k| k.stringify == \"__nil\" } - cfv.keys\n\n                                          unless missing_keys.empty?\n                                            missing_keys.each do |mk|\n                                              mt = prop_type[mk]\n\n                                              can_be_missing = if mt.is_a?(TypeNode)\n                                                                 mt.nilable?\n                                                               elsif mt.is_a?(TypeDeclaration)\n                                                                 mt.type.resolve.nilable? || !mt.value.is_a?(Nop)\n                                                               elsif mt.is_a?(NamedTupleLiteral)\n                                                                 # For nested object_schema references\n                                                                 !mt[\"value\"].is_a?(Nop)\n                                                               else\n                                                                 false\n                                                               end\n\n                                              unless can_be_missing\n                                                path = \"#{stack[0]}\"\n\n                                                stack[1..].each do |p|\n                                                  path += if p.is_a?(NumberLiteral)\n                                                            \"[#{p}]\"\n                                                          else\n                                                            \".#{p}\"\n                                                          end\n                                                end\n\n                                                type = prop_type[mk]\n                                                type = type.is_a?(TypeDeclaration) ? type.type : type\n\n                                                cfv.raise \"Configuration value '#{ext_name.id}.#{path.id}' is missing required value for '#{mk}' of type '#{type}'.\"\n                                              end\n                                            end\n                                          end\n\n                                          nil\n                                        end\n                                      end\n\n                      if resolved_type\n                        # Handles outer most typing issues.\n                        # Skip type check when prop_type is a NamedTupleLiteral (member map for nested validation)\n                        if resolved_type.is_a?(TypeNode) && prop_type.is_a?(TypeNode) && !(resolved_type <= prop_type)\n                          path = \"#{stack[0]}\"\n\n                          stack[1..].each do |p|\n                            path += if p.is_a?(NumberLiteral)\n                                      \"[#{p}]\"\n                                    else\n                                      \".#{p}\"\n                                    end\n                          end\n\n                          cfv.raise \"Expected configuration value '#{ext_name.id}.#{path.id}' to be a '#{prop_type}', but got '#{resolved_type}'.\"\n                        end\n                      end\n                    end\n                  elsif prop[\"default\"].is_a?(Nop) && !prop[\"type\"].resolve.nilable?\n                    path = [ext_name]\n\n                    unless ext_path.empty?\n                      ext_path.each do |p|\n                        path << if p.is_a?(NumberLiteral)\n                          \"[#{p}]\"\n                        else\n                          \"#{p}\"\n                        end\n                      end\n                    end\n\n                    prop[\"root\"].raise \"Required configuration property '#{path.join('.').id}.#{prop[\"name\"]} : #{prop[\"type\"]}' must be provided.\"\n                  end\n                end\n              end\n            end\n          end\n        %}\n\n        # Validate the arguments for each service\n        {%\n          SERVICE_HASH.each do |service_id, definition|\n            definition[\"parameters\"].each do |_, param|\n              error = nil\n\n              # Type of the resolved argument matches the method param restriction\n              if param[\"value\"] != nil\n                value = param[\"value\"]\n                restriction = param[\"resolved_restriction\"]\n\n                if restriction && restriction <= String && (!value.is_a?(StringLiteral) && !value.is_a?(Call))\n                  type_name = if value.is_a?(NumberLiteral)\n                                kind = value.kind\n                                if kind.starts_with? 'i'\n                                  \"Int#{kind[1..].id}\"\n                                elsif kind.starts_with? 'u'\n                                  \"UInt#{kind[1..].id}\"\n                                elsif kind.starts_with? 'f'\n                                  \"Float#{kind[1..].id}\"\n                                else\n                                  value.class_name\n                                end\n                              elsif value.is_a?(BoolLiteral)\n                                \"Bool\"\n                              elsif value.is_a?(SymbolLiteral)\n                                \"Symbol\"\n                              else\n                                value.class_name\n                              end\n                  error = \"Service '#{service_id.id}' (#{definition[\"class\"]}): parameter expects a 'String' but got '#{type_name.id}'.\"\n                end\n\n                if (s = SERVICE_HASH[value.stringify]) && (klass = s[\"class\"]).is_a?(TypeNode) && !(klass <= restriction)\n                  error = \"Service '#{service_id.id}' (#{definition[\"class\"]}): parameter expects '#{restriction}' but\" \\\n                          \" the resolved service '#{value.id}' is of type '#{s[\"class\"].id}'.\"\n                end\n              elsif !param[\"resolved_restriction\"].nilable?\n                error = \"Failed to resolve argument for service '#{service_id.id}' (#{definition[\"class\"]}).\"\n              end\n\n              if error\n                param[\"declaration\"].raise error\n              end\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/compiler_passes/validate_generics.cr",
    "content": "# :nodoc:\n#\n# Validate generic services.\nmodule Athena::DependencyInjection::ServiceContainer::ValidateGenerics\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          SERVICE_HASH.each do |service_id, definition|\n            klass = definition[\"class\"]\n            generics = definition[\"generics\"]\n\n            if !klass.type_vars.empty? && generics.empty?\n              klass.raise \"Failed to register service '#{service_id.id}'. Generic services must provide the types to use via the 'generics' field.\"\n            end\n\n            if klass.type_vars.size != generics.size\n              klass.raise \"Failed to register service '#{service_id.id}'. Expected #{klass.type_vars.size} generics types got #{generics.size}.\"\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/extension.cr",
    "content": "# Used to denote a module as an extension schema.\n# Defines the configuration properties exposed to compile passes added via `ADI.add_compiler_pass`.\n# Schemas must be registered via `ADI.register_extension`.\n#\n# EXPERIMENTAL: This feature is intended for internal/advanced use and, for now, comes with limited public documentation.\n#\n# ## Member Markup\n#\n# `#object_of` and `#array_of` support a special doc comment markup that can be used to better document each member of the objects.\n# The markup consists of `---` to denote the start and end of the block.\n# `>>` denotes the start of the docs for a specific property.\n# The name of the property followed by a `:` should directly follow.\n# From there, any text will be attributed to that property, until the next `>>` or `---`.\n# Not all properties need to be included.\n#\n# For example:\n#\n# ```\n# module Schema\n#   include ADI::Extension::Schema\n#\n#   # Represents a connection to the database.\n#   #\n#   # ---\n#   # >>username: The username, should be set to `admin` for elevated privileges.\n#   # >>port: Defaults to the default PG port.\n#   # ---\n#   object_of? connection, username : String, password : String, port : Int32 = 5432\n# end\n# ```\n#\n# WARNING: The custom markup is only supported when using `mkdocs` with some custom templates.\nmodule Athena::DependencyInjection::Extension::Schema\n  macro included\n    # :nodoc:\n    #\n    # Array of schema property definitions. Each entry is a NamedTupleLiteral with:\n    #   - name: property name\n    #   - type: Crystal type (e.g., String, Int32, Hash for map_of)\n    #   - default: default value (Nop if required)\n    #   - root: root property name for error messages\n    #   - members: (optional) for array_of/object_of/map_of, a NamedTupleLiteral where:\n    #       - keys are member names\n    #       - values are either TypeDeclaration (simple members) or NamedTupleLiteral (object_schema references, with keys: type, value, members)\n    #   - global: whether the type uses global namespace (::)\n    OPTIONS = [] of Nil\n\n    # :nodoc:\n    #\n    # Registry of reusable object schemas defined via `object_schema`.\n    # Keys are schema names (e.g., \"JwtConfig\"), values are NamedTupleLiterals with:\n    #   - members: member map (same structure as OPTIONS members)\n    #   - doc: documentation string\n    OBJECT_SCHEMAS = {} of Nil => Nil\n\n    # This must be public so its included in docs and mkdocs can access it.\n    CONFIG_DOCS = [] of Nil\n  end\n\n  # Defines a reusable object schema that can be referenced by name in other schema definitions.\n  # This is useful for defining nested object structures or sharing schemas between properties.\n  #\n  # ```\n  # module Schema\n  #   include ADI::Extension::Schema\n  #\n  #   object_schema JwtConfig,\n  #     secret : String,\n  #     algorithm : String = \"hmac.sha256\"\n  #\n  #   map_of hubs,\n  #     url : String,\n  #     jwt : JwtConfig\n  # end\n  # ```\n  #\n  # NOTE: Object schemas must be defined before they are referenced.\n  macro object_schema(name, *members)\n    {%\n      __nil = nil\n\n      doc_string = \"\"\n      member_doc_map = {} of Nil => Nil\n      in_member_docblock = false\n      current_member = nil\n\n      @caller.first.doc.lines.each_with_index do |line, idx|\n        if \"---\" == line\n          in_member_docblock = true\n        elsif in_member_docblock && line.starts_with?(\">>\")\n          member_text = line[2..]\n          colon_idx = nil\n          member_text.chars.each_with_index { |c, i| colon_idx = i if c == ':' && colon_idx == nil }\n          current_member = member_text[0...colon_idx]\n          docs = member_text[(colon_idx + 1)..]\n          member_doc_map[current_member.id.stringify] = \"#{docs.id}\\\\n\"\n        elsif current_member\n          member_doc_map[current_member.id.stringify] += \"#{line.id}\\\\n\"\n        elsif \"---\" == line && in_member_docblock\n          in_member_docblock = false\n          current_member = nil\n        else\n          doc_string += \"#{idx == 0 ? \"\".id : \"\\# \".id}#{line.id}\\n\"\n        end\n      end\n\n      members_string = \"[\"\n      member_map = {__nil: nil}\n\n      members.each_with_index do |m, idx|\n        m.raise \"All members must be `TypeDeclaration`s.\" unless m.is_a? TypeDeclaration\n\n        # Check if this member type references another object_schema\n        member_type = m.type\n        if nested_schema = OBJECT_SCHEMAS[member_type.id.stringify]\n          member_map[m.var.id] = {type: m.type, value: m.value, members: nested_schema[\"members\"]}\n        else\n          member_map[m.var.id] = m\n        end\n\n        if nested = OBJECT_SCHEMAS[member_type.id.stringify]\n          nested_doc = (nested[\"doc\"] || \"\").strip.gsub(/\\n\\# ?/, \"\\\\n\").gsub(/\\n/, \"\\\\n\")\n          member_doc = (member_doc_map[m.var.stringify] || nested_doc).strip.gsub(/\"/, \"\\\\\\\"\")\n          members_string += %({\"name\":\"#{m.var.id}\",\"type\":\"`#{m.type.id}`\",\"default\":\"`#{m.value.id}`\",\"doc\":\"#{member_doc.id}\",\"members\":#{nested[\"members_string\"].id}})\n        else\n          members_string += %({\"name\":\"#{m.var.id}\",\"type\":\"`#{m.type.id}`\",\"default\":\"`#{m.value.id}`\",\"doc\":\"#{(member_doc_map[m.var.stringify] || \"\").strip.strip.gsub(/\"/, \"\\\\\\\"\").id}\"})\n        end\n        members_string += \",\" unless idx == members.size - 1\n      end\n      members_string += \"]\"\n\n      OBJECT_SCHEMAS[name.id.stringify] = {members: member_map, doc: doc_string, members_string: members_string}\n    %}\n  end\n\n  # Defines a schema property via the provided [declaration](https://crystal-lang.org/api/Crystal/Macros/TypeDeclaration.html).\n  # The type may be any primitive Crystal type (String, Bool, Array, Hash, Enum, Number, etc).\n  #\n  # ```\n  # module Schema\n  #   include ADI::Extension::Schema\n  #\n  #   property enabled : Bool = true\n  #   property name : String\n  # end\n  #\n  # ADI.register_extension \"test\", Schema\n  #\n  # ADI.configure({\n  #   test: {\n  #     name: \"Fred\",\n  #   },\n  # })\n  # ```\n  macro property(declaration)\n    {%\n      __nil = nil\n\n      # Special case: Allow using NoReturn to \"inherit\" type from the TypeDeclaration for Array types.\n      # I.e. to make it so you do not have to retype the type if its long/complex\n      default = if declaration.type.resolve <= Array &&\n                   !declaration.value.is_a?(Nop) &&\n                   declaration.value.is_a?(ArrayLiteral) &&\n                   (array_type = ((declaration.value.of || declaration.value.type))) &&\n                   !array_type.is_a?(Nop) &&\n                   array_type.resolve == NoReturn.resolve\n                  \"#{declaration.type.id}.new\".id\n                else\n                  declaration.value\n                end\n\n      OPTIONS << {name: declaration.var.id, type: declaration.type.resolve, default: default, root: declaration, global: declaration.type.is_a?(Path) && declaration.type.global?}\n      CONFIG_DOCS << %({\"name\":\"#{declaration.var.id}\",\"type\":\"`#{declaration.type.id}`\",\"default\":\"`#{default.id}`\"}).id\n    %}\n\n    # {{ @caller.first.doc_comment }}\n    abstract def {{declaration.var.id}} : {{declaration.type.id}}\n  end\n\n  # Defines a required strictly typed `NamedTupleLiteral` object with the provided *name* and *members*.\n  # The members consist of a variadic list of [declarations](https://crystal-lang.org/api/Crystal/Macros/TypeDeclaration.html), with optional default values.\n  # ```\n  # module Schema\n  #   include ADI::Extension::Schema\n  #\n  #   object_of connection,\n  #     username : String,\n  #     password : String,\n  #     hostname : String = \"localhost\",\n  #     port : Int32 = 5432\n  # end\n  #\n  # ADI.register_extension \"test\", Schema\n  #\n  # ADI.configure({\n  #   test: {\n  #     connection: {username: \"admin\", password: \"abc123\"},\n  #   },\n  # })\n  # ```\n  #\n  # This macro is preferred over a direct `NamedTuple` type as it allows default values to be defined, and for the members to be documented via the special [Member Markup][Athena::DependencyInjection::Extension::Schema--member-markup]\n  macro object_of(name, *members)\n    process_object_of({{name}}, {{members.splat}}, nilable: false)\n  end\n\n  # Same as `#object_of` but makes the object optional, defaulting to `nil`.\n  macro object_of?(name, *members)\n    process_object_of({{name}}, {{members.splat}}, nilable: true)\n  end\n\n  private macro process_object_of(name_or_assign, *members, nilable)\n    {%\n      __nil = nil\n\n      if name_or_assign.is_a?(Assign)\n        name = name_or_assign.target.id\n        default = name_or_assign.value\n      else\n        name = name_or_assign.name\n        default = pp # Hack to ensure the default is a Nop to differentiate it from `nil`\n      end\n\n      doc_string = \"\"\n      member_doc_map = {} of Nil => Nil\n      in_member_docblock = false\n      current_member = nil\n\n      @caller.first.doc.lines.each_with_index do |line, idx|\n        # --- denotes member docblock start/end\n        if \"---\" == line\n          in_member_docblock = true\n\n          # >> denotes start of property docs\n        elsif in_member_docblock && line.starts_with?(\">>\")\n          current_member, docs = line[2..].split(':')\n\n          member_doc_map[current_member.id.stringify] = \"#{docs.id}\\\\n\"\n        elsif current_member\n          member_doc_map[current_member.id.stringify] += \"#{line.id}\\\\n\"\n        elsif \"---\" == line && in_member_docblock\n          in_member_docblock = false\n          current_member = nil\n        else\n          # The line where the docs are added in already have a `#`,\n          # so no need to\n          doc_string += \"#{idx == 0 ? \"\".id : \"\\# \".id}#{line.id}\\n\"\n        end\n      end\n\n      members_string = \"[\"\n      member_map = {__nil: nil}\n      members.each_with_index do |m, idx|\n        m.raise \"All members must be `TypeDeclaration`s.\" unless m.is_a? TypeDeclaration\n\n        # Check if this member type references an object_schema\n        member_type = m.type\n        if nested_schema = OBJECT_SCHEMAS[member_type.id.stringify]\n          member_map[m.var.id] = {type: m.type, value: m.value, members: nested_schema[\"members\"]}\n        else\n          member_map[m.var.id] = m\n        end\n\n        if nested = OBJECT_SCHEMAS[member_type.id.stringify]\n          nested_doc = (nested[\"doc\"] || \"\").strip.gsub(/\\n\\# ?/, \"\\\\n\").gsub(/\\n/, \"\\\\n\")\n          member_doc = (member_doc_map[m.var.stringify] || nested_doc).strip.gsub(/\"/, \"\\\\\\\"\")\n          members_string += %({\"name\":\"#{m.var.id}\",\"type\":\"`#{m.type.id}`\",\"default\":\"`#{m.value.id}`\",\"doc\":\"#{member_doc.id}\",\"members\":#{nested[\"members_string\"].id}})\n        else\n          members_string += %({\"name\":\"#{m.var.id}\",\"type\":\"`#{m.type.id}`\",\"default\":\"`#{m.value.id}`\",\"doc\":\"#{(member_doc_map[m.var.stringify] || \"\").strip.strip.gsub(/\"/, \"\\\\\\\"\").id}\"})\n        end\n        members_string += \",\" unless idx == members.size - 1\n      end\n      members_string += \"]\"\n\n      OPTIONS << {name: name, type: (type = (nilable ? parse_type(\"NamedTuple?\").resolve : NamedTuple)), default: nilable ? nil : default, root: name, members: member_map, global: type.is_a?(Path) && type.global?}\n      CONFIG_DOCS << %({\"name\":\"#{name.id}\",\"type\":\"`#{type.id}`\",\"default\":\"`#{(nilable && default.is_a?(Nop) ? nil : default).id}`\",\"members\":#{members_string.id}}).id\n    %}\n\n    # {{ doc_string.strip.id }}\n    abstract def {{name.id}}\n  end\n\n  # Similar to `#object_of`, but defines an array of objects.\n  # ```\n  # module Schema\n  #   include ADI::Extension::Schema\n  #\n  #   array_of rules,\n  #     path : String,\n  #     value : String\n  # end\n  #\n  # ADI.register_extension \"test\", Schema\n  #\n  # ADI.configure({\n  #   test: {\n  #     rules: [\n  #       {path: \"/foo\", value: \"foo\"},\n  #       {path: \"/bar\", value: \"bar\"},\n  #     ],\n  #   },\n  # })\n  # ```\n  #\n  # If not provided, the property defaults to an empty array.\n  macro array_of(name, *members)\n    process_array_of({{name}}, {{members.splat}}, nilable: false)\n  end\n\n  # Same as `#array_of` but makes the default value of the property `nil`.\n  macro array_of?(name, *members)\n    process_array_of({{name}}, {{members.splat}}, nilable: true)\n  end\n\n  private macro process_array_of(name_or_assign, *members, nilable)\n    {%\n      __nil = nil\n\n      if name_or_assign.is_a?(Assign)\n        name = name_or_assign.target.id\n        default = name_or_assign.value\n      else\n        name = name_or_assign.name\n        default = [] of NoReturn\n      end\n\n      doc_string = \"\"\n      member_doc_map = {} of Nil => Nil\n      in_member_docblock = false\n      current_member = nil\n\n      @caller.first.doc.lines.each_with_index do |line, idx|\n        # --- denotes member docblock start/end\n        if \"---\" == line\n          in_member_docblock = true\n\n          # >> denotes start of property docs\n        elsif in_member_docblock && line.starts_with?(\">>\")\n          current_member, docs = line[2..].split(':')\n\n          member_doc_map[current_member.id.stringify] = \"#{docs.id}\\\\n\"\n        elsif current_member\n          member_doc_map[current_member.id.stringify] += \"#{line.id}\\\\n\"\n        elsif \"---\" == line && in_member_docblock\n          in_member_docblock = false\n          current_member = nil\n        else\n          # The line where the docs are added in already have a `#`,\n          # so no need to\n          doc_string += \"#{idx == 0 ? \"\".id : \"\\# \".id}#{line.id}\\n\"\n        end\n      end\n\n      members_string = \"[\"\n      member_map = {__nil: nil}\n\n      members.each_with_index do |m, idx|\n        m.raise \"All members must be `TypeDeclaration`s.\" unless m.is_a? TypeDeclaration\n\n        # Check if this member type references an object_schema\n        member_type = m.type\n        if nested_schema = OBJECT_SCHEMAS[member_type.id.stringify]\n          member_map[m.var.id] = {type: m.type, value: m.value, members: nested_schema[\"members\"]}\n        else\n          member_map[m.var.id] = m\n        end\n\n        if nested = OBJECT_SCHEMAS[member_type.id.stringify]\n          nested_doc = (nested[\"doc\"] || \"\").strip.gsub(/\\n\\# ?/, \"\\\\n\").gsub(/\\n/, \"\\\\n\")\n          member_doc = (member_doc_map[m.var.stringify] || nested_doc).strip.gsub(/\"/, \"\\\\\\\"\")\n          members_string += %({\"name\":\"#{m.var.id}\",\"type\":\"`#{m.type.id}`\",\"default\":\"`#{m.value.id}`\",\"doc\":\"#{member_doc.id}\",\"members\":#{nested[\"members_string\"].id}})\n        else\n          members_string += %({\"name\":\"#{m.var.id}\",\"type\":\"`#{m.type.id}`\",\"default\":\"`#{m.value.id}`\",\"doc\":\"#{(member_doc_map[m.var.stringify] || \"\").strip.strip.gsub(/\"/, \"\\\\\\\"\").id}\"})\n        end\n        members_string += \",\" unless idx == members.size - 1\n      end\n      members_string += \"]\"\n\n      OPTIONS << {name: name, type: (type = (nilable ? parse_type(\"Array?\").resolve : Array)), default: nilable ? nil : default, root: name, members: member_map, global: type.is_a?(Path) && type.global?}\n      CONFIG_DOCS << %({\"name\":\"#{name.id}\",\"type\":\"`#{type.id}`\",\"default\":\"`#{(nilable && default.empty? ? nil : default).id}`\",\"members\":#{members_string.id}}).id\n    %}\n\n    # {{ doc_string.strip.id }}\n    abstract def {{name.id}}\n  end\n\n  # Defines a map where keys are arbitrary names and values follow a typed object schema.\n  # This is useful for configuration patterns where named entries share a common structure.\n  # ```\n  # module Schema\n  #   include ADI::Extension::Schema\n  #\n  #   map_of hubs,\n  #     url : String,\n  #     port : Int32 = 5432\n  # end\n  #\n  # ADI.register_extension \"test\", Schema\n  #\n  # ADI.configure({\n  #   test: {\n  #     hubs: {\n  #       primary:   {url: \"localhost\"},\n  #       secondary: {url: \"remote\", port: 5433},\n  #     },\n  #   },\n  # })\n  # ```\n  #\n  # If not provided, the property defaults to an empty map.\n  macro map_of(name, *members)\n    process_map_of({{name}}, {{members.splat}}, nilable: false)\n  end\n\n  # Same as `#map_of` but makes the default value of the property `nil`.\n  macro map_of?(name, *members)\n    process_map_of({{name}}, {{members.splat}}, nilable: true)\n  end\n\n  private macro process_map_of(name_or_assign, *members, nilable)\n    {%\n      __nil = nil\n\n      if name_or_assign.is_a?(Assign)\n        name = name_or_assign.target.id\n        default = name_or_assign.value\n      else\n        name = name_or_assign.name\n        default = {__nil: nil}\n      end\n\n      doc_string = \"\"\n      member_doc_map = {} of Nil => Nil\n      in_member_docblock = false\n      current_member = nil\n\n      @caller.first.doc.lines.each_with_index do |line, idx|\n        if \"---\" == line\n          in_member_docblock = true\n        elsif in_member_docblock && line.starts_with?(\">>\")\n          member_text = line[2..]\n          colon_idx = nil\n          member_text.chars.each_with_index { |c, i| colon_idx = i if c == ':' && colon_idx == nil }\n          current_member = member_text[0...colon_idx]\n          docs = member_text[(colon_idx + 1)..]\n          member_doc_map[current_member.id.stringify] = \"#{docs.id}\\\\n\"\n        elsif current_member\n          member_doc_map[current_member.id.stringify] += \"#{line.id}\\\\n\"\n        elsif \"---\" == line && in_member_docblock\n          in_member_docblock = false\n          current_member = nil\n        else\n          doc_string += \"#{idx == 0 ? \"\".id : \"\\# \".id}#{line.id}\\n\"\n        end\n      end\n\n      members_string = \"[\"\n      member_map = {__nil: nil}\n\n      # Build the member_map which describes the schema for each map entry's value.\n      # Each member becomes either:\n      #   - A TypeDeclaration directly (e.g., `url : String`) for simple types\n      #   - A NamedTupleLiteral with {type:, value:, members:} for object_schema references\n      members.each_with_index do |m, idx|\n        m.raise \"All members must be `TypeDeclaration`s.\" unless m.is_a? TypeDeclaration\n\n        # Check if this member type references an object_schema\n        member_type = m.type\n        if nested_schema = OBJECT_SCHEMAS[member_type.id.stringify]\n          member_map[m.var.id] = {type: m.type, value: m.value, members: nested_schema[\"members\"]}\n        else\n          member_map[m.var.id] = m\n        end\n\n        if nested = OBJECT_SCHEMAS[member_type.id.stringify]\n          nested_doc = (nested[\"doc\"] || \"\").strip.gsub(/\\n\\# ?/, \"\\\\n\").gsub(/\\n/, \"\\\\n\")\n          member_doc = (member_doc_map[m.var.stringify] || nested_doc).strip.gsub(/\"/, \"\\\\\\\"\")\n          members_string += %({\"name\":\"#{m.var.id}\",\"type\":\"`#{m.type.id}`\",\"default\":\"`#{m.value.id}`\",\"doc\":\"#{member_doc.id}\",\"members\":#{nested[\"members_string\"].id}})\n        else\n          members_string += %({\"name\":\"#{m.var.id}\",\"type\":\"`#{m.type.id}`\",\"default\":\"`#{m.value.id}`\",\"doc\":\"#{(member_doc_map[m.var.stringify] || \"\").strip.strip.gsub(/\"/, \"\\\\\\\"\").id}\"})\n        end\n        members_string += \",\" unless idx == members.size - 1\n      end\n      members_string += \"]\"\n\n      # map_of uses Hash as type marker (checked in compiler passes via `prop[\"type\"] <= Hash`)\n      OPTIONS << {name: name, type: (type = (nilable ? parse_type(\"Hash?\").resolve : Hash)), default: nilable ? nil : default, root: name, members: member_map, global: type.is_a?(Path) && type.global?}\n      CONFIG_DOCS << %({\"name\":\"#{name.id}\",\"type\":\"`#{type.id}`\",\"default\":\"`#{(nilable && default.keys.reject { |k| k.stringify == \"__nil\" }.empty? ? nil : default).id}`\",\"members\":#{members_string.id}}).id\n    %}\n\n    # {{ doc_string.strip.id }}\n    abstract def {{name.id}}\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/proxy.cr",
    "content": "# Represents a lazily initialized service.\n# See the \"Service Proxies\" section within `ADI::Register`.\nstruct Athena::DependencyInjection::Proxy(O)\n  forward_missing_to self.instance\n\n  # :nodoc:\n  delegate :==, :===, :=~, :hash, :tap, :not_nil!, :dup, :clone, :try, to: self.instance\n\n  # Returns proxied service `O`; instantiating it if it has not already been.\n  getter instance : O { @instantiated = true; @loader.call }\n\n  # Returns the service ID (name) of the proxied service.\n  getter service_id : String\n\n  # Returns whether the proxied service has been instantiated yet.\n  getter? instantiated : Bool = false\n\n  def initialize(@service_id : String, @loader : Proc(O)); end\n\n  # Returns the type of the proxied service.\n  def service_type : O.class\n    O\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/service_container.cr",
    "content": "class Athena::DependencyInjection::ServiceContainer; end\n\nrequire \"./compiler_passes/*\"\n\n# Where the instantiated services live.\n#\n# If a service is public, a getter based on the service's name as well as its type is defined.  Otherwise, services are only available via constructor DI.\n#\n# TODO: Reduce the amount of duplication when [this issue](https://github.com/crystal-lang/crystal/pull/9091) is resolved.\nclass Athena::DependencyInjection::ServiceContainer\n  # :nodoc:\n  #\n  # Define a hash to store services while the container is being built\n  # Key is the ID of the service and the value is another hash containing its arguments, type, etc.\n  SERVICE_HASH = {} of Nil => Nil\n\n  # :nodoc:\n  #\n  # Maps services to their aliases\n  #\n  # Hash(String | TypeNode, Array(NamedTuple(id: String, public: Bool, name: String?)))\n  ALIASES = {} of Nil => Nil\n\n  # Define a hash to store the service ids for each tag.\n  #\n  # Tag Name, service_id, array attributes\n  # Hash(String, Hash(String, Array(NamedTuple)))\n  private TAG_HASH = {} of Nil => Nil\n\n  # :nodoc:\n  EXTENSIONS = {} of Nil => Nil\n\n  # :nodoc:\n  #\n  # Tracks service reference counts for optimization passes.\n  # Populated by AnalyzeServiceReferences pass.\n  # Hash(String, NamedTuple(count: Int32, referenced_by: Array(String), public: Bool))\n  SERVICE_REFERENCES = {} of Nil => Nil\n\n  # :nodoc:\n  #\n  # Holds the compiler pass configuration, including the type of each pass, and the default order the built-in ones execute in.\n  PASS_CONFIG = {\n    # Global pre-optimization modules\n    # Sets up common concepts so that future passes can leverage them\n    before_optimization: {\n      1028 => [\n        # Ensure merged configuration is available\n        MergeConfigs,\n        MergeExtensionConfig,\n      ],\n\n      100 => [\n        NormalizeDefinitions,\n        RegisterServices,\n        ProcessAliases,\n        ProcessAutoconfigureAnnotations,\n        ProcessTags,\n        ProcessParameters,\n        ValidateGenerics,\n      ],\n    },\n\n    # Prepare the services for usage by resolving arguments, parameters, and ensure validity of each service\n    optimization: {\n      0 => [\n        ResolveParameterPlaceholders,\n        ProcessBindings,\n        ProcessAnnotationBindings,\n        AutoWire,\n        ResolveTaggedIterators,\n        ResolveValues,\n        ValidateArguments,\n      ],\n    },\n\n    # Determine what could be removed?\n    before_removing: {\n      # Framework passes (RegisterCommands, RegisterEventListenersPass) run at priority 0; analysis must run after them\n      -50 => [AnalyzeServiceReferences],\n    },\n\n    # Cleanup the container, removing unused services and such\n    removing: {\n        0 => [RemoveUnusedServices],\n      -10 => [InlineServiceDefinitions],\n    },\n\n    # Codegen things that create types/methods within the container instance, such as the getters for each service\n    after_removing: {\n      -100 => [\n        DefineGetters,\n      ],\n    },\n  }\n\n  macro finished\n    {%\n      passes = [] of Nil\n\n      PASS_CONFIG.keys.each do |type|\n        (p = PASS_CONFIG[type]).keys.sort_by { |tk| -tk }.each do |k|\n          p[k].each do |pass|\n            passes << pass\n          end\n        end\n      end\n    %}\n\n    {% for pass in passes %}\n      include {{pass.id}}\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/dependency_injection/src/spec.cr",
    "content": "# A set of testing utilities/types to aid in testing `Athena::DependencyInjection` related types.\n#\n# ### Getting Started\n#\n# Require this module in your `spec_helper.cr` file.\n#\n# ```\n# # This also requires \"spec\".\n# require \"athena-dependency_injection/spec\"\n# ```\nmodule Athena::DependencyInjection::Spec\n  # A mock implementation of `ADI::ServiceContainer` that be used within a testing context to allow for mocking out services without affecting the actual container outside of tests.\n  #\n  # An example of this is when integration testing service based [ATH::Controller][Athena::Framework::Controller]s.\n  # Service dependencies that interact with an external source, like a third party API or a database, should most likely be mocked out.\n  # However your other services should be left as is in order to get the most benefit from the test.\n  #\n  # ## Mocking\n  #\n  # The `ADI::ServiceContainer` is nothing more than a normal Crystal class with some instance variables and methods.\n  # As such, mocking services is as easy as monkey patching `self` with the mocked versions, assuming of course they are of a compatible type.\n  #\n  # Given Crystal's lack of a robust mocking shard, it isn't as straightforward as other languages.\n  # The best way at the moment is either using inheritance or interfaces (modules) to manually create a concrete test class/struct;\n  # with the latter option being preferred as it would work for both structs and classes.\n  #\n  # For example, we can create a mock implementation of a type by extending it:\n  # ```\n  # class MockMyService < MyService\n  #   def get_value\n  #     # Can now just return a static expected value.\n  #     # Test properties/constructor(s) can also be added to make it a bit more generic.\n  #     1234\n  #   end\n  # end\n  # ```\n  #\n  # Because our mock extends `MyService`, it is a compatible type for anything typed as `MyService`.\n  #\n  # Another way to handle mocking is via interfaces (modules).\n  #\n  # ```\n  # module SomeInterface; end\n  #\n  # struct MockMyService\n  #   include SomeInterface\n  # end\n  # ```\n  #\n  # Because our mock implements `SomeInterface`, it is a compatible type for anything typed as `SomeInterface`.\n  #\n  # NOTE: Service mocks do not need to registered as services themselves since they will need to be configured manually.\n  # NOTE: The `type` argument as part of the `ADI::Register` annotation can be used to set the type of a service within the container.\n  # See `ADI::Register@customizing-services-type` for more details.\n  #\n  # ### Dynamic Mocks\n  #\n  # A dynamic mock consists of adding a `setter` to `self` that allows setting the mocked service dynamically at runtime,\n  # while keeping the original up until if/when it is replaced.\n  #\n  # ```\n  # class ADI::Spec::MockableServiceContainer\n  #   # The setter should be nilable as they're lazily initialized within the container.\n  #   setter my_service : MyServiceInterface?\n  # end\n  #\n  # # ...\n  #\n  # # Now the `my_service` service can be replaced at runtime.\n  # mock_container.my_service = MockMyService.new\n  #\n  # # ...\n  # ```\n  #\n  # ### Global Mocks\n  #\n  # Global mocks totally replace the original service, i.e. always return the mocked service.\n  #\n  # ```\n  # class ADI::Spec::MockableServiceContainer\n  #   # Global mocks should use the block based `getter` macro.\n  #   getter my_service : MyServiceInterface { MockMyService.new }\n  # end\n  #\n  # # `MockMyService` will now be injected across the board when using `self`.\n  # # ...\n  # ```\n  #\n  # ### Hybrid Mocks\n  #\n  # Dynamic and Global mocking can also be combined to allow having a default mock, but allow overriding if/when needed.\n  # This can be accomplished by adding both a getter and setter to `self.`\n  #\n  # ```\n  # class ADI::Spec::MockableServiceContainer\n  #   # Hybrid mocks should use the block based `property` macro.\n  #   property my_service : MyServiceInterface { DefaultMockService.new }\n  # end\n  #\n  # # ...\n  #\n  # # `DefaultMockService` will now be injected across the board by when using `self`.\n  #\n  # # But can still be replaced at runtime.\n  # mock_container.my_service = CustomMockService.new\n  #\n  # # ...\n  # ```\n  class MockableServiceContainer < ADI::ServiceContainer; end\nend\n"
  },
  {
    "path": "src/components/dotenv/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/dotenv/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/dotenv/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.2.1] - 2025-11-09\n\n### Fixed\n\n- Fix being unable to call `Athena::Dotenv.load` with a single file ([#609]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.2.1]: https://github.com/athena-framework/dotenv/releases/tag/v0.2.1\n[#609]: https://github.com/athena-framework/athena/pull/609\n\n## [0.2.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/dotenv/releases/tag/v0.2.0\n[#428]: https://github.com/athena-framework/athena/pull/428\n\n## [0.1.3] - 2024-07-31\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.13.0` ([#433]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/dotenv/releases/tag/v0.1.3\n[#433]: https://github.com/athena-framework/athena/pull/433\n\n## [0.1.2] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- Add helper `Athena::Dotenv.load` method to create and load `.env` files in one call ([#363]) (George Dietrich)\n\n### Fixed\n\n- Fixed error parsing ENV vars starting with `_` ([#346]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/dotenv/releases/tag/v0.1.2\n[#346]: https://github.com/athena-framework/athena/pull/346\n[#363]: https://github.com/athena-framework/athena/pull/363\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.1.1] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.1.1]: https://github.com/athena-framework/dotenv/releases/tag/v0.1.1\n\n## [0.1.0] - 2023-04-23\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/dotenv/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/dotenv/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/dotenv/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2023 George Dietrich\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": "src/components/dotenv/README.md",
    "content": "# Dotenv\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/dotenv.svg)](https://github.com/athena-framework/dotenv/releases)\n\nRegisters environment variables from a .env file.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Dotenv).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/dotenv/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.2.0\n\n### Normalization of Exception types\n\nThe namespace exception types live in has changed from `Athena::Dotenv::Exceptions` to `Athena::Dotenv::Exception`.\nAny usages of `dotenv` exception types will need to be updated.\n\nIf using a `rescue` statement with a parent exception type, either from the `console` component or Crystal stdlib, double check it to ensure it'll still rescue what you are expecting it will.\n"
  },
  {
    "path": "src/components/dotenv/docs/README.md",
    "content": "The `Athena::Dotenv` component parses the `.env` files to make ENV vars stored within them accessible.\nUsing [Environment variables](https://en.wikipedia.org/wiki/Environment_variable) (ENV vars) is a common practice to configure options that depend on where the application is run;\nallowing the application's configuration to be de-coupled from its code.\nE.g. anything that changes from one machine to another, such as database credentials.\n\n`.env` files are a convenient way to get the benefits of ENV vars, without taking on the extra complexity of other tools/abstractions until if/when they are needed.\nThe file(s) can be defined at the root of your project for development, or placed next to the binary if running outside of a dev environment.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-dotenv:\n    github: athena-framework/dotenv\n    version: ~> 0.2.0\n```\n\n## Usage\n\nIn most cases all that needs to be done is:\n\n```crystal\nrequire \"athena-dotenv\"\n\n# For most use cases, returns a `Athena::Dotenv` instance.\ndotenv = Athena::Dotenv.load # Loads .env\n\n# Multiple files may also be loaded if needed\nAthena::Dotenv.load \".env\", \".env.local\"\n```\n\n\nFor more complex setups, the [Athena::Dotenv](/Dotenv/top_level/) instance can be manually instantiated.\nE.g. to use the other helper methods such as [#load_environment](</Dotenv/top_level/#Athena::Dotenv#load_environment(path,env_key,default_environment,test_environments,override_existing_vars)>), [#overload](</Dotenv/top_level/#Athena::Dotenv#overload(*)>), or [#populate](</Dotenv/top_level/#Athena::Dotenv#populate(values,override_existing_vars)>)\n\n```crystal\nrequire \"athena-dotenv\"\n\ndotenv = Athena::Dotenv.new\n\n# Overrides existing variables\ndotenv.overload \".env.overrides\"\n\n# Load all files for the current $APP_ENV\n# .env, .env.local, and .env.$APP_ENV.local or .env.$APP_ENV\ndotenv.load_environment \".env\"\n```\n\n[Athena::Dotenv::Exception::Path](/Dotenv/Exception/Path/) error will be raised if the provided file was not found, or is not readable.\n"
  },
  {
    "path": "src/components/dotenv/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Dotenv\nsite_url: https://athenaframework.org/Dotenv/\nrepo_url: https://github.com/athena-framework/dotenv\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-dotenv/src/athena-dotenv.cr\n          source_locations:\n            lib/athena-dotenv: https://github.com/athena-framework/dotenv/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/dotenv/shard.yml",
    "content": "name: athena-dotenv\n\nversion: 0.2.1\n\ncrystal: ~> 1.13\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/dotenv\n\ndocumentation: https://athenaframework.org/Dotenv\n\ndescription: |\n  Registers environment variables from a .env file.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n"
  },
  {
    "path": "src/components/dotenv/spec/athena-dotenv_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct DotEnvTest < ASPEC::TestCase\n  def initialize\n    ENV.clear\n  end\n\n  @[DataProvider(\"env_data\")]\n  def test_parse(data : String, expected : Hash(String, String)) : Nil\n    ENV[\"LOCAL\"] = \"local\"\n    ENV[\"REMOTE\"] = \"remote\"\n\n    Athena::Dotenv.new.parse(data).should eq expected\n  end\n\n  def env_data : Array\n    tests = [\n      # Backslashes\n      {\"FOO=foo\\\\\\\\bar\", {\"FOO\" => \"foo\\\\bar\"}},\n      {\"FOO='foo\\\\\\\\bar'\", {\"FOO\" => \"foo\\\\\\\\bar\"}},\n      {\"FOO=\\\"foo\\\\\\\\bar\\\"\", {\"FOO\" => \"foo\\\\bar\"}},\n\n      # Escaped backslash in front of variable\n      {\"BAR=bar\\nFOO=foo\\\\\\\\$BAR\", {\"BAR\" => \"bar\", \"FOO\" => \"foo\\\\bar\"}},\n      {\"BAR=bar\\nFOO='foo\\\\\\\\$BAR'\", {\"BAR\" => \"bar\", \"FOO\" => \"foo\\\\\\\\$BAR\"}},\n      {\"BAR=bar\\nFOO=\\\"foo\\\\\\\\$BAR\\\"\", {\"BAR\" => \"bar\", \"FOO\" => \"foo\\\\bar\"}},\n\n      {\"FOO=foo\\\\\\\\\\\\$BAR\", {\"FOO\" => \"foo\\\\$BAR\"}},\n      {\"FOO='foo\\\\\\\\\\\\$BAR'\", {\"FOO\" => \"foo\\\\\\\\\\\\$BAR\"}},\n      {\"FOO=\\\"foo\\\\\\\\\\\\$BAR\\\"\", {\"FOO\" => \"foo\\\\$BAR\"}},\n\n      # Spaces\n      {\"FOO=bar\", {\"FOO\" => \"bar\"}},\n      {\" FOO=bar \", {\"FOO\" => \"bar\"}},\n      {\"FOO=\", {\"FOO\" => \"\"}},\n      {\"FOO=\\n\\n\\nBAR=bar\", {\"FOO\" => \"\", \"BAR\" => \"bar\"}},\n      {\"FOO=  \", {\"FOO\" => \"\"}},\n      {\"FOO=\\nBAR=bar\", {\"FOO\" => \"\", \"BAR\" => \"bar\"}},\n\n      # Newlines\n      {\"\\n\\nFOO=bar\\r\\n\\n\", {\"FOO\" => \"bar\"}},\n      {\"FOO=bar\\r\\nBAR=foo\", {\"FOO\" => \"bar\", \"BAR\" => \"foo\"}},\n      {\"FOO=bar\\rBAR=foo\", {\"FOO\" => \"bar\", \"BAR\" => \"foo\"}},\n      {\"FOO=bar\\nBAR=foo\", {\"FOO\" => \"bar\", \"BAR\" => \"foo\"}},\n\n      # Quotes\n      {\"FOO=\\\"bar\\\"\\n\", {\"FOO\" => \"bar\"}},\n      {\"FOO=\\\"bar'foo\\\"\\n\", {\"FOO\" => \"bar'foo\"}},\n      {\"FOO='bar'\\n\", {\"FOO\" => \"bar\"}},\n      {\"FOO='bar\\\"foo'\\n\", {\"FOO\" => \"bar\\\"foo\"}},\n      {\"FOO=\\\"bar\\\\\\\"foo\\\"\\n\", {\"FOO\" => \"bar\\\"foo\"}},\n      {\"FOO=\\\"bar\\nfoo\\\"\", {\"FOO\" => \"bar\\nfoo\"}},\n      {\"FOO=\\\"bar\\\\rfoo\\\"\", {\"FOO\" => \"bar\\rfoo\"}}, # Double quote expands to real `\\r`\n      {\"FOO='bar\\nfoo'\", {\"FOO\" => \"bar\\nfoo\"}},\n      {\"FOO='bar\\\\rfoo'\", {\"FOO\" => \"bar\\\\rfoo\"}}, # Single quotes keep the literal `\\r`\n      {\"FOO='bar\\nfoo'\", {\"FOO\" => \"bar\\nfoo\"}},\n      {\"FOO=\\\" FOO \\\"\", {\"FOO\" => \" FOO \"}},\n      {\"FOO=\\\"  \\\"\", {\"FOO\" => \"  \"}},\n      {\"PATH=\\\"c:\\\\\\\\\\\"\", {\"PATH\" => \"c:\\\\\"}},\n      {\"FOO=\\\"bar\\nfoo\\\"\", {\"FOO\" => \"bar\\nfoo\"}},\n      {\"FOO=BAR\\\\\\\"\", {\"FOO\" => \"BAR\\\"\"}},\n      {\"FOO=BAR\\\\'BAZ\", {\"FOO\" => \"BAR'BAZ\"}},\n      {\"FOO=\\\\\\\"BAR\", {\"FOO\" => \"\\\"BAR\"}},\n\n      # Concatenated values\n      {\"FOO='bar''foo'\\n\", {\"FOO\" => \"barfoo\"}},\n      {\"FOO='bar '' baz'\", {\"FOO\" => \"bar  baz\"}},\n      {\"FOO=bar\\nBAR='baz'\\\"$FOO\\\"\", {\"FOO\" => \"bar\", \"BAR\" => \"bazbar\"}},\n      {\"FOO='bar '\\\\'' baz'\", {\"FOO\" => \"bar ' baz\"}},\n\n      # Comments\n      {\"#FOO=bar\\nBAR=foo\", {\"BAR\" => \"foo\"}},\n      {\"#FOO=bar # Comment\\nBAR=foo\", {\"BAR\" => \"foo\"}},\n      {\"FOO='bar foo' # Comment\", {\"FOO\" => \"bar foo\"}},\n      {\"FOO='bar#foo' # Comment\", {\"FOO\" => \"bar#foo\"}},\n      {\"# Comment\\r\\nFOO=bar\\n# Comment\\nBAR=foo\", {\"FOO\" => \"bar\", \"BAR\" => \"foo\"}},\n      {\"FOO=bar # Another comment\\nBAR=foo\", {\"FOO\" => \"bar\", \"BAR\" => \"foo\"}},\n      {\"FOO=\\n\\n# comment\\nBAR=bar\", {\"FOO\" => \"\", \"BAR\" => \"bar\"}},\n      {\"FOO=NOT#COMMENT\", {\"FOO\" => \"NOT#COMMENT\"}},\n      {\"FOO=  # Comment\", {\"FOO\" => \"\"}},\n\n      # Edge cases - no conversions, only strings as values\n      {\"FOO=0\", {\"FOO\" => \"0\"}},\n      {\"FOO=false\", {\"FOO\" => \"false\"}},\n      {\"FOO=null\", {\"FOO\" => \"null\"}},\n\n      # Export\n      {\"export FOO=bar\", {\"FOO\" => \"bar\"}},\n      {\"  export   FOO=bar\", {\"FOO\" => \"bar\"}},\n\n      # Variable expansion\n      {\"FOO=BAR\\nBAR=$FOO\", {\"FOO\" => \"BAR\", \"BAR\" => \"BAR\"}},\n      {\"FOO=BAR\\nBAR=\\\"$FOO\\\"\", {\"FOO\" => \"BAR\", \"BAR\" => \"BAR\"}},\n      {\"FOO=BAR\\nBAR='$FOO'\", {\"FOO\" => \"BAR\", \"BAR\" => \"$FOO\"}},\n      {\"FOO_BAR9=BAR\\nBAR=$FOO_BAR9\", {\"FOO_BAR9\" => \"BAR\", \"BAR\" => \"BAR\"}},\n      {\"FOO=BAR\\nBAR=${FOO}Z\", {\"FOO\" => \"BAR\", \"BAR\" => \"BARZ\"}},\n      {\"FOO=BAR\\nBAR=$FOO}\", {\"FOO\" => \"BAR\", \"BAR\" => \"BAR}\"}},\n      {\"FOO=BAR\\nBAR=\\\\$FOO\", {\"FOO\" => \"BAR\", \"BAR\" => \"$FOO\"}},\n      {\"FOO=\\\" \\\\$ \\\"\", {\"FOO\" => \" $ \"}},\n      {\"FOO=\\\" $ \\\"\", {\"FOO\" => \" $ \"}},\n      {\"BAR=$LOCAL\", {\"BAR\" => \"local\"}},\n      {\"BAR=$REMOTE\", {\"BAR\" => \"remote\"}},\n      {\"FOO=$NOTDEFINED\", {\"FOO\" => \"\"}},\n      {\"FOO=BAR\\nBAR=${FOO:-TEST}\", {\"FOO\" => \"BAR\", \"BAR\" => \"BAR\"}},\n      {\"FOO=BAR\\nBAR=${NOTDEFINED:-TEST}\", {\"FOO\" => \"BAR\", \"BAR\" => \"TEST\"}},\n      {\"FOO=\\nBAR=${FOO:-TEST}\", {\"FOO\" => \"\", \"BAR\" => \"TEST\"}},\n      {\"FOO=\\nBAR=$FOO:-TEST}\", {\"FOO\" => \"\", \"BAR\" => \"TEST}\"}},\n      {\"FOO=BAR\\nBAR=${FOO:=TEST}\", {\"FOO\" => \"BAR\", \"BAR\" => \"BAR\"}},\n      {\"FOO=BAR\\nBAR=${NOTDEFINED:=TEST}\", {\"FOO\" => \"BAR\", \"NOTDEFINED\" => \"TEST\", \"BAR\" => \"TEST\"}},\n      {\"FOO=\\nBAR=${FOO:=TEST}\", {\"FOO\" => \"TEST\", \"BAR\" => \"TEST\"}},\n      {\"FOO=\\nBAR=$FOO:=TEST}\", {\"FOO\" => \"TEST\", \"BAR\" => \"TEST}\"}},\n      {\"FOO=foo\\nFOOBAR=${FOO}${BAR}\", {\"FOO\" => \"foo\", \"FOOBAR\" => \"foo\"}},\n\n      # Underscores\n      {\"_FOO=BAR\", {\"_FOO\" => \"BAR\"}},\n      {\"_FOO_BAR=FOOBAR\", {\"_FOO_BAR\" => \"FOOBAR\"}},\n    ] of {String, Hash(String, String)}\n\n    {% if flag? :unix %}\n      tests.push(\n        {\"FOO=$(echo foo)\", {\"FOO\" => \"foo\"}},\n        {\"FOO=$((1+2))\", {\"FOO\" => \"3\"}},\n        {\"FOO=FOO$((1+2))BAR\", {\"FOO\" => \"FOO3BAR\"}},\n        {\"FOO=$(echo \\\"$(echo \\\"$(echo \\\"$(echo foo)\\\")\\\")\\\")\", {\"FOO\" => \"foo\"}},\n        {\"FOO=$(echo \\\"Quotes won't be a problem\\\")\", {\"FOO\" => \"Quotes won't be a problem\"}},\n        {\"FOO=bar\\nBAR=$(echo \\\"FOO is $FOO\\\")\", {\"FOO\" => \"bar\", \"BAR\" => \"FOO is bar\"}},\n      )\n    {% end %}\n\n    tests\n  end\n\n  @[DataProvider(\"env_data_with_format_errors\")]\n  def test_parse_with_format_error(data : String, error_message : String | Regex) : Nil\n    dotenv = Athena::Dotenv.new\n\n    expect_raises Athena::Dotenv::Exception::Format, error_message do\n      dotenv.parse data\n    end\n  end\n\n  def env_data_with_format_errors : Array\n    tests = [\n      {\"FOO=BAR BAZ\", \"A value containing spaces must be surrounded by quotes in '.env' at line 1.\\n...FOO=BAR BAZ...\\n             ^ line 1 offset 11\"},\n      {\"FOO BAR=BAR\", \"Whitespace characters are not supported after the variable name in '.env' at line 1.\\n...FOO BAR=BAR...\\n     ^ line 1 offset 3\"},\n      {\"FOO\", \"Missing = in the environment variable declaration in '.env' at line 1.\\n...FOO...\\n     ^ line 1 offset 3\"},\n      {\"FOO=\\\"foo\", \"Missing quote to end the value in '.env' at line 1.\\n...FOO=\\\"foo...\\n          ^ line 1 offset 8\"},\n      {\"FOO='foo\", \"Missing quote to end the value in '.env' at line 1.\\n...FOO='foo...\\n          ^ line 1 offset 8\"},\n      {\"FOO=\\\"foo\\nBAR=\\\"bar\\\"\", \"Missing quote to end the value in '.env' at line 1.\\n...FOO=\\\"foo\\\\nBAR=\\\"bar\\\"...\\n                     ^ line 1 offset 18\"},\n      {\"FOO='foo\\n\", \"Missing quote to end the value in '.env' at line 1.\\n...FOO='foo\\\\n...\\n            ^ line 1 offset 9\"},\n      {\"export FOO\", \"Unable to unset an environment variable in '.env' at line 1.\\n...export FOO...\\n            ^ line 1 offset 10\"},\n      {\"FOO=${FOO\", \"Unclosed braces on variable expansion in '.env' at line 1.\\n...FOO=${FOO...\\n           ^ line 1 offset 9\"},\n      {\"FOO= BAR\", \"Whitespace is not supported before the value in '.env' at line 1.\\n...FOO= BAR...\\n      ^ line 1 offset 4\"},\n      {\"Стасян\", \"Invalid character in variable name in '.env' at line 1.\\n...Стасян...\\n  ^ line 1 offset 0\"},\n      {\"FOO!\", \"Missing = in the environment variable declaration in '.env' at line 1.\\n...FOO!...\\n     ^ line 1 offset 3\"},\n      {\"FOO=$(echo foo\", \"Missing closing parenthesis in '.env' at line 1.\\n...FOO=$(echo foo...\\n                ^ line 1 offset 14\"},\n      {\"FOO=$(echo foo\\n\", \"Missing closing parenthesis in '.env' at line 1.\\n...FOO=$(echo foo\\\\n...\\n                ^ line 1 offset 14\"},\n      {\"FOO=\\nBAR=${FOO:-\\\\'a{a}a}\", \"Unsupported character ''' found in the default value of variable '$FOO' in '.env' at line 2.\\n...\\\\nBAR=${FOO:-\\\\'a{a}a}...\\n                       ^ line 2 offset 24\"},\n      {\"FOO=\\nBAR=${FOO:-a$a}\", \"Unsupported character '$' found in the default value of variable '$FOO' in '.env' at line 2.\\n...FOO=\\\\nBAR=${FOO:-a$a}...\\n                       ^ line 2 offset 20\"},\n      {\"FOO=\\nBAR=${FOO:-a\\\"a}\", \"Unclosed braces on variable expansion in '.env' at line 2.\\n...FOO=\\\\nBAR=${FOO:-a\\\"a}...\\n                    ^ line 2 offset 17\"},\n      {\"_=FOO\", \"Invalid character in variable name in '.env' at line 1.\\n..._=FOO...\\n  ^ line 1 offset 0\"},\n    ] of {String, String | Regex}\n\n    {% if flag? :unix %}\n      tests << {\"FOO=$((1dd2))\", /Issue expanding a command \\(.*\\n\\) in '\\.env' at line 1\\.\\n\\.\\.\\.FOO=\\$\\(\\(1dd2\\)\\)\\.\\.\\.\\n               \\^ line 1 offset 13/}\n    {% end %}\n\n    tests\n  end\n\n  def test_load : Nil\n    ENV.delete \"FOO\"\n    ENV.delete \"BAR\"\n\n    file1 = File.tempfile do |f|\n      f.puts \"FOO=BAR\"\n    end\n\n    file2 = File.tempfile do |f|\n      f.puts \"BAR=BAZ\"\n    end\n\n    Athena::Dotenv.new.load file1.path, file2.path\n\n    ENV[\"FOO\"]?.should eq \"BAR\"\n    ENV[\"BAR\"]?.should eq \"BAZ\"\n\n    ENV.delete \"FOO\"\n    ENV.delete \"BAR\"\n\n    file1.delete\n    file2.delete\n  end\n\n  def test_class_load : Nil\n    ENV.delete \"FOO\"\n    ENV.delete \"BAR\"\n\n    file1 = File.tempfile do |f|\n      f.puts \"FOO=BAR\"\n    end\n\n    file2 = File.tempfile do |f|\n      f.puts \"BAR=BAZ\"\n    end\n\n    Athena::Dotenv.load file1.path, file2.path\n\n    ENV[\"FOO\"]?.should eq \"BAR\"\n    ENV[\"BAR\"]?.should eq \"BAZ\"\n\n    ENV.delete \"FOO\"\n    ENV.delete \"BAR\"\n\n    file1.delete\n    file2.delete\n  end\n\n  def test_class_load_single_file : Nil\n    ENV.delete \"FOO\"\n\n    file = File.tempfile do |f|\n      f.puts \"FOO=BAR\"\n    end\n\n    Athena::Dotenv.load file.path\n\n    ENV[\"FOO\"]?.should eq \"BAR\"\n\n    ENV.delete \"FOO\"\n\n    file.delete\n  end\n\n  def test_class_load_defaults : Nil\n    ENV.delete \"BAZ\"\n\n    file = File.open \".env\", \"w\"\n    file.puts \"BAZ=BAZ\"\n    file.flush\n\n    Athena::Dotenv.load\n\n    ENV[\"BAZ\"]?.should eq \"BAZ\"\n\n    ENV.delete \"BAZ\"\n\n    file.delete\n  end\n\n  def test_load_environment : Nil\n    reset_context = Proc(Nil).new do\n      ENV.delete \"ATHENA_DOTENV_VARS\"\n      ENV.delete \"FOO\"\n      ENV.delete \"TEST_APP_ENV\"\n\n      ENV[\"EXISTING_KEY\"] = \"EXISTING_VALUE\"\n    end\n\n    path = File.tempname\n\n    # .env\n    reset_context.call\n    File.write path, \"FOO=BAR\\nEXISTING_KEY=NEW_VALUE\"\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\"\n    ENV[\"FOO\"]?.should eq \"BAR\"\n    ENV[\"TEST_APP_ENV\"]?.should eq \"dev\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"EXISTING_VALUE\"\n\n    reset_context.call\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\", override_existing_vars: true\n    ENV[\"FOO\"]?.should eq \"BAR\"\n    ENV[\"TEST_APP_ENV\"]?.should eq \"dev\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"NEW_VALUE\"\n\n    # .env.local\n    reset_context.call\n    ENV[\"TEST_APP_ENV\"] = \"local\"\n    File.write \"#{path}.local\", \"FOO=localBAR\\nEXISTING_KEY=localNEW_VALUE\"\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\"\n    ENV[\"FOO\"]?.should eq \"localBAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"EXISTING_VALUE\"\n\n    reset_context.call\n    ENV[\"TEST_APP_ENV\"] = \"local\"\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\", override_existing_vars: true\n    ENV[\"FOO\"]?.should eq \"localBAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"localNEW_VALUE\"\n\n    # Special case for test\n    reset_context.call\n    ENV[\"TEST_APP_ENV\"] = \"test\"\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\"\n    ENV[\"FOO\"]?.should eq \"BAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"EXISTING_VALUE\"\n\n    reset_context.call\n    ENV[\"TEST_APP_ENV\"] = \"test\"\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\", override_existing_vars: true\n    ENV[\"FOO\"]?.should eq \"BAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"NEW_VALUE\"\n\n    # .env.dev\n    reset_context.call\n    File.write \"#{path}.dev\", \"FOO=devBAR\\nEXISTING_KEY=devNEW_VALUE\"\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\"\n    ENV[\"FOO\"]?.should eq \"devBAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"EXISTING_VALUE\"\n\n    reset_context.call\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\", override_existing_vars: true\n    ENV[\"FOO\"]?.should eq \"devBAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"devNEW_VALUE\"\n\n    # .env.dev.local\n    reset_context.call\n    File.write \"#{path}.dev.local\", \"FOO=devlocalBAR\\nEXISTING_KEY=devlocalNEW_VALUE\"\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\"\n    ENV[\"FOO\"]?.should eq \"devlocalBAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"EXISTING_VALUE\"\n\n    reset_context.call\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\", override_existing_vars: true\n    ENV[\"FOO\"]?.should eq \"devlocalBAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"devlocalNEW_VALUE\"\n\n    File.delete? \"#{path}.local\"\n    File.delete? \"#{path}.dev\"\n    File.delete? \"#{path}.dev.local\"\n\n    # .env.dist\n    reset_context.call\n    File.write \"#{path}.dist\", \"FOO=distBAR\\nEXISTING_KEY=distNEW_VALUE\"\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\"\n    ENV[\"FOO\"]?.should eq \"distBAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"EXISTING_VALUE\"\n\n    reset_context.call\n\n    Athena::Dotenv.new.load_environment path, \"TEST_APP_ENV\", override_existing_vars: true\n    ENV[\"FOO\"]?.should eq \"distBAR\"\n    ENV[\"EXISTING_KEY\"]?.should eq \"distNEW_VALUE\"\n\n    File.delete \"#{path}.dist\"\n\n    reset_context.call\n    ENV.delete \"EXISTING_KEY\"\n\n    File.delete? path\n  end\n\n  def test_overload : Nil\n    ENV.delete \"FOO\"\n    ENV.delete \"BAR\"\n\n    ENV[\"FOO\"] = \"initial_foo_value\"\n    ENV[\"BAR\"] = \"initial_bar_value\"\n\n    file1 = File.tempfile do |f|\n      f.puts \"FOO=BAR\"\n    end\n\n    file2 = File.tempfile do |f|\n      f.puts \"BAR=BAZ\"\n    end\n\n    Athena::Dotenv.new.overload file1.path, file2.path\n\n    ENV[\"FOO\"]?.should eq \"BAR\"\n    ENV[\"BAR\"]?.should eq \"BAZ\"\n\n    ENV.delete \"FOO\"\n    ENV.delete \"BAR\"\n\n    file1.delete\n    file2.delete\n  end\n\n  def test_load_directory : Nil\n    expect_raises Athena::Dotenv::Exception::Path do\n      Athena::Dotenv.new.load __DIR__\n    end\n  end\n\n  def test_does_not_override_by_default : Nil\n    ENV[\"TEST_ENV_VAR\"] = \"original_value\"\n\n    Athena::Dotenv.new.populate({\"TEST_ENV_VAR\" => \"new_value\"})\n\n    ENV[\"TEST_ENV_VAR\"]?.should eq \"original_value\"\n\n    ENV.delete \"TEST_ENV_VAR\"\n  end\n\n  def test_allows_override : Nil\n    ENV[\"TEST_ENV_VAR\"] = \"original_value\"\n\n    Athena::Dotenv.new.populate({\"TEST_ENV_VAR\" => \"new_value\"}, true)\n\n    ENV[\"TEST_ENV_VAR\"]?.should eq \"new_value\"\n\n    ENV.delete \"TEST_ENV_VAR\"\n  end\n\n  def test_memorizing_loaded_var_names_in_special_variable : Nil\n    # Does not already exist\n    ENV.delete \"ATHENA_DOTENV_VARS\"\n\n    ENV.delete \"APP_DEBUG\"\n    ENV.delete \"FOO\"\n\n    Athena::Dotenv.new.populate({\"APP_DEBUG\" => \"1\", \"FOO\" => \"BAR\"})\n\n    ENV[\"ATHENA_DOTENV_VARS\"]?.should eq \"APP_DEBUG,FOO\"\n\n    # Already exists\n    ENV[\"ATHENA_DOTENV_VARS\"] = \"APP_ENV\"\n\n    ENV[\"APP_DEBUG\"] = \"1\"\n    ENV.delete \"FOO\"\n\n    dotenv = Athena::Dotenv.new\n    dotenv.populate({\"APP_DEBUG\" => \"0\", \"FOO\" => \"BAR\"})\n    dotenv.populate({\"FOO\" => \"BAZ\"})\n\n    ENV[\"ATHENA_DOTENV_VARS\"]?.should eq \"APP_ENV,FOO\"\n  end\n\n  def test_overriding_env_vars_with_names_memorized_in_special_variable : Nil\n    ENV[\"ATHENA_DOTENV_VARS\"] = \"FOO,BAR,BAZ\"\n\n    ENV[\"FOO\"] = \"foo\"\n    ENV[\"BAR\"] = \"bar\"\n    ENV[\"BAZ\"] = \"bar\"\n    ENV[\"DOCUMENT_ROOT\"] = \"/var/www\"\n\n    Athena::Dotenv.new.populate({\n      \"FOO\"           => \"foo1\",\n      \"BAR\"           => \"bar1\",\n      \"BAZ\"           => \"baz1\",\n      \"DOCUMENT_ROOT\" => \"/boot\",\n    })\n\n    ENV[\"FOO\"]?.should eq \"foo1\"\n    ENV[\"BAR\"]?.should eq \"bar1\"\n    ENV[\"BAZ\"]?.should eq \"baz1\"\n    ENV[\"DOCUMENT_ROOT\"]?.should eq \"/var/www\"\n  end\nend\n"
  },
  {
    "path": "src/components/dotenv/spec/spec_helper.cr",
    "content": "require \"spec\"\n\nrequire \"athena-spec\"\n\nrequire \"../src/athena-dotenv\"\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/dotenv/src/athena-dotenv.cr",
    "content": "class Athena::Dotenv; end\n\nrequire \"./exception/*\"\n\n# All usage involves using an `Athena::Dotenv` instance.\n# For example:\n#\n# ```\n# require \"athena-dotenv\"\n#\n# # Create a new instance\n# dotenv = Athena::Dotenv.new\n#\n# # Load a file\n# dotenv.load \"./.env\"\n#\n# # Load multiple files\n# dotenv.load \"./.env\", \"./.env.dev\"\n#\n# # Overrides existing variables\n# dotenv.overload \"./.env\"\n#\n# # Load all files for the current $APP_ENV\n# # .env, .env.local, and .env.$APP_ENV.local or .env.$APP_ENV\n# dotenv.load_environment \"./.env\"\n# ```\n# A `Athena::Dotenv::Exception::Path` error will be raised if the provided file was not found, or is not readable.\n#\n# ## Syntax\n#\n# ENV vars should be defined one per line.\n# There should be no space between the `=` between the var name and its value.\n#\n# ```text\n# DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name\n# ```\n#\n# A`Athena::Dotenv::Exception::Format` error will be raised if a formatting/parsing error is encountered.\n#\n# ### Comments\n#\n# Comments can be defined by prefixing them with a `#` character.\n# Comments can defined on its own line, or inlined after an ENV var definition.\n#\n# ```text\n# # Single line comment\n# FOO=BAR\n#\n# BAR=BAZ # Inline comment\n# ```\n#\n# ### Quotes\n#\n# Unquoted values, or those quoted with single (`'`) quotes behave as literals while double (`\"`) quotes will have special chars expanded.\n# For example, given the following `.env` file:\n#\n# ```text\n# UNQUOTED=FOO\\nBAR\n# SINGLE_QUOTES='FOO\\nBAR'\n# DOUBLE_QUOTES=\"FOO\\nBAR\"\n# ```\n# ```\n# require \"athena-dotenv\"\n#\n# Athena::Dotenv.new.load \"./.env\"\n#\n# ENV[\"UNQUOTED\"]      # => \"FOO\\\\nBAR\"\n# ENV[\"SINGLE_QUOTES\"] # => \"FOO\\\\nBAR\"\n# ENV[\"DOUBLE_QUOTES\"] # => \"FOO\\n\" + \"BAR\"\n# ```\n#\n# Notice how only the double quotes version actually expands `\\n` into a newline, whereas the others treat it as a literal `\\n`.\n#\n# Quoted values may also extend over multiple lines:\n#\n# ```text\n# FOO=\"FOO\n# BAR\\n\n# BAZ\"\n# ```\n#\n# Both single and double quotes will include the actual newline characters, however only double quotes would expand the extra newline in `BAR\\n`.\n#\n# ### Variables\n#\n# ENV vars can be used in values by prefixing the variable name with a `$` with optional opening and closing `{}`.\n#\n# ```text\n# FOO=BAR\n# BAZ=$FOO\n# BIZ=${BAZ}\n# ```\n#\n# WARNING: The order is important when using variables.\n# In the previous example `FOO` must be defined `BAZ` which must be defined before `BIZ`.\n# This also extends to when loading multiple files, where a variable may use the value in another file.\n#\n# Default values may also be defined in case the related ENV var is not set:\n#\n# ```text\n# DB_USER=${DB_USER:-root}\n# ```\n#\n# This would set the value of `DB_USER` to be `root`, unless `DB_USER` is defined elsewhere in which case it would use the value of that variable.\n#\n# ### Commands\n#\n# Shell commands can be evaluated via `$()`.\n#\n# NOTE: Commands are currently not supported on Windows.\n#\n# ```text\n# DATE=$(date)\n# ```\n#\n# ## File Precedence\n#\n# The default `.env` file defines _ALL_ ENV vars used within an application, with sane defaults.\n# This file should be committed and should not contain any sensitive values.\n#\n# However in some cases you may need to define values to override those in `.env`,\n# whether that be only for a single machine, or all machines in a specific environment.\n#\n# For these purposes there are other `.env` files that are loaded in a specific order to allow for just this use case:\n#\n# * `.env` - Defines all ENV vars, and their default values, used by the application.\n# * `.env.local` - Overrides ENV vars for all environments, but only for the machine that contains the file.\n#       This file should _NOT_ be committed, and is ignored in the `test` environment to ensure reproducibility.\n# * `.env.<environment>` (e.g. `.env.test`) - Overrides ENV vars for only this one environment. These files _SHOULD_ be committed.\n# * `.env.<environment>.local` (e.g. `.env.test.local`) - Machine-specific overrides, but only for a single environment. This file should _NOT_ be committed.\n#\n# See `#load_environment` for more information.\n#\n# NOTE: Real ENV vars always win against those created in any `.env` file.\n#\n# TIP: Environment specific `.env` files should _ONLY_ to override values defined within the default `.env` file and _NOT_ as a replacement to it.\n# This ensures there is still a single source of truth and removes the need to duplicate everything for each environment.\n#\n# ## Production\n#\n# `.env` files are mainly intended for non-production environments in order to give the benefits of using ENV vars, but be more convenient/easier to use.\n# They can of course continue to be used in production by distributing the base `.env` file along with the binary, then creating a `.env.local` on the production server and including production values within it.\n# This can work quite well for simple applications, but ultimately a more robust solution that best leverages the features of the server the application is running on is best.\nclass Athena::Dotenv\n  VERSION = \"0.2.1\"\n\n  # Both acts as a namespace for exceptions related to the `Athena::Dotenv` component, as well as a way to check for exceptions from the component.\n  module Exception; end\n\n  private VARNAME_REGEX = /(?i:_?[A-Z][A-Z0-9_]*+)/\n\n  private enum State\n    VARNAME\n    VALUE\n  end\n\n  # Convenience method that loads the file at the provided *path*, defaulting to `.env`.\n  def self.load(path : String | ::Path = \".env\") : self\n    instance = new\n    instance.load path\n    instance\n  end\n\n  # Convenience method that loads one or more `.env` files, defaulting to `.env`.\n  def self.load(path : String | ::Path = \".env\", *paths : String | ::Path) : self\n    instance = new\n    instance.load path, *paths\n    instance\n  end\n\n  @path : String | ::Path = \"\"\n  @data = \"\"\n  @values = Hash(String, String).new\n  @reader : Char::Reader\n  @line_number = 1\n\n  def initialize(\n    @env_key : String = \"APP_ENV\",\n  )\n    # Can't use a `getter!` macro since that would return a copy of the reader each time :/\n    @reader = uninitialized Char::Reader\n  end\n\n  # Loads each `.env` file within the provided *paths*.\n  #\n  # ```\n  # require \"athena-dotenv\"\n  #\n  # dotenv = Athena::Dotenv.new\n  #\n  # dotenv.load \"./.env\"\n  # dotenv.load \"./.env\", \"./.env.dev\"\n  # ```\n  def load(*paths : String | ::Path) : Nil\n    self.load false, paths\n  end\n\n  # Loads a `.env` file and its related additional files based on their [precedence][Athena::Dotenv--file-precedence] if they exist.\n  #\n  # The current ENV is determined by the value of `APP_ENV`, which is configurable globally via `.new`, or for a single load via the *env_key* parameter.\n  # If no environment ENV var is defined, *default_environment* will be used.\n  # The `.env.local` file will _NOT_ be loaded if the current environment is included within *test_environments*.\n  #\n  # Existing ENV vars may optionally be overridden by passing `true` to *override_existing_vars*.\n  #\n  # ```\n  # require \"athena-dotenv\"\n  #\n  # dotenv = Athena::Dotenv.new\n  #\n  # # Use `APP_ENV`, or `dev`\n  # dotenv.load_environment \"./.env\"\n  #\n  # # Custom *env_key* and *default_environment*\n  # dotenv.load_environment \"./.env\", \"ATHENA_ENV\", \"qa\"\n  # ```\n  def load_environment(\n    path : String | ::Path,\n    env_key : String? = nil,\n    default_environment : String = \"dev\",\n    test_environments : Enumerable(String) = {\"test\"},\n    override_existing_vars : Bool = false,\n  ) : Nil\n    env_key = env_key || @env_key\n\n    dist_path = \"#{path}.dist\"\n    if File.file?(path) && !File.file?(dist_path)\n      self.load override_existing_vars, {path}\n    else\n      self.load override_existing_vars, {dist_path}\n    end\n\n    unless env = ENV[env_key]?\n      self.populate({env_key => env = default_environment}, override_existing_vars)\n    end\n\n    local_path = \"#{path}.local\"\n    if !test_environments.includes?(env) && File.file?(local_path)\n      self.load override_existing_vars, {local_path}\n      env = ENV.fetch env_key, env\n    end\n\n    return if \"local\" == env\n\n    if File.file? p = \"#{path}.#{env}\"\n      self.load override_existing_vars, {p}\n    end\n\n    if File.file? p = \"#{path}.#{env}.local\"\n      self.load override_existing_vars, {p}\n    end\n  end\n\n  # Same as `#load`, but will override existing ENV vars.\n  def overload(*paths : String | ::Path) : Nil\n    self.load true, paths\n  end\n\n  # Parses and returns a Hash based on the string contents of the provided *data* string.\n  # The original `.env` file path may also be provided to *path* for more meaningful error messages.\n  #\n  # ```\n  # require \"athena-dotenv\"\n  #\n  # path = \"/path/to/.env\"\n  # dotenv = Athena::Dotenv.new\n  #\n  # File.write path, \"FOO=BAR\"\n  #\n  # dotenv.parse File.read(path), path # => {\"FOO\" => \"BAR\"}\n  # ```\n  def parse(data : String, path : String | ::Path = \".env\") : Hash(String, String)\n    @path = path\n    @data = data = data.gsub(\"\\r\\n\", \"\\n\").gsub(\"\\r\", \"\\n\")\n    @reader = Char::Reader.new data\n\n    @values.clear\n\n    state : State = :varname\n    name = \"\"\n\n    self.skip_empty_lines\n\n    while @reader.has_next?\n      case state\n      in .varname?\n        name = self.lex_varname\n        state = :value\n      in .value?\n        @values[name] = self.lex_value\n        state = :varname\n      end\n    end\n\n    if state.value?\n      @values[name] = \"\"\n    end\n\n    begin\n      @values.dup\n    ensure\n      @values.clear\n      @reader = uninitialized Char::Reader\n    end\n  end\n\n  # Populates the provides *values* into the environment.\n  #\n  # Existing ENV vars may optionally be overridden by passing `true` to *override_existing_vars*.\n  #\n  # ```\n  # require \"athena-dotenv\"\n  #\n  # ENV[\"FOO\"]? # => nil\n  #\n  # Athena::Dotenv.new.populate({\"FOO\" => \"BAR\"})\n  #\n  # ENV[\"FOO\"]? # => \"BAR\"\n  # ```\n  def populate(values : Hash(String, String), override_existing_vars : Bool = false) : Nil\n    update_loaded_vars = false\n\n    loaded_vars = ENV.fetch(\"ATHENA_DOTENV_VARS\", \"\").split(',').to_set\n\n    values.each do |name, value|\n      if !loaded_vars.includes?(name) && !override_existing_vars && ENV.has_key?(name)\n        next\n      end\n\n      ENV[name] = value\n\n      if !loaded_vars.includes?(name)\n        loaded_vars << name\n        update_loaded_vars = true\n      end\n    end\n\n    if update_loaded_vars\n      loaded_vars.delete \"\"\n      ENV[\"ATHENA_DOTENV_VARS\"] = loaded_vars.join ','\n    end\n  end\n\n  private def advance_reader(string : String) : Nil\n    @reader.pos += string.size\n    @line_number += string.count '\\n'\n  end\n\n  private def create_format_exception(message : String) : Athena::Dotenv::Exception::Format\n    Athena::Dotenv::Exception::Format.new(\n      message,\n      Athena::Dotenv::Exception::Format::Context.new(\n        @data,\n        @path,\n        @line_number,\n        @reader.pos\n      )\n    )\n  end\n\n  private def lex_nested_expression : String\n    char = @reader.next_char\n    value = \"\"\n\n    until char.in? '\\n', ')'\n      value += char\n\n      if '(' == char\n        value += \"#{self.lex_nested_expression})\"\n      end\n\n      char = @reader.next_char\n\n      unless @reader.has_next?\n        raise self.create_format_exception \"Missing closing parenthesis\"\n      end\n    end\n\n    if '\\n' == char\n      raise self.create_format_exception \"Missing closing parenthesis\"\n    end\n\n    value\n  end\n\n  private def lex_varname : String\n    unless match = /(export[ \\t]++)?(#{VARNAME_REGEX})/.match(@data, @reader.pos, Regex::MatchOptions[:anchored])\n      raise self.create_format_exception \"Invalid character in variable name\"\n    end\n\n    self.advance_reader match[0]\n\n    if !@reader.has_next? || @reader.current_char.in? '\\n', '#'\n      raise self.create_format_exception \"Unable to unset an environment variable\" if match[1]?\n      raise self.create_format_exception \"Missing = in the environment variable declaration\"\n    end\n\n    if @reader.current_char.whitespace?\n      raise self.create_format_exception \"Whitespace characters are not supported after the variable name\"\n    end\n\n    if '=' != @reader.current_char\n      raise self.create_format_exception \"Missing = in the environment variable declaration\"\n    end\n\n    @reader.pos += 1\n\n    match[2]\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def lex_value : String\n    if match = (/[ \\t]*+(?:#.*)?$/m).match(@data, @reader.pos, Regex::MatchOptions[:anchored])\n      self.advance_reader match[0]\n      self.skip_empty_lines\n\n      return \"\"\n    end\n\n    if @reader.current_char.whitespace?\n      raise self.create_format_exception \"Whitespace is not supported before the value\"\n    end\n\n    loaded_vars = ENV.fetch(\"ATHENA_DOTENV_VARS\", \"\").split(',').to_set\n    loaded_vars.delete \"\"\n    v = \"\"\n\n    loop do\n      case char = @reader.current_char\n      when '\\''\n        len = 0\n\n        loop do\n          if @reader.pos + (len += 1) == @data.size\n            @reader.pos += len\n\n            raise self.create_format_exception \"Missing quote to end the value\"\n          end\n\n          break if @data[@reader.pos + len] == '\\''\n        end\n\n        v += @data[1 + @reader.pos, len - 1]\n        @reader.pos += 1 + len\n      when '\"'\n        value = \"\"\n\n        char = @reader.next_char\n\n        unless @reader.has_next?\n          raise self.create_format_exception \"Missing quote to end the value\"\n        end\n\n        while '\"' != char || ('\\\\' == @data[@reader.pos - 1] && '\\\\' != @data[@reader.pos - 2])\n          value += char\n\n          char = @reader.next_char\n\n          unless @reader.has_next?\n            raise self.create_format_exception \"Missing quote to end the value\"\n          end\n        end\n\n        @reader.next_char\n        value = value.gsub(%(\\\\\"), '\"').gsub(\"\\\\r\", \"\\r\").gsub(\"\\\\n\", \"\\n\")\n        resolved_value = value\n        resolved_value = self.resolve_commands resolved_value, loaded_vars\n        resolved_value = self.resolve_variables resolved_value, loaded_vars\n        resolved_value = resolved_value.gsub \"\\\\\\\\\", \"\\\\\"\n\n        v += resolved_value\n      else\n        value = \"\"\n        previous_char = @reader.previous_char\n        char = @reader.next_char\n        while @reader.has_next? && !char.in?('\\n', '\"', '\\'') && !((previous_char.in?(' ', '\\t')) && '#' == char)\n          if '\\\\' == char && @reader.has_next? && @reader.peek_next_char.in? '\\'', '\"'\n            char = @reader.next_char\n          end\n\n          value += (previous_char = char)\n\n          if '$' == char && @reader.has_next? && '(' == @reader.peek_next_char\n            @reader.next_char\n            value += \"(#{self.lex_nested_expression})\"\n          end\n\n          char = @reader.next_char\n        end\n\n        value = value.strip\n\n        resolved_value = value\n        resolved_value = self.resolve_commands resolved_value, loaded_vars\n        resolved_value = self.resolve_variables resolved_value, loaded_vars\n        resolved_value = resolved_value.gsub \"\\\\\\\\\", \"\\\\\"\n\n        if resolved_value == value && value.each_char.any? &.whitespace?\n          raise self.create_format_exception \"A value containing spaces must be surrounded by quotes\"\n        end\n\n        v += resolved_value\n\n        if @reader.has_next? && '#' == char\n          break\n        end\n      end\n\n      break unless @reader.has_next? && @reader.current_char != '\\n'\n    end\n\n    self.skip_empty_lines\n\n    v\n  end\n\n  private def load(override_existing_vars : Bool, paths : Enumerable(String | ::Path)) : Nil\n    paths.each do |path|\n      if !File::Info.readable?(path) || File.directory?(path)\n        raise Athena::Dotenv::Exception::Path.new path\n      end\n\n      self.populate(self.parse(File.read(path), path), override_existing_vars)\n    end\n  end\n\n  private def resolve_commands(value : String, loaded_vars : Set(String)) : String\n    return value unless value.includes? '$'\n\n    regex = /\n      (\\\\\\\\)?               # escaped with a backslash?\n      \\$\n      (?<cmd>\n          \\(                # require opening parenthesis\n          ([^()]|\\g<cmd>)+  # allow any number of non-parens, or balanced parens (by nesting the <cmd> expression recursively)\n          \\)                # require closing paren\n      )\n    /x\n\n    value.gsub regex do |_, match|\n      if '\\\\' == match[1]?\n        next match[0][1..]\n      end\n\n      {% if flag? :win32 %}\n        # TODO: Support windows?\n        raise RuntimeError.new \"Resolving commands is not supported on Windows.\"\n      {% end %}\n\n      env = {} of String => String\n      @values.each do |k, v|\n        if loaded_vars.includes?(k) || !ENV.has_key?(k)\n          env[k] = v\n        end\n      end\n\n      output = IO::Memory.new\n      error = IO::Memory.new\n\n      status = Process.run(\n        \"echo #{match[0]}\",\n        shell: true,\n        env: env,\n        output: output,\n        error: error\n      )\n\n      unless status.success?\n        raise self.create_format_exception \"Issue expanding a command (#{error})\"\n      end\n\n      output.to_s.gsub /[\\r\\n]+$/, \"\"\n    end\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def resolve_variables(value : String, loaded_vars : Set(String)) : String\n    return value unless value.includes? '$'\n\n    regex = /\n      (?<!\\\\)\n      (?P<backslashes>\\\\*)             # escaped with a backslash?\n      \\$\n      (?!\\()                           # no opening parenthesis\n      (?P<opening_brace>\\{)?           # optional brace\n      (?P<name>(?i:[A-Z][A-Z0-9_]*+))? # var name\n      (?P<default_value>:[-=][^\\}]++)? # optional default value\n      (?P<closing_brace>\\})?           # optional closing brace\n    /x\n\n    value.gsub regex do |_, match|\n      if match[\"backslashes\"].size.odd?\n        next match[0][1..]\n      end\n\n      # Unescaped $ not followed by var name\n      if match[\"name\"]?.nil?\n        next match[0]\n      end\n\n      if \"{\" == match[\"opening_brace\"]? && match[\"closing_brace\"]?.nil?\n        raise self.create_format_exception \"Unclosed braces on variable expansion\"\n      end\n\n      name = match[\"name\"]\n\n      value = if loaded_vars.includes?(name) && @values.has_key?(name)\n                @values[name]\n              elsif @values.has_key? name\n                @values[name]\n              else\n                ENV.fetch name, \"\"\n              end\n\n      if value.empty? && (default_value = match[\"default_value\"]?.presence)\n        if unsupported_char = default_value.each_char.find &.in?('\\'', '\"', '{', '$')\n          raise self.create_format_exception \"Unsupported character '#{unsupported_char}' found in the default value of variable '$#{name}'\"\n        end\n\n        value = match[\"default_value\"][2..]\n\n        if '=' == match[\"default_value\"][1]\n          @values[name] = value\n        end\n      end\n\n      if !match[\"opening_brace\"]?.presence && !match[\"closing_brace\"]?.nil?\n        value += '}'\n      end\n\n      \"#{match[\"backslashes\"]}#{value}\"\n    end\n  end\n\n  private def skip_empty_lines : Nil\n    if match = (/(?:\\s*+(?:#[^\\n]*+)?+)++/).match(@data, @reader.pos, Regex::MatchOptions[:anchored])\n      self.advance_reader match[0]\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/dotenv/src/exception/format.cr",
    "content": "require \"./logic\"\n\n# Raised when there is a parsing error within a `.env` file.\nclass Athena::Dotenv::Exception::Format < Athena::Dotenv::Exception::Logic\n  # Stores contextual information related to an `Athena::Dotenv::Exception::Format`.\n  #\n  # ```\n  # begin\n  #   dotenv = Athena::Dotenv.new.parse \"NAME=Jim\\nFOO=BAR BAZ\"\n  # rescue ex : Athena::Dotenv::Exception::Format\n  #   ctx = ex.context\n  #\n  #   ctx.path        # => \".env\"\n  #   ctx.line_number # => 2\n  #   ctx.details     # => \"...NAME=Jim\\nFOO=BAR BAZ...\\n                       ^ line 2 offset 20\"\n  # end\n  # ```\n  struct Context\n    # Returns the path to the improperly formatted `.env` file.\n    getter path : String\n\n    # Returns the line number of the format error.\n    getter line_number : Int32\n\n    def initialize(\n      @data : String,\n      path : ::Path | String,\n      @line_number : Int32,\n      @offset : Int32,\n    )\n      @path = path.to_s\n    end\n\n    # Returns a details string that includes the markup before/after the error, along with what line number and offset the error occurred at.\n    def details : String\n      before = @data[Math.max(0, @offset - 20), Math.min(20, @offset)].gsub \"\\n\", \"\\\\n\"\n      after = @data[@offset, 20].gsub \"\\n\", \"\\\\n\"\n\n      %(...#{before}#{after}...\\n#{\" \" * (before.size + 2)}^ line #{@line_number} offset #{@offset})\n    end\n  end\n\n  # Returns an object containing contextual information about this error.\n  getter context : Athena::Dotenv::Exception::Format::Context\n\n  def initialize(message : String, @context : Athena::Dotenv::Exception::Format::Context, cause : ::Exception? = nil)\n    super \"#{message} in '#{@context.path}' at line #{@context.line_number}.\\n#{@context.details}\", cause\n  end\nend\n"
  },
  {
    "path": "src/components/dotenv/src/exception/logic.cr",
    "content": "# Represents a code logic error that should lead directly to a fix in your code.\nclass Athena::Dotenv::Exception::Logic < ::Exception\n  include Athena::Dotenv::Exception\nend\n"
  },
  {
    "path": "src/components/dotenv/src/exception/path.cr",
    "content": "# Raised when a `.env` file is unable to be read, or non-existent.\nclass Athena::Dotenv::Exception::Path < RuntimeError\n  include Athena::Dotenv::Exception\n\n  def initialize(path : String | Path, cause : ::Exception? = nil)\n    super \"Unable to read the '#{path}' environment file.\", cause\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/event_dispatcher/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/event_dispatcher/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.4.1] - 2026-04-19\n\n### Changed\n\n- Improve compile time error messages ([#646]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix compatibility with `ACTR::EventDispatcher::Event` based event types ([#656]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.1]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.4.1\n[#646]: https://github.com/athena-framework/athena/pull/646\n[#656]: https://github.com/athena-framework/athena/pull/656\n\n## [0.4.0] - 2025-09-04\n\n### Changed\n\n- **Breaking:** Changed interface of `AED::EventDispatcherInterface#dispatch` to accept an `ACTR::EventDispatcher::Event` vs `AED::Event` ([#544]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Removed\n\n- Removed `AED::StoppableEvent` in favor of `ACTR::EventDispatcher::StoppableEvent` ([#544]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.0]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.4.0\n[#544]: https://github.com/athena-framework/athena/pull/544\n\n## [0.3.1] - 2025-01-26\n\n_Administrative release, no functional changes_\n\n[0.3.1]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.3.1\n\n## [0.3.0] - 2024-04-09\n\n### Changed\n\n- **Breaking:** remove `AED::EventListenerInterface` ([#391]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.3.0\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#391]: https://github.com/athena-framework/athena/pull/391\n\n## [0.2.3] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.2.3]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.2.3\n\n## [0.2.2] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.2.2]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.2.2\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.2.1] - 2023-02-04\n\n### Added\n\n- Add better integration between `Athena::EventDispatcher` and `Athena::DependencyInjection` ([#259]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.2.1\n[#259]: https://github.com/athena-framework/athena/pull/259\n\n## [0.2.0] - 2023-01-07\n\n### Changed\n\n- **Breaking:** refactor how listeners are registered to use the new `AEDA::AsEventListener` annotation on the method instead of the `self.subscribed_events` class method ([#236]) (George Dietrich)\n- **Breaking:** refactor and rename the majority of `AED::EventDispatcherInterface` API ([#236]) (George Dietrich)\n- **Breaking:** change the representation of a listener when returned from a dispatcher to be an `AED::Callable` instance ([#236]) (George Dietrich)\n- **Breaking:** refactor `AED::Event` to now be `abstract` ([#236]) (George Dietrich)\n\n### Added\n\n- Add `AED::GenericEvent` that can be used for convenience within simple use cases ([#236]) (George Dietrich)\n- Add the ability to use a listener method without the `AED::EventDispatcherInterface` parameter ([#236]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** remove ability for listeners to automatically be registered with the dispatcher ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED::EventDispatcher.new` constructor that accepts an `Array(AED::EventListenerInterface)` ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED::EventListenerType` alias ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED::SubscribedEvents` alias ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED::EventListener` struct ([#236]) (George Dietrich)\n- **Breaking:** remove the `AED.create_listener` method ([#236]) (George Dietrich)\n- Remove the requirement that listeners methods need to be called `call` ([#236]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.2.0\n[#236]: https://github.com/athena-framework/athena/pull/236\n\n## [0.1.4] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Fix the `VERSION` constant's value ([#166]) (George Dietrich)\n\n[0.1.4]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.4\n[#166]: https://github.com/athena-framework/athena/pull/166\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.3] - 2021-01-29\n\n### Changed\n\n- Migrate documentation to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#14]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.3\n[#14]: https://github.com/athena-framework/event-dispatcher/pull/14\n\n## [0.1.2] - 2020-12-03\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#13]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.2\n[#13]: https://github.com/athena-framework/event-dispatcher/pull/13\n\n## [0.1.1] - 2020-11-12\n\n### Added\n\n- Add the [AED::Spec](https://athenaframework.org/EventDispatcher/Spec/) module to provide helpful testing utilities ([#11]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.1\n[#11]: https://github.com/athena-framework/event-dispatcher/pull/11\n\n## [0.1.0] - 2020-01-11\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/event-dispatcher/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/event_dispatcher/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/event_dispatcher/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020 Blacksmoke16\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": "src/components/event_dispatcher/README.md",
    "content": "# Event Dispatcher\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/event-dispatcher.svg?style=flat-square)](https://github.com/athena-framework/event-dispatcher/releases)\n\nA [Mediator](https://en.wikipedia.org/wiki/Mediator_pattern) and [Observer](https://en.wikipedia.org/wiki/Observer_pattern) pattern event library.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/EventDispatcher).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/event_dispatcher/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.3.0\n\n### Remove `AED::EventListenerInterface`\n\nThe `AED::EventListenerInterface` no longer needs included in your event listener types, and can simply be removed. A type with one or more `AEDA::AsEventListener` annotated methods is now all that is required.\n"
  },
  {
    "path": "src/components/event_dispatcher/docs/README.md",
    "content": "Object-oriented code has helped a lot in ensuring code extensibility.\nBy having classes with well defined responsibilities, it becomes more flexible and easily extendable to modify their behavior.\nHowever inheritance has its limits is not the best option when these modifications need to be shared between other modified subclasses.\nSay for example you want to do something before and after a method is executed, without interfering with the other logic.\n\nThe `Athena::EventDispatcher` component is a [Mediator](https://en.wikipedia.org/wiki/Mediator_pattern) and [Observer](https://en.wikipedia.org/wiki/Observer_pattern) pattern event library.\nThis pattern allows creating very flexibly and truly extensible applications.\n\nA good example of this is the [architecture](/getting_started/middleware#events) of the Athena Framework itself in how it uses `Athena::EventDispatcher` to dispatch events that then is able to notify all registered listeners for that event.\nThese listeners could then make any necessary modifications seamlessly without affecting the framework logic itself, or the other listeners.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-event_dispatcher:\n    github: athena-framework/event-dispatcher\n    version: ~> 0.4.0\n```\n## Usage\n\nUsage of this component centers around [AED::EventDispatcherInterface](/EventDispatcher/EventDispatcherInterface/), an extension of the base [ACTR::EventDispatcher::Interface](/Contracts/EventDispatcher/Interface/) with extra functionality,\nwith the default implementation being [AED::EventDispatcher](/EventDispatcher/EventDispatcher/).\nThe event dispatcher  keeps track of the listeners on various [AED::Event](/EventDispatcher/Event/)s.\nAn event is nothing more than a plain old Crystal object that provides access to data related to the event.\n\n```crystal\n# Create a custom event that can be emitted when an order is placed.\nclass OrderPlaced < AED::Event\n  getter order : Order\n\n  def initialize(@order : Order); end\nend\n```\n\nFor simple use cases, listeners may be registered directly:\n\n```crystal\ndispatcher = AED::EventDispatcher.new\n\n# Register a listener on our event directly with the dispatcher\ndispatcher.listener OrderPlaced do |event|\n  pp event.order\nend\n```\n\nHowever having a dedicated type is usually the better practice.\n\n```crystal\nstruct SendConfirmationListener\n  @[AEDA::AsEventListener]\n  def order_placed(event : OrderPlaced) : Nil\n    # Send a confirmation email to the user\n  end\nend\n\ndispatcher.listener SendConfirmationListener.new\n```\n\nIn either case, the dispatcher can then be used to dispatch our event.\n\n```crystal\n# Assume this is a real object\nrecord Order, id : String\n\nevent = OrderPlaced.new Order.new \"order 1\"\n\ndispatcher.dispatch Order.new\n# => Order(@id=\"order1\")\n```\n\nWARNING: If using this component within the context of something that handles independent execution flows, such as a web framework, you will want there to be a dedicated dispatcher instance for each path.\nThis ensures that one flow will not leak state to any other flow, while still allowing flow specific mutations to be used.\nConsider pairing this component with the [Athena::DependencyInjection](/DependencyInjection) component as a way to handle this.\n\n## Learn More\n\n* [Listener Priority](/EventDispatcher/EventDispatcherInterface/#Athena::EventDispatcher::EventDispatcherInterface--listener-priority)\n* [Stoppable](/Contracts/EventDispatcher/StoppableEvent/) events\n* [Testing Abstractions](/EventDispatcher/Spec/TracableEventDispatcher/)\n"
  },
  {
    "path": "src/components/event_dispatcher/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Event Dispatcher\nsite_url: https://athenaframework.org/EventDispatcher/\nrepo_url: https://github.com/athena-framework/event-dispatcher\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-contracts/src/athena-contracts.cr\n            - ./lib/athena-event_dispatcher/src/athena-event_dispatcher.cr\n            - ./lib/athena-event_dispatcher/src/spec.cr\n          source_locations:\n            lib/athena-event_dispatcher: https://github.com/athena-framework/event-dispatcher/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/event_dispatcher/shard.yml",
    "content": "name: athena-event_dispatcher\n\nversion: 0.4.1\n\ncrystal: ~> 1.4\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/event-dispatcher\n\ndocumentation: https://athenaframework.org/EventDispatcher\n\ndescription: |\n  A Mediator and Observer pattern event library.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\ndependencies:\n  athena-contracts:\n    github: athena-framework/contracts\n    version: ~> 0.1.0\n"
  },
  {
    "path": "src/components/event_dispatcher/spec/callable_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate class TestListener\nend\n\ndescribe AED::Callable do\n  describe \"#name\" do\n    it \"defaults to a generic name if not supplied\" do\n      callable = AED::Callable::Event(AED::GenericEvent(String, String)).new(\n        Proc(AED::GenericEvent(String, String), Nil).new { },\n        0,\n        nil,\n      )\n\n      callable.name.should eq \"unknown callable\"\n    end\n\n    it \"EventListenerInstance defaults to a more useful name\" do\n      callable = AED::Callable::EventListenerInstance(TestListener, AED::GenericEvent(String, String)).new(\n        Proc(AED::GenericEvent(String, String), Nil).new { },\n        TestListener.new,\n        0,\n        nil,\n      )\n\n      callable.name.should eq \"unknown TestListener method\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/spec/compiler_spec.cr",
    "content": "require \"./spec_helper\"\n\n# Changes here should also be reflected in `framework/spec/ext/event_dispatcher/register_listeners_spec.cr`\ndescribe Athena::EventDispatcher do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"when the listener method is static\" do\n      ASPEC::Methods.assert_compile_time_error \"Event listener methods can only be defined as instance methods. Did you mean 'MyListener#listener'?\", <<-CR\n        require \"./spec_helper.cr\"\n        class MyListener\n          @[AEDA::AsEventListener]\n          def self.listener(blah : AED::GenericEvent(String, String)) : Nil\n          end\n        end\n        AED::EventDispatcher.new.listener MyListener.new\n      CR\n    end\n\n    it \"with no parameters\" do\n      ASPEC::Methods.assert_compile_time_error \"Expected 'MyListener#listener' to have 1..2 parameters, got '0'.\", <<-CR\n        require \"./spec_helper.cr\"\n        class MyListener\n          @[AEDA::AsEventListener]\n          def listener : Nil\n          end\n        end\n        AED::EventDispatcher.new.listener MyListener.new\n      CR\n    end\n\n    it \"with too many parameters\" do\n      ASPEC::Methods.assert_compile_time_error \"Expected 'MyListener#listener' to have 1..2 parameters, got '3'.\", <<-CR\n        require \"./spec_helper.cr\"\n        class MyListener\n          @[AEDA::AsEventListener]\n          def listener(foo, bar, baz) : Nil\n          end\n        end\n        AED::EventDispatcher.new.listener MyListener.new\n      CR\n    end\n\n    it \"first parameter unrestricted\" do\n      ASPEC::Methods.assert_compile_time_error \"'MyListener#listener': event parameter must have a type restriction of an 'Athena::Contracts::EventDispatcher::Event' instance.\", <<-CR\n        require \"./spec_helper.cr\"\n        class MyListener\n          @[AEDA::AsEventListener]\n          def listener(foo) : Nil\n          end\n        end\n        AED::EventDispatcher.new.listener MyListener.new\n      CR\n    end\n\n    it \"first parameter non Athena::Contracts::EventDispatcher::Event restriction\" do\n      ASPEC::Methods.assert_compile_time_error \"'MyListener#listener': event parameter must have a type restriction of an 'Athena::Contracts::EventDispatcher::Event' instance, not 'String'.\", <<-CR\n        require \"./spec_helper.cr\"\n        class MyListener\n          @[AEDA::AsEventListener]\n          def listener(foo : String) : Nil\n          end\n        end\n        AED::EventDispatcher.new.listener MyListener.new\n      CR\n    end\n\n    it \"second parameter unrestricted\" do\n      ASPEC::Methods.assert_compile_time_error \"'MyListener#listener': dispatcher parameter must have a type restriction of 'AED::EventDispatcherInterface'.\", <<-CR\n        require \"./spec_helper.cr\"\n        class MyListener\n          @[AEDA::AsEventListener]\n          def listener(foo : AED::GenericEvent(String, String), dispatcher) : Nil\n          end\n        end\n        AED::EventDispatcher.new.listener MyListener.new\n      CR\n    end\n\n    it \"second parameter non AED::EventDispatcherInterface restriction\" do\n      ASPEC::Methods.assert_compile_time_error \"'MyListener#listener': dispatcher parameter must have a type restriction of 'AED::EventDispatcherInterface', not 'String'.\", <<-CR\n        require \"./spec_helper.cr\"\n        class MyListener\n          @[AEDA::AsEventListener]\n          def listener(foo : AED::GenericEvent(String, String), dispatcher : String) : Nil\n          end\n        end\n        AED::EventDispatcher.new.listener MyListener.new\n      CR\n    end\n\n    it \"non integer priority field\" do\n      ASPEC::Methods.assert_compile_time_error \"Event listener method 'MyListener#listener' expects a 'NumberLiteral' for its 'AEDA::AsEventListener#priority' field, but got a 'StringLiteral'.\", <<-CR\n        require \"./spec_helper.cr\"\n        class MyListener\n          @[AEDA::AsEventListener(priority: \"foo\")]\n          def listener(foo : AED::GenericEvent(String, String)) : Nil\n          end\n        end\n        AED::EventDispatcher.new.listener MyListener.new\n      CR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/spec/event_dispatcher_spec.cr",
    "content": "require \"./spec_helper\"\n\nclass PreFoo < AED::Event; end\n\nclass PostFoo < AED::Event; end\n\nclass PreBar < AED::Event; end\n\nclass ContractEvent < ACTR::EventDispatcher::Event; end\n\nclass Sum < AED::Event\n  property value : Int32 = 0\nend\n\nclass TestListener\n  getter values = [] of Int32\n\n  @[AEDA::AsEventListener]\n  def on_pre1(event : PreFoo) : Nil\n    @values << 1\n  end\n\n  @[AEDA::AsEventListener(priority: 10)]\n  def on_pre2(event : PreFoo, dispatcher : AED::EventDispatcherInterface) : Nil\n    @values << 2\n  end\n\n  @[AEDA::AsEventListener]\n  def on_post1(event : PostFoo) : Nil\n    @values << 3\n  end\n\n  @[AEDA::AsEventListener]\n  def on_contract(event : ContractEvent) : Nil\n    @values << -1\n  end\nend\n\nmodule SomeInterface; end\n\nabstract class Animal; end\n\nclass Dog < Animal; end\n\nclass Cat < Animal\n  include SomeInterface\nend\n\nabstract class ParentAnimal < Animal; end\n\nclass Sloth < ParentAnimal\n  include SomeInterface\nend\n\nclass ThreeToedSloth < Sloth; end\n\nclass GenericAnimalEvent(T) < AED::Event\n  getter animal : T\n\n  def initialize(@animal : T); end\nend\n\nclass AnimalListener\n  getter all_animal_calls : Array(Animal.class) = [] of Animal.class\n  getter only_child_animal_calls : Array(Animal.class) = [] of Animal.class\n  getter only_interface_animal_calls : Array(Animal.class) = [] of Animal.class\n  getter non_abstract_animal_calls : Array(Animal.class) = [] of Animal.class\n\n  @[AEDA::AsEventListener]\n  def all_animals(event : GenericAnimalEvent(Animal)) : Nil\n    @all_animal_calls << event.animal.class\n  end\n\n  @[AEDA::AsEventListener]\n  def only_child_animals(event : GenericAnimalEvent(ParentAnimal), dispatcher : AED::EventDispatcherInterface) : Nil\n    @only_child_animal_calls << event.animal.class\n  end\n\n  @[AEDA::AsEventListener]\n  def only_interface_animals(event : GenericAnimalEvent(SomeInterface)) : Nil\n    @only_interface_animal_calls << event.animal.class\n  end\n\n  @[AEDA::AsEventListener]\n  def non_abstract_animals(event : GenericAnimalEvent(Sloth)) : Nil\n    @non_abstract_animal_calls << event.animal.class\n  end\nend\n\nstruct EventDispatcherTest < ASPEC::TestCase\n  @dispatcher : AED::EventDispatcher\n\n  def initialize\n    @dispatcher = AED::EventDispatcher.new\n  end\n\n  @[Tags(\"compiled\")]\n  def test_listener_not_passed_event_class : Nil\n    ASPEC::Methods.assert_compile_time_error \"expected argument #1 to 'listener' to be Athena::Contracts::EventDispatcher::Event.class, not String.\", <<-CR\n      require \"./spec_helper.cr\"\n\n      AED::EventDispatcher.new.listener String do\n      end\n    CR\n  end\n\n  def test_initial_state : Nil\n    @dispatcher.listeners.should be_empty\n    @dispatcher.has_listeners?.should be_false\n    @dispatcher.has_listeners?(PreFoo).should be_false\n    @dispatcher.has_listeners?(PostFoo).should be_false\n  end\n\n  def test_listener_block : Nil\n    @dispatcher.listener PreFoo do\n    end\n\n    @dispatcher.listener PreFoo, name: \"#2\" do\n    end\n\n    @dispatcher.listener PostFoo do\n    end\n\n    @dispatcher.has_listeners?.should be_true\n    @dispatcher.has_listeners?(PreFoo).should be_true\n    @dispatcher.has_listeners?(PostFoo).should be_true\n    @dispatcher.has_listeners?(PreBar).should be_false\n\n    @dispatcher.listeners(PreFoo).size.should eq 2\n    @dispatcher.listeners(PreFoo).map(&.name).should eq [\"unknown callable\", \"#2\"]\n    @dispatcher.listeners(PostFoo).size.should eq 1\n    @dispatcher.listeners.size.should eq 2\n  end\n\n  def test_listener_callable : Nil\n    callback1 = PreFoo.callable do\n    end\n\n    callback2 = PostFoo.callable do\n    end\n\n    @dispatcher.listener callback1\n    @dispatcher.listener callback2\n\n    @dispatcher.has_listeners?.should be_true\n    @dispatcher.has_listeners?(PreFoo).should be_true\n    @dispatcher.has_listeners?(PostFoo).should be_true\n    @dispatcher.has_listeners?(PreBar).should be_false\n\n    @dispatcher.listeners(PreFoo).size.should eq 1\n    @dispatcher.listeners(PostFoo).size.should eq 1\n    @dispatcher.listeners.size.should eq 2\n  end\n\n  def test_listeners_sorted_by_priority : Nil\n    callback1 = PreFoo.callable(priority: -10) { }\n    callback2 = PreFoo.callable(priority: 10) { }\n    callback3 = PreFoo.callable { }\n    callback4 = PreFoo.callable(priority: 20) { }\n    callback5 = PreFoo.callable { }\n\n    @dispatcher.listener callback1\n    @dispatcher.listener callback2\n    @dispatcher.listener callback3\n    @dispatcher.listener callback4\n\n    # Returns a new copy with thew new priority set\n    callback5 = @dispatcher.listener callback5, priority: 5\n\n    @dispatcher.listeners(PreFoo).should eq([\n      callback4,\n      callback2,\n      callback5,\n      callback3,\n      callback1,\n    ])\n  end\n\n  def test_all_listeners_sorts_by_priority\n    callback1 = PreFoo.callable(priority: -10) { }\n    callback2 = PreFoo.callable { }\n    callback3 = PreFoo.callable(priority: 10) { }\n\n    callback4 = PostFoo.callable(priority: -10) { }\n    callback5 = PostFoo.callable { }\n    callback6 = PostFoo.callable(priority: 10) { }\n\n    @dispatcher.listener callback1\n    @dispatcher.listener callback2\n    @dispatcher.listener callback3\n    @dispatcher.listener callback4\n    @dispatcher.listener callback5\n    @dispatcher.listener callback6\n\n    @dispatcher.listeners.should eq({\n      PreFoo  => [callback3, callback2, callback1],\n      PostFoo => [callback6, callback5, callback4],\n    })\n  end\n\n  def test_listeners_are_sorted_stably : Nil\n    callback1 = PreFoo.callable(priority: -10) { }\n    callback2 = PreFoo.callable { }\n    callback3 = PreFoo.callable { }\n    callback4 = PreFoo.callable(priority: 10) { }\n\n    @dispatcher.listener callback1\n    @dispatcher.listener callback2\n    @dispatcher.listener callback3\n    @dispatcher.listener callback4\n\n    @dispatcher.listeners(PreFoo).should eq([\n      callback4,\n      callback2,\n      callback3,\n      callback1,\n    ])\n  end\n\n  def test_callable_exposes_correct_priority : Nil\n    callback1 = PreFoo.callable priority: -10 { }\n    callback2 = PreFoo.callable { }\n    callback3 = PreFoo.callable priority: 50 { }\n\n    @dispatcher.listener callback1\n    @dispatcher.listener callback2\n\n    # Returns a new copy with thew new priority set\n    callback3 = @dispatcher.listener callback3, priority: 10\n\n    callback1.priority.should eq -10\n    callback2.priority.should eq 0\n    callback3.priority.should eq 10\n    PreFoo.callable { }.priority.should eq 0\n  end\n\n  def test_dispatch : Nil\n    event = Sum.new\n\n    @dispatcher.listener Sum do |e|\n      e.value += 10\n    end\n\n    @dispatcher.listener PostFoo do\n    end\n\n    @dispatcher.dispatch event\n    @dispatcher.dispatch PostFoo.new\n\n    event.value.should eq 10\n  end\n\n  def test_dispatch_contract_event : Nil\n    event = ContractEvent.new\n\n    @dispatcher.listener ContractEvent do\n    end\n\n    returned_event = @dispatcher.dispatch event\n    returned_event.should be event\n  end\n\n  def test_dispatch_sub_dispatch : Nil\n    value = 0\n\n    @dispatcher.listener Sum do\n      value += 123\n    end\n\n    @dispatcher.listener PostFoo do |_, dispatcher|\n      dispatcher.dispatch Sum.new\n    end\n\n    @dispatcher.dispatch PostFoo.new\n\n    value.should eq 123\n  end\n\n  def test_dispatch_stop_event_propagation : Nil\n    pre_foo_invoked = false\n    other_pre_foo_invoked = false\n\n    @dispatcher.listener PreFoo do |event|\n      pre_foo_invoked = true\n      event.stop_propagation\n    end\n\n    @dispatcher.listener PreFoo do\n      other_pre_foo_invoked = true\n    end\n\n    @dispatcher.dispatch PreFoo.new\n\n    pre_foo_invoked.should be_true\n    other_pre_foo_invoked.should be_false\n  end\n\n  def test_listener_generic_polymorphism : Nil\n    animal_listener = AnimalListener.new\n\n    @dispatcher.listener animal_listener\n\n    @dispatcher.has_listeners?(GenericAnimalEvent(Cat)).should be_true\n    @dispatcher.has_listeners?(GenericAnimalEvent(Sloth)).should be_true\n    @dispatcher.has_listeners?(GenericAnimalEvent(ThreeToedSloth)).should be_true\n    @dispatcher.has_listeners?(GenericAnimalEvent(Dog)).should be_true\n\n    # Should not include module/abstract types that cannot actually exist.\n    @dispatcher.has_listeners?(GenericAnimalEvent(Animal)).should be_false\n    @dispatcher.has_listeners?(GenericAnimalEvent(SomeInterface)).should be_false\n    @dispatcher.has_listeners?(GenericAnimalEvent(ParentAnimal)).should be_false\n\n    @dispatcher.dispatch GenericAnimalEvent(Cat).new Cat.new\n    @dispatcher.dispatch GenericAnimalEvent(Sloth).new Sloth.new\n    @dispatcher.dispatch GenericAnimalEvent(Dog).new Dog.new\n    @dispatcher.dispatch GenericAnimalEvent(ThreeToedSloth).new ThreeToedSloth.new\n\n    animal_listener.all_animal_calls.should eq [Cat, Sloth, Dog, ThreeToedSloth]\n    animal_listener.only_child_animal_calls.should eq [Sloth, ThreeToedSloth]\n    animal_listener.only_interface_animal_calls.should eq [Cat, Sloth]\n    animal_listener.non_abstract_animal_calls.should eq [Sloth, ThreeToedSloth]\n  end\n\n  def test_remove_listener : Nil\n    callback1 = PreFoo.callable { }\n\n    @dispatcher.listener callback1\n    @dispatcher.has_listeners?(PreFoo).should be_true\n\n    @dispatcher.remove_listener callback1\n    @dispatcher.has_listeners?(PreFoo).should be_false\n\n    @dispatcher.remove_listener callback1\n  end\n\n  def test_remove_listener_via_get : Nil\n    @dispatcher.listener(PreFoo) { }\n\n    @dispatcher.has_listeners?(PreFoo).should be_true\n\n    @dispatcher.remove_listener @dispatcher.listeners(PreFoo).first\n\n    @dispatcher.has_listeners?(PreFoo).should be_false\n  end\n\n  def test_add_event_listener_instance\n    listener = TestListener.new\n\n    @dispatcher.listener listener\n\n    @dispatcher.has_listeners?(PreFoo).should be_true\n    @dispatcher.listeners(PreFoo).size.should eq 2\n    @dispatcher.listeners(PreFoo).map(&.name).should eq [\"TestListener#on_pre2\", \"TestListener#on_pre1\"]\n\n    @dispatcher.dispatch PreFoo.new\n\n    listener.values.should eq [2, 1]\n  end\n\n  def test_remove_event_listener_instance\n    listener = TestListener.new\n    listener2 = TestListener.new\n\n    @dispatcher.listener listener\n    @dispatcher.has_listeners?(PreFoo).should be_true\n    @dispatcher.listeners(PreFoo).size.should eq 2\n\n    @dispatcher.has_listeners?(PostFoo).should be_true\n    @dispatcher.listeners(PostFoo).size.should eq 1\n\n    @dispatcher.listener listener2\n    @dispatcher.has_listeners?(PreFoo).should be_true\n    @dispatcher.listeners(PreFoo).size.should eq 4\n\n    @dispatcher.has_listeners?(PostFoo).should be_true\n    @dispatcher.listeners(PostFoo).size.should eq 2\n\n    @dispatcher.remove_listener listener\n\n    @dispatcher.has_listeners?(PreFoo).should be_true\n    @dispatcher.listeners(PreFoo).size.should eq 2\n\n    @dispatcher.has_listeners?(PostFoo).should be_true\n    @dispatcher.listeners(PostFoo).size.should eq 1\n\n    @dispatcher.remove_listener listener2\n\n    @dispatcher.has_listeners?(PreFoo).should be_false\n    @dispatcher.has_listeners?(PostFoo).should be_false\n    @dispatcher.listeners.should be_empty\n  end\n\n  def test_remove_event_listener_instance_diff_instance\n    listener = TestListener.new\n    listener2 = TestListener.new\n\n    @dispatcher.listener listener\n    @dispatcher.listener listener2\n\n    @dispatcher.listeners(PreFoo).size.should eq 4\n\n    @dispatcher.remove_listener TestListener.new\n\n    @dispatcher.listeners(PreFoo).size.should eq 4\n\n    @dispatcher.remove_listener listener2\n\n    @dispatcher.listeners(PreFoo).size.should eq 2\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/spec/generic_event_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct GenericEventTest < ASPEC::TestCase\n  def test_with_arguments : Nil\n    event = AED::GenericEvent(String, Int32 | String).new(\n      \"foo\",\n      args = {\"counter\" => 0, \"data\" => \"bar\"}\n    )\n\n    event.subject.should eq \"foo\"\n    event.arguments.should eq args\n    event.arguments = {\"counter\" => 2} of String => Int32 | String\n    event.arguments.should eq({\"counter\" => 2})\n\n    event[\"counter\"].should eq 2\n    event[\"foo\"]?.should be_nil\n    event[\"counter\"] = 5\n    event[\"counter\"].should eq 5\n    event.has_key?(\"counter\").should be_true\n  end\n\n  def test_without_arguments : Nil\n    event = AED::GenericEvent.new \"foo\"\n    event.subject.should eq \"foo\"\n    event.arguments.should be_empty\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"athena-spec\"\nrequire \"../src/athena-event_dispatcher\"\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/event_dispatcher/src/annotations.cr",
    "content": "# Can be applied to method(s) within a type to denote that method is an event listener.\n# The annotation expects to be assigned to an instance method with between 1 and 2 parameters with a return type of `Nil`.\n# The first parameter should be the concrete `ACTR::EventDispatcher::Event` instance the method is listening on.\n# The optional second parameter should be typed as an `AED::EventDispatcherInterface`.\n#\n# The annotation accepts an optional `priority` field, defaulting to `0`, denoting the [listener's priority][Athena::EventDispatcher::EventDispatcherInterface--listener-priority]\n#\n# ```\n# class MyListener\n#   # Single parameter\n#   @[AEDA::AsEventListener]\n#   def single_param(event : MyEvent) : Nil\n#   end\n#\n#   # Double parameter\n#   @[AEDA::AsEventListener]\n#   def double_param(event : MyEvent, dispatcher : AED::EventDispatcherInterface) : Nil\n#   end\n#\n#   # With priority\n#   @[AEDA::AsEventListener(priority: 10)]\n#   def with_priority(event : MyEvent) : Nil\n#   end\n# end\n# ```\nannotation Athena::EventDispatcher::Annotations::AsEventListener; end\n"
  },
  {
    "path": "src/components/event_dispatcher/src/athena-event_dispatcher.cr",
    "content": "require \"athena-contracts/event_dispatcher\"\n\nrequire \"./annotations\"\nrequire \"./event_dispatcher\"\nrequire \"./generic_event\"\n\n# Convenience alias to make referencing `Athena::EventDispatcher` types easier.\nalias AED = Athena::EventDispatcher\n\n# Convenience alias to make referencing `AED::Annotations` types easier.\nalias AEDA = AED::Annotations\n\nmodule Athena::EventDispatcher\n  VERSION = \"0.4.1\"\n\n  # Contains all the `Athena::EventDispatcher` based annotations.\n  module Annotations; end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/src/callable.cr",
    "content": "# Encapsulates everything required to represent an event listener.\n# Including what event is being listened on, the callback itself, and its priority.\n#\n# Each subclass represents a specific \"type\" of listener.\n# See each subclass for more information.\n#\n# TIP: These types can be manually instantiated and added via the related `AED::EventDispatcherInterface#listener(callable)` overload.\n# This can be useful as a point of integration to other libraries, such as lazily instantiating listener instances.\n#\n# ### Name\n#\n# Each callable also has an optional *name* that can be useful for debugging to allow identifying a specific callable\n# since there would be no way to tell apart two listeners on the same event, with the same priority.\n#\n# ```\n# class MyEvent < AED::Event; end\n#\n# dispatcher = AED::EventDispatcher.new\n#\n# dispatcher.listener(MyEvent) { }\n# dispatcher.listener(MyEvent, name: \"block-listener\") { }\n#\n# class MyListener\n#   @[AEDA::AsEventListener]\n#   def on_my_event(event : MyEvent) : Nil\n#   end\n# end\n#\n# dispatcher.listener MyListener.new\n#\n# dispatcher.listeners(MyEvent).map &.name # => [\"unknown callable\", \"block-listener\", \"MyListener#on_my_event\"]\n# ```\n#\n# `AED::Callable::EventListenerInstance` instances registered via `AED::EventDispatcherInterface#listener(listener)` will automatically have a name including the\n# method and listener class names in the format of `ClassName#method_name`.\nabstract struct Athena::EventDispatcher::Callable\n  include Comparable(self)\n\n  # Returns what `ACTR::EventDispatcher::Event` class this callable represents.\n  getter event_class : ACTR::EventDispatcher::Event.class\n\n  # Returns the name of this callable.\n  # Useful for debugging to identify a specific callable added from a block, or which method an `AED::Callable::EventListenerInstance` is associated with.\n  getter name : String\n\n  # Returns the [listener priority][Athena::EventDispatcher::EventDispatcherInterface--listener-priority] of this callable.\n  getter priority : Int32\n\n  def initialize(\n    @event_class : ACTR::EventDispatcher::Event.class,\n    name : String?,\n    @priority : Int32,\n  )\n    @name = name || \"unknown callable\"\n  end\n\n  # :nodoc:\n  def <=>(other : AED::Callable) : Int32?\n    other.priority <=> @priority\n  end\n\n  # :nodoc:\n  def call(event : ACTR::EventDispatcher::Event, dispatcher : AED::EventDispatcherInterface) : NoReturn\n    raise \"BUG: Invoked wrong `call` overload\"\n  end\n\n  protected abstract def copy_with(priority _priority = @priority)\n\n  # Represents a listener that only accepts the `ACTR::EventDispatcher::Event` instance.\n  struct Event(E) < Athena::EventDispatcher::Callable\n    @callback : E -> Nil\n\n    def initialize(\n      @callback : E -> Nil,\n      priority : Int32 = 0,\n      name : String? = nil,\n      event_class : E.class = E,\n    )\n      super event_class, name, priority\n    end\n\n    # :nodoc:\n    def_equals @event_class, @priority, @callback\n\n    # :nodoc:\n    def call(event : E, dispatcher : AED::EventDispatcherInterface) : Nil\n      @callback.call event\n    end\n\n    protected def copy_with(priority _priority = @priority) : self\n      Event(E).new(\n        callback: @callback,\n        priority: _priority,\n      )\n    end\n  end\n\n  # Represents a listener that accepts both the `ACTR::EventDispatcher::Event` instance and the `AED::EventDispatcherInterface` instance.\n  # Such as when using [AED::EventDispatcherInterface#listener(event_class,*,priority,&)][Athena::EventDispatcher::EventDispatcherInterface#listener(callable,*,priority)], or the `AED::Event.callable` method.\n  struct EventDispatcher(E) < Athena::EventDispatcher::Callable\n    @callback : E, AED::EventDispatcherInterface -> Nil\n\n    def initialize(\n      @callback : E, AED::EventDispatcherInterface -> Nil,\n      priority : Int32 = 0,\n      name : String? = nil,\n      event_class : E.class = E,\n    )\n      super event_class, name, priority\n    end\n\n    # :nodoc:\n    def_equals @event_class, @priority, @callback\n\n    # :nodoc:\n    def call(event : E, dispatcher : AED::EventDispatcherInterface) : Nil\n      @callback.call event, dispatcher\n    end\n\n    protected def copy_with(priority _priority = @priority) : self\n      EventDispatcher(E).new(\n        callback: @callback,\n        priority: _priority,\n      )\n    end\n  end\n\n  # Represents a dedicated type based listener using `AEDA::AsEventListener` annotations.\n  struct EventListenerInstance(I, E) < Athena::EventDispatcher::Callable\n    # Returns the listener instance this callable is associated with.\n    getter instance : I\n\n    @callback : Proc(E, Nil) | Proc(E, AED::EventDispatcherInterface, Nil)\n\n    def initialize(\n      @callback : Proc(E, Nil) | Proc(E, AED::EventDispatcherInterface, Nil),\n      @instance : I,\n      priority : Int32 = 0,\n      name : String? = nil,\n      event_class : E.class = E,\n    )\n      super event_class, name || \"unknown #{@instance.class} method\", priority\n    end\n\n    # :nodoc:\n    def_equals @event_class, @priority, @callback, @instance\n\n    # :nodoc:\n    def call(event : ACTR::EventDispatcher::Event, dispatcher : AED::EventDispatcherInterface) : Nil\n      return unless event.is_a?(E)\n\n      case cb = @callback\n      when Proc(E, Nil)                                then cb.call event\n      when Proc(E, AED::EventDispatcherInterface, Nil) then cb.call event, dispatcher\n      else\n        raise \"BUG: Tried to call unknown event type.\"\n      end\n    end\n\n    protected def copy_with(priority _priority = @priority) : self\n      EventListenerInstance(I, E).new(\n        callback: @callback,\n        instance: @instance,\n        priority: _priority,\n      )\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/src/event.cr",
    "content": "# Extension of `ACTR::EventDispatcher::Event` to add additional functionality.\n#\n# ## Generics\n#\n# Events with generic type variables are also supported, the `AED::GenericEvent` event is an example of this.\n# Listeners on events with generics are a bit unique in how they behave in that each unique instantiation is treated as its own event.\n# For example:\n#\n# ```\n# class Foo; end\n#\n# subject = Foo.new\n#\n# dispatcher.listener AED::GenericEvent(Foo, Int32) do |e|\n#   e[\"counter\"] += 1\n# end\n#\n# dispatcher.listener AED::GenericEvent(String, String) do |e|\n#   e[\"class\"] = e.subject.upcase\n# end\n#\n# dispatcher.dispatch AED::GenericEvent.new subject, data = {\"counter\" => 0}\n#\n# data[\"counter\"] # => 1\n#\n# dispatcher.dispatch AED::GenericEvent.new \"foo\", data = {\"bar\" => \"baz\"}\n#\n# data[\"class\"] # => \"FOO\"\n# ```\n#\n# Notice that the listeners are registered with the generic types included.\n# This allows the component to treat `AED::GenericEvent(String, Int32)` differently than `AED::GenericEvent(String, String)`.\n# The added benefit of this is that the listener is also aware of the type returned by the related methods, so no manual casting is required.\n#\n# TIP: Use type aliases to give better names to commonly used generic types.\n#\n# ```\n# alias UserCreatedEvent = AED::GenericEvent(User, String)\n# ```\n#\n# ### Polymorphism\n#\n# There is special handling for an event class has a single generic type variable.\n# When used within an event listener, if the generic type has child types or is included in other types (when it's a module), then that listener will be registered for each concrete descendant of that type.\n#\n# ```\n# abstract struct Animal; end\n#\n# struct Dog < Animal; end\n#\n# struct Cat < Animal; end\n#\n# class GenericAnimalEvent(T) < AED::Event\n#   getter animal : T\n#\n#   def initialize(@animal : T); end\n# end\n#\n# class AnimalsListener\n#   @[AEDA::AsEventListener]\n#   def all_animals(event : GenericAnimalEvent(Animal)) : Nil\n#     pp \"All Animals: #{event.animal}\"\n#   end\n#\n#   @[AEDA::AsEventListener]\n#   def dog_only(event : GenericAnimalEvent(Dog)) : Nil\n#     pp \"Dog Only: #{event.animal}\"\n#   end\n# end\n#\n# dispatcher = AED::EventDispatcher.new\n# animal_listener = AnimalsListener.new\n# dispatcher.listener animal_listener\n#\n# dispatcher.dispatch GenericAnimalEvent(Cat).new Cat.new\n# dispatcher.dispatch GenericAnimalEvent(Dog).new Dog.new\n# # \"All Animals: Cat()\"\n# # \"All Animals: Dog()\"\n# # \"Dog Only: Dog()\"\n# ```\n#\n# In this example, notice how the `all_animals` event listener's *event* parameter is typed as `GenericAnimalEvent(Animal)` and gets invoked for both children of the abstract `Animal` type.\n# Whereas the event for the `dog_only` event listener is typed as `GenericAnimalEvent(Dog)`, so it only gets invoked once when the animal is a `Dog`.\nabstract class Athena::EventDispatcher::Event < Athena::Contracts::EventDispatcher::Event\n  # Returns an `AED::Callable` based on the event class the method was called on.\n  # Optionally allows customizing the *priority* and *name* of the listener.\n  #\n  # ```\n  # class MyEvent < AED::Event; end\n  #\n  # callable = MyEvent.callable do |event, dispatcher|\n  #   # Do something with the event, and/or dispatcher\n  # end\n  #\n  # dispatcher.listener callable\n  # ```\n  #\n  # Essentially the same as using [AED::EventDispatcherInterface#listener(event_class,*,priority,&)][Athena::EventDispatcher::EventDispatcherInterface#listener(callable,*,priority)], but removes the need to pass the *event_class*.\n  def self.callable(*, priority : Int32 = 0, name : String? = nil, &block : self, AED::EventDispatcherInterface -> Nil) : AED::Callable\n    AED::Callable::EventDispatcher(self).new block, priority, name\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/src/event_dispatcher.cr",
    "content": "require \"./event_dispatcher_interface\"\nrequire \"./event\"\nrequire \"./callable\"\n\n# Default implementation of `AED::EventDispatcherInterface`.\nclass Athena::EventDispatcher::EventDispatcher\n  include Athena::EventDispatcher::EventDispatcherInterface\n\n  @listeners = Hash(ACTR::EventDispatcher::Event.class, Array(AED::Callable)).new\n\n  # Keeps track of event types that have already been sorted.\n  @sorted = Set(ACTR::EventDispatcher::Event.class).new\n\n  # :inherit:\n  def dispatch(event : ACTR::EventDispatcher::Event) : ACTR::EventDispatcher::Event\n    self.call_listeners event, self.listeners event.class\n\n    event\n  end\n\n  # :inherit:\n  def has_listeners? : Bool\n    @listeners.each_value.any? { |listeners| !listeners.empty? }\n  end\n\n  # :inherit:\n  def has_listeners?(event_class : ACTR::EventDispatcher::Event.class) : Bool\n    @listeners.has_key? event_class\n  end\n\n  # :inherit:\n  def listener(callable : AED::Callable) : AED::Callable\n    self.add_callable callable\n  end\n\n  # :inherit:\n  def listener(callable : AED::Callable, *, priority : Int32) : AED::Callable\n    self.add_callable callable.copy_with priority: priority\n  end\n\n  # :inherit:\n  def listener(event_class : E.class, *, priority : Int32 = 0, name : String? = nil, &block : E, AED::EventDispatcherInterface -> Nil) : AED::Callable forall E\n    {%\n      unless E <= ACTR::EventDispatcher::Event\n        @def.args[0].raise \"expected argument #1 to '#{@def.name}' to be #{ACTR::EventDispatcher::Event.class}, not #{E}.\"\n      end\n    %}\n\n    self.add_callable AED::Callable::EventDispatcher(E).new block, priority, name\n  end\n\n  # :inherit:\n  def listener(listener : T) : Nil forall T\n    {% begin %}\n      {% listeners = [] of Nil %}\n\n      {%\n        class_listeners = T.class.methods.select &.annotation(AEDA::AsEventListener)\n\n        # Raise compile time error if a listener is defined as a class method.\n        unless class_listeners.empty?\n          class_listeners.first.raise \"Event listener methods can only be defined as instance methods. Did you mean '#{T.name}##{class_listeners.first.name}'?\"\n        end\n\n        T.methods.select(&.annotation(AEDA::AsEventListener)).each do |m|\n          # Validate the parameters of each method.\n          if (m.args.size < 1) || (m.args.size > 2)\n            m.raise \"Expected '#{T.name}##{m.name}' to have 1..2 parameters, got '#{m.args.size}'.\"\n          end\n\n          event_arg = m.args[0]\n\n          # Validate the type restriction of the first parameter, if present\n          if event_arg.restriction.is_a?(Nop)\n            event_arg.raise \"'#{T.name}##{m.name}': event parameter must have a type restriction of an 'Athena::Contracts::EventDispatcher::Event' instance.\"\n          end\n\n          if !(event_arg.restriction.resolve <= ACTR::EventDispatcher::Event)\n            event_arg.raise \"'#{T.name}##{m.name}': event parameter must have a type restriction of an 'Athena::Contracts::EventDispatcher::Event' instance, not '#{event_arg.restriction}'.\"\n          end\n\n          if dispatcher_arg = m.args[1]\n            if dispatcher_arg.restriction.is_a?(Nop)\n              dispatcher_arg.raise \"'#{T.name}##{m.name}': dispatcher parameter must have a type restriction of 'AED::EventDispatcherInterface'.\"\n            end\n\n            if !(dispatcher_arg.restriction.resolve <= AED::EventDispatcherInterface)\n              dispatcher_arg.raise \"'#{T.name}##{m.name}': dispatcher parameter must have a type restriction of 'AED::EventDispatcherInterface', not '#{dispatcher_arg.restriction}'.\"\n            end\n          end\n\n          priority = m.annotation(AEDA::AsEventListener)[:priority] || 0\n\n          unless priority.is_a? NumberLiteral\n            m.raise \"Event listener method '#{T.name}##{m.name}' expects a 'NumberLiteral' for its 'AEDA::AsEventListener#priority' field, but got a '#{priority.class_name.id}'.\"\n          end\n\n          # A listener whose event arg is `Foo(Animal)` will not be invoked for a dispatched `Foo(Dog)`, because the dispatcher matches on exact `event.class`.\n          # To work around this, register a listener for each non-abstract descendant of that type.\n          restriction = event_arg.restriction\n          if restriction.is_a?(Generic) && restriction.type_vars.size == 1\n            type_param = restriction.type_vars.first.resolve\n            concrete_types = [type_param] + type_param.all_subclasses + type_param.includers\n            concrete_types.each do |concrete_type|\n              concrete_event = \"#{restriction.name}(#{concrete_type})\".id\n              listeners << {concrete_event, m.args.size, m.name.id, priority} if !(concrete_type.abstract? || concrete_type.module?)\n            end\n          else\n            listeners << {restriction.id, m.args.size, m.name.id, priority}\n          end\n        end\n      %}\n\n      {% for info in listeners %}\n        {% event, count, method, priority = info %}\n\n        {% if 1 == count %}\n          self.add_callable(\n            AED::Callable::EventListenerInstance(T, {{event}}).new(\n              ->listener.{{method}}({{event}}),\n              listener,\n              {{priority}},\n              \"{{T}}##{{{method.stringify}}}\"\n            )\n          )\n        {% else %}\n          self.add_callable(\n            AED::Callable::EventListenerInstance(T, {{event}}).new(\n              ->listener.{{method}}({{event}}, AED::EventDispatcherInterface),\n              listener,\n              {{priority}},\n              \"{{T}}##{{{method.stringify}}}\"\n            )\n          )\n        {% end %}\n      {% end %}\n    {% end %}\n  end\n\n  protected def add_callable(callable : AED::Callable) : AED::Callable\n    (@listeners[callable.event_class] ||= Array(Callable).new) << callable\n\n    @sorted.delete callable.event_class\n\n    callable\n  end\n\n  # :inherit:\n  def listeners : Hash(ACTR::EventDispatcher::Event.class, Array(AED::Callable))\n    @listeners.each_key do |ec|\n      self.sort_listeners ec unless @sorted.includes? ec\n    end\n\n    @listeners\n  end\n\n  # :inherit:\n  def listeners(for event_class : ACTR::EventDispatcher::Event.class) : Array(AED::Callable)\n    return [] of AED::Callable unless @listeners.has_key? event_class\n\n    unless @sorted.includes? event_class\n      self.sort_listeners event_class\n    end\n\n    @listeners[event_class]\n  end\n\n  # :inherit:\n  def remove_listener(callable : AED::Callable) : Nil\n    return unless listeners = @listeners[callable.event_class]?\n\n    listeners.reject! { |c| c == callable }\n\n    @listeners.delete callable.event_class if listeners.empty?\n    @sorted.delete callable.event_class\n  end\n\n  # :inherit:\n  def remove_listener(listener : T) : Nil forall T\n    @listeners.each do |event_class, listeners|\n      listeners.reject! { |l| l.is_a?(AED::Callable::EventListenerInstance) && l.instance == listener }\n\n      @listeners.delete event_class if listeners.empty?\n    end\n  end\n\n  private def call_listeners(event : ACTR::EventDispatcher::Event, listeners : Array(AED::Callable)) : Nil\n    listeners.each do |listener|\n      break if event.is_a?(ACTR::EventDispatcher::StoppableEvent) && !event.propagate?\n\n      listener.call event, self\n    end\n  end\n\n  private def sort_listeners(event_class : ACTR::EventDispatcher::Event.class) : Nil\n    # Use stable sort to ensure callables with priority of `0` are invoked in the order they were inserted\n    @listeners[event_class].sort!\n    @sorted << event_class\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/src/event_dispatcher_interface.cr",
    "content": "# An event dispatcher is the primary type of `Athena::EventDispatcher`.\n# Extends `ACTR::EventDispatcher::Interface` to add additional functionality.\n# It maintains a registry of listeners, with events also being dispatched via this type.\n# When dispatched, the dispatcher notifies all listeners registered with that event.\n#\n# ## Usage\n#\n# Listeners can be added in a few ways, with the simplest being registering a block directly on the dispatcher instance.\n#\n# ```\n# class MyEvent < ACTR::EventDispatcher::Event; end\n#\n# dispatcher.listener MyEvent do |event, dispatcher|\n#   # Do something with the event, and/or dispatcher\n# end\n# ```\n#\n# Another way involves passing an `AED::Callable` instance, created manually or via the `AED::Event.callable` method.\n# Lastly, a type that has one or more `AEDA::AsEventListener` annotated methods may also be passed.\n#\n# Once all listeners are registered, you can begin to dispatch events.\n# Dispatching an event is simply calling the `#dispatch` method with an `ACTR::EventDispatcher::Event` subclass instance as an argument.\n#\n# ### Listener Priority\n#\n# As you may have noticed, each way of registering a listener has an optional *priority* parameter.\n# This value can be a positive or negative integer, with a default of `0` that controls the order in which each listener is executed.\n# The higher the value, the sooner that listener would be executed.\n# If two listeners have the same priority, they are executed in the order in which they were registered with the dispatcher.\n#\n# ```\n# class MyEvent < ACTR::EventDispatcher::Event; end\n#\n# dispatcher = AED::EventDispatcher.new\n# dispatcher.listener(MyEvent, priority: -10) { pp \"callback1\" }\n# dispatcher.listener(MyEvent, priority: 10) { pp \"callback2\" }\n# dispatcher.listener(MyEvent) { pp \"callback3\" }\n# dispatcher.listener(MyEvent, priority: 20) { pp \"callback4\" }\n# dispatcher.listener(MyEvent) { pp \"callback5\" }\n#\n# dispatcher.dispatch MyEvent.new\n# # =>\n# #   \"callback4\"\n# #   \"callback2\"\n# #   \"callback3\"\n# #   \"callback5\"\n# #   \"callback1\"\n# ```\n#\n# NOTE: While the priority can be any `Int32`, best practices suggest keeping it in the `-255..255` range.\nmodule Athena::EventDispatcher::EventDispatcherInterface\n  include Athena::Contracts::EventDispatcher::Interface\n\n  # Returns `true` if there are any listeners on any event.\n  abstract def has_listeners? : Bool\n\n  # Returns `true` if this dispatcher has any listeners on the provided *event_class*.\n  abstract def has_listeners?(event_class : ACTR::EventDispatcher::Event.class) : Bool\n\n  # Registers the provided *callable* listener to this dispatcher.\n  abstract def listener(callable : AED::Callable) : AED::Callable\n\n  # Registers the provided *callable* listener to this dispatcher, overriding its priority with that of the provided *priority*.\n  abstract def listener(callable : AED::Callable, *, priority : Int32) : AED::Callable\n\n  # Registers the block as an `AED::Callable` on the provided *event_class*, optionally with the provided *priority* and/or *name*.\n  abstract def listener(event_class : E.class, *, priority : Int32 = 0, name : String? = nil, &block : E, AED::EventDispatcherInterface -> Nil) : AED::Callable forall E\n\n  # Registers the provided *listener* instance to this dispatcher.\n  #\n  # `T` is any type that has methods annotated with `AEDA::AsEventListener`.\n  def listener(listener : T) : Nil forall T\n    # TODO: Make this actually abstract once https://github.com/crystal-lang/crystal/issues/14451 is resolved.\n    {% @type.raise \"abstract `def Athena::EventDispatcher::EventDispatcherInterface#listener(listener : T) : Nil forall T` must be implemented by #{@type}\" %}\n  end\n\n  # Returns a hash of all registered listeners as a `Hash(ACTR::EventDispatcher::Event.class, Array(AED::Callable))`.\n  abstract def listeners : Hash(ACTR::EventDispatcher::Event.class, Array(AED::Callable))\n\n  # Returns an `Array(AED::Callable)` for all listeners on the provided *event_class*.\n  abstract def listeners(for event_class : ACTR::EventDispatcher::Event.class) : Array(AED::Callable)\n\n  # Deregisters the provided *callable* from this dispatcher.\n  #\n  # TIP: The callable may be one retrieved via either `#listeners` method.\n  abstract def remove_listener(callable : AED::Callable) : Nil\n\n  # Deregisters listeners based on the provided *listener* from this dispatcher.\n  #\n  # `T` is any type that has methods annotated with `AEDA::AsEventListener`.\n  def remove_listener(listener : T) : Nil forall T\n    # TODO: Make this actually abstract once https://github.com/crystal-lang/crystal/issues/14451 is resolved.\n    {% @type.raise \"abstract `def Athena::EventDispatcher::EventDispatcherInterface#remove_listener(listener : T) : Nil forall T` must be implemented by #{@type}\" %}\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/src/generic_event.cr",
    "content": "# An extension of `AED::Event` that provides a generic event type that can be used in place of dedicated event types.\n# Allows using various instantiations of this one event type to handle multiple events.\n#\n# INFO: This type is provided for convenience for use within simple use cases.\n# Dedicated event types are still considered a best practice.\n#\n# ## Usage\n#\n# A generic event consists of a `#subject` of type `S`, which is some object/value representing an event that has occurred.\n# `#arguments` of type `V` may also be provided to augment the event with additional context, which is modeled as a `Hash(String, V)`.\n#\n# ```\n# dispatcher.dispatch(\n#   AED::GenericEvent(MyClass, Int32 | String).new(\n#     my_class_instance,\n#     {\"counter\" => 0, \"data\" => \"bar\"}\n#   )\n# )\n# ```\n#\n# Refer to [AED::Event][Athena::EventDispatcher::Event--generics] for examples of how listeners on events with generics behave.\n#\n# TODO: Make this include `Mappable` when/if https://github.com/crystal-lang/crystal/issues/10886 is implemented.\nclass Athena::EventDispatcher::GenericEvent(S, V) < Athena::EventDispatcher::Event\n  # Returns the subject of this event.\n  getter subject : S\n\n  # Returns the extra information stored with this event.\n  getter arguments : Hash(String, V)\n\n  # Sets the extra information that should be stored with this event.\n  setter arguments : Hash(String, V)\n\n  def self.new(subject : S)\n    AED::GenericEvent(S, NoReturn).new subject, Hash(String, NoReturn).new\n  end\n\n  def initialize(\n    @subject : S,\n    @arguments : Hash(String, V),\n  ); end\n\n  # Returns the argument with the provided *key*, raising if it does not exist.\n  def [](key : String) : V\n    @arguments[key]\n  end\n\n  # Returns the argument with the provided *key*, or `nil` if it does not exist.\n  def []?(key : String) : V?\n    @arguments[key]?\n  end\n\n  # Sets the argument with the provided *key* to the provided *value*.\n  def []=(key : String, value : V) : Nil\n    @arguments[key] = value\n  end\n\n  # Returns `true` if there is an argument with the provided *key*, otherwise `false`.\n  def has_key?(key : String) : Bool\n    @arguments.has_key? key\n  end\nend\n"
  },
  {
    "path": "src/components/event_dispatcher/src/spec.cr",
    "content": "require \"spec\"\n\n# A set of testing utilities/types to aid in testing `Athena::EventDispatcher` related types.\n#\n# ### Getting Started\n#\n# Require this module in your `spec_helper.cr` file.\n#\n# ```\n# # This also requires \"spec\".\n# require \"athena-event_dispatcher/spec\"\n# ```\nmodule Athena::EventDispatcher::Spec\n  # Test implementation of `AED::EventDispatcherInterface` that keeps track of the events that were dispatched.\n  #\n  # ```\n  # class MyEvent < AED::Event; end\n  #\n  # class OtherEvent < AED::Event; end\n  #\n  # dispatcher = AED::Spec::TracableEventDispatcher.new\n  #\n  # dispatcher.dispatch MyEvent.new\n  # dispatcher.dispatch OtherEvent.new\n  #\n  # dispatcher.emitted_events # => [MyEvent, OtherEvent]\n  # ```\n  class TracableEventDispatcher < AED::EventDispatcher\n    # Returns an array of each `Athena::Contracts::EventDispatcher::Event.class` that was dispatched via this dispatcher.\n    getter emitted_events : Array(ACTR::EventDispatcher::Event.class) = [] of ACTR::EventDispatcher::Event.class\n\n    # :inherit:\n    def dispatch(event : ACTR::EventDispatcher::Event) : Nil\n      @emitted_events << event.class\n\n      super\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/framework/.gitignore",
    "content": "*.dwarf\n/.shards/\n/bin/\n/lib/\n/logs/\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in application that uses them\n/shard.lock\n"
  },
  {
    "path": "src/components/framework/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.22.0] - 2026-04-19\n\n### Changed\n\n- **Breaking:** Store `ATH::Action` within `ATH::Request#attributes` instead of within an ivar ([#636]) (George Dietrich) <!-- blacksmoke16 -->\n- **Breaking:** Extract out HTTP related `framework` types into the new `http` component ([#640]) (George Dietrich) <!-- blacksmoke16 -->\n- **Breaking:** Extract out Request/Response handling related `framework` types into the new `http_kernel` component ([#657]) (George Dietrich) <!-- blacksmoke16 -->\n- **Breaking:** Refactor how annotations are fetched off an action/parameter ([#655]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix CORS error when using HTTP/2 but providing uppercase header names ([#670]) (George Dietrich) <!-- blackmsoke16 -->\n- Fix compile time error when inadvertently using a type name that conflicts with an internal component type ([#678]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.22.0]: https://github.com/athena-framework/framework/releases/tag/v0.22.0\n[#636]: https://github.com/athena-framework/athena/pull/636\n[#640]: https://github.com/athena-framework/athena/pull/640\n[#657]: https://github.com/athena-framework/athena/pull/657\n[#655]: https://github.com/athena-framework/athena/pull/655\n[#670]: https://github.com/athena-framework/athena/pull/670\n[#678]: https://github.com/athena-framework/athena/pull/678\n\n## [0.21.1] - 2025-10-04\n\n### Fixed\n\n- Fix improper handling of optional file uploads ([#595]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.21.1]: https://github.com/athena-framework/framework/releases/tag/v0.21.1\n[#595]: https://github.com/athena-framework/athena/pull/595\n\n## [0.21.0] - 2025-09-04\n\n### Changed\n\n- **Breaking:** Leverage `ATH::AbstractFile` within `ATH::BinaryFileResponse` ([#563]) (George Dietrich) <!-- blacksmoke16 -->\n- Leverage `mime` component within `ATH::BinaryFileResponse` ([#545]) (George Dietrich) <!-- blacksmoke16 -->\n- Setter methods on `ATH::Response` and subclasses now return `self` to better support method chaining ([#563]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Add support for Athena Contract component types ([#544]) (George Dietrich) <!-- blacksmoke16 -->\n- Add native file upload support ([#559]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Correctly apply `emit_nil` value from `ATHA::View` ([#526]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.21.0]: https://github.com/athena-framework/framework/releases/tag/v0.21.0\n[#545]: https://github.com/athena-framework/athena/pull/545\n[#563]: https://github.com/athena-framework/athena/pull/563\n[#544]: https://github.com/athena-framework/athena/pull/544\n[#559]: https://github.com/athena-framework/athena/pull/559\n[#526]: https://github.com/athena-framework/athena/pull/526\n\n## [0.20.1] - 2025-02-08\n\n### Fixed\n\n- Fix `ATH::ViewHandler` bundle configuration values not being correctly set ([#520]) (George Dietrich)\n\n[0.20.1]: https://github.com/athena-framework/framework/releases/tag/v0.20.1\n[#520]: https://github.com/athena-framework/athena/pull/520\n\n## [0.20.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n- **Breaking:** The `ATHR::Interface.configuration` macro is no longer scoped to the resolver namespace ([#425]) (George Dietrich)\n- **Breaking:** Rename `ATHR::RequestBody::Extract` to `ATHA::MapRequestBody` ([#425]) (George Dietrich)\n- **Breaking:** Rename `ATHR::Time::Format` to `ATHA::MapTime` ([#425]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.14.0` ([#433]) (George Dietrich)\n- Refactor auto redirection logic to be more robust ([#436], [#480]) (George Dietrich)\n- Refactor `ATHR::RequestBody` to raise more accurate deserialization errors ([#490]) (George Dietrich)\n\n### Added\n\n- Add support for [Proxies & Load Balancers](https://athenaframework.org/guides/proxies/) ([#440], [#444]) (George Dietrich)\n- Add new `trusted_host` bundle scheme property to allow setting trusted hostnames ([#474]) (George Dietrich)\n- Add support for deserializing `application/x-www-form-urlencoded` bodies via `ATHA::MapRequestBody` ([#477]) (George Dietrich)\n- Add `ATHA::MapQueryString` to map a request's query string into a DTO type ([#477]) (George Dietrich)\n- Add `ATH::Exception.from_status` helper method ([#426]) (George Dietrich)\n- Add `ATHA::MapQueryParameter` for handling query parameters ([#426]) (George Dietrich)\n- Add `#validation_groups` and `#accept_formats` annotation properties to `ATHA::MapRequestBody` ([#486]) (George Dietrich)\n- Add `#validation_groups` annotation property to `ATHA::MapQueryString` ([#486]) (George Dietrich)\n- Add `ATH::Request#port` and `ATH::Response#redirect?` methods ([#436]) (George Dietrich)\n- Add `#host`, `#scheme`, `#secure?`, and `#from_trusted_proxy?` methods to `ATH::Request` ([#440]) (George Dietrich)\n- Add `ATH::Request#content_type_format` to return the request format's name from its `content-type` header ([#477]) (George Dietrich)\n- Add `ATH::IPUtils` module ([#440]) (George Dietrich)\n- Add `.unquote`, `.split`, and `.combine` methods `ATH::HeaderUtils` ([#440]) (George Dietrich)\n- Add request matchers for headers and query parameters ([#491]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** Remove `ATHA::QueryParam` ([#426]) (George Dietrich)\n- **Breaking:** Remove `ATHA::RequestParam` ([#426]) (George Dietrich)\n- **Breaking:** Remove `ATH::Exception::InvalidParameter` ([#426]) (George Dietrich)\n- **Breaking:** Remove everything within `ATH::Params` namespace ([#426]) (George Dietrich)\n- **Breaking:** Remove `ATH::Action#params` ([#426]) (George Dietrich)\n- **Breaking:** Remove `ATH::Listeners::ParamFetcher` ([#426]) (George Dietrich)\n\n### Fixed\n\n- Fix query parameters being dropped when redirecting to a trailing/non-trailing slash endpoint ([#436]) (George Dietrich)\n- Fix auto redirection with non-standard ports ([#480]) (George Dietrich)\n- Fix `multipart/form-data` not being mapped to the `form` format ([#441]) (George Dietrich)\n- Fix being unable to provide the path of an `ARTA::Route` annotation on a class as a positional argument ([#482]) (George Dietrich)\n- Fix error when attempting to use `ATH::Controller#redirect_view` and `ATH::Controller#route_redirect_view` ([#498]) (George Dietrich)\n- Fix error when attempting to use `ATH::Spec::APITestCase#unlink` ([#498]) (George Dietrich)\n\n[0.20.0]: https://github.com/athena-framework/framework/releases/tag/v0.20.0\n[#425]: https://github.com/athena-framework/athena/pull/425\n[#426]: https://github.com/athena-framework/athena/pull/426\n[#428]: https://github.com/athena-framework/athena/pull/428\n[#433]: https://github.com/athena-framework/athena/pull/433\n[#436]: https://github.com/athena-framework/athena/pull/436\n[#440]: https://github.com/athena-framework/athena/pull/440\n[#441]: https://github.com/athena-framework/athena/pull/441\n[#444]: https://github.com/athena-framework/athena/pull/444\n[#474]: https://github.com/athena-framework/athena/pull/474\n[#477]: https://github.com/athena-framework/athena/pull/477\n[#480]: https://github.com/athena-framework/athena/pull/480\n[#482]: https://github.com/athena-framework/athena/pull/482\n[#486]: https://github.com/athena-framework/athena/pull/486\n[#490]: https://github.com/athena-framework/athena/pull/490\n[#491]: https://github.com/athena-framework/athena/pull/491\n[#498]: https://github.com/athena-framework/athena/pull/498\n\n## [0.19.2] - 2024-07-31\n\n### Added\n\n- Add `ATH.run_console` as an easier entrypoint into the console application ([#413]) (George Dietrich)\n- Add support for additional boolean conversion values from request attributes ([#422]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** `ATH::RequestMatcher::Method` now requires an `Array(String)` as opposed to any `Enumerable(String)` ([#431]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.13.0` ([#433]) (George Dietrich)\n- Updates usages of `UTF-8` in response headers to `utf-8` as preferred by the RFC ([#417]) (George Dietrich)\n\n### Fixed\n\n- Fix the content negotiation implementation not working ([#431]) (George Dietrich)\n\n[0.19.2]: https://github.com/athena-framework/framework/releases/tag/v0.19.2\n[#413]: https://github.com/athena-framework/athena/pull/413\n[#417]: https://github.com/athena-framework/athena/pull/417\n[#422]: https://github.com/athena-framework/athena/pull/422\n[#431]: https://github.com/athena-framework/athena/pull/431\n[#433]: https://github.com/athena-framework/athena/pull/433\n\n## [0.19.1] - 2024-04-27\n\n### Fixed\n\n- Fix `framework` component docs landing on an empty page ([#399]) (George Dietrich)\n- Fix `Athena::Clock` not being aliased to the interface correctly ([#400]) (George Dietrich)\n- Fix `ATHA::View` annotation being defined in incorrect namespace ([#403]) (George Dietrich)\n- Fix `ATH::ErrorRenderer` not being aliased to the interface correctly ([#404]) (George Dietrich)\n\n[0.19.1]: https://github.com/athena-framework/framework/releases/tag/v0.19.1\n[#399]: https://github.com/athena-framework/athena/pull/399\n[#400]: https://github.com/athena-framework/athena/pull/400\n[#403]: https://github.com/athena-framework/athena/pull/403\n[#404]: https://github.com/athena-framework/athena/pull/404\n\n## [0.19.0] - 2024-04-09\n\n### Changed\n\n- **Breaking:** change how framework features are configured ([#337], [#374], [#383]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.11.0` ([#270]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- Support for Windows OS ([#270]) (George Dietrich)\n- Add `ATH::RequestMatcher` as a generic way of matching an `ATH::Request` given a set of rules ([#338]) (George Dietrich)\n- Raise an exception if a controller's return value fails to serialize instead of just returning `nil` ([#357]) (George Dietrich)\n- Add support for new Crystal 1.12 `Process.on_terminate` method ([#394]) (George Dietrich)\n\n### Fixed\n\n- Fix macro splat deprecation ([#330]) (George Dietrich)\n- Normalize `ATH::Request#method` to always be uppercase ([#338]) (George Dietrich)\n- Fixed not being able to use top level configuration annotations on controller action parameters ([#356]) (George Dietrich)\n\n[0.19.0]: https://github.com/athena-framework/framework/releases/tag/v0.19.0\n[#270]: https://github.com/athena-framework/athena/pull/270\n[#330]: https://github.com/athena-framework/athena/pull/330\n[#337]: https://github.com/athena-framework/athena/pull/337\n[#338]: https://github.com/athena-framework/athena/pull/338\n[#356]: https://github.com/athena-framework/athena/pull/356\n[#357]: https://github.com/athena-framework/athena/pull/357\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#374]: https://github.com/athena-framework/athena/pull/374\n[#383]: https://github.com/athena-framework/athena/pull/383\n[#394]: https://github.com/athena-framework/athena/pull/394\n\n## [0.18.2] - 2023-10-09\n\n### Changed\n\n- Change routing logic to redirect `GET` and `HEAD` requests with a trailing slash to the route without one if it exists, and vice versa ([#307]) (George Dietrich)\n\n### Added\n\n- Add native tab completion support to the built-in `ATH::Commands` ([#296]) (George Dietrich)\n- Add support for defining multiple route annotations on a single controller action method ([#315]) (George Dietrich)\n- Require the new `Athena::Clock` component ([#318]) (George Dietrich)\n- Add additional `ATH::Spec::APITestCase` request helper methods ([#312], [#313]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrectly generated route paths with a controller level prefix and no action level `/` prefix ([#308]) (George Dietrich)\n\n[0.18.2]: https://github.com/athena-framework/framework/releases/tag/v0.18.2\n[#296]: https://github.com/athena-framework/athena/pull/296\n[#307]: https://github.com/athena-framework/athena/pull/307\n[#308]: https://github.com/athena-framework/athena/pull/308\n[#312]: https://github.com/athena-framework/athena/pull/312\n[#313]: https://github.com/athena-framework/athena/pull/313\n[#315]: https://github.com/athena-framework/athena/pull/315\n[#318]: https://github.com/athena-framework/athena/pull/318\n\n## [0.18.1] - 2023-05-29\n\n### Added\n\n- Add support for serializing arbitrarily nested controller action return types ([#273]) (George Dietrich)\n- Allow using constants for controller action's `path` ([#279]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect `content-length` header value when returning multi-byte strings ([#288]) (George Dietrich)\n\n[0.18.1]: https://github.com/athena-framework/framework/releases/tag/v0.18.1\n[#273]: https://github.com/athena-framework/athena/pull/273\n[#279]: https://github.com/athena-framework/athena/pull/279\n[#288]: https://github.com/athena-framework/athena/pull/288\n\n## [0.18.0] - 2023-02-20\n\n### Changed\n\n- **Breaking:** upgrade [Athena::EventDispatcher](https://athenaframework.org/EventDispatcher/) to [0.2.x](https://github.com/athena-framework/event-dispatcher/blob/master/CHANGELOG.md#020---2023-01-07) ([#205]) (George Dietrich)\n- **Breaking:** deprecate the `ATH::ParamConverter` concept in favor of [Value Resolvers](https://athenaframework.org/Framework/Controller/ValueResolvers/Interface) ([#243]) (George Dietrich)\n- **Breaking:** rename various types/methods to better adhere to https://github.com/crystal-lang/crystal/issues/10374 ([#243]) (George Dietrich)\n- **Breaking:** Change `ATH::Spec::AbstractBrowser` to be a `class` ([#249]) (George Dietrich)\n- **Breaking:** upgrade [Athena::Validator](https://athenaframework.org/Validator/) to [0.3.x](https://github.com/athena-framework/validator/blob/master/CHANGELOG.md#030---2023-01-07) ([#250]) (George Dietrich)\n- Improve service `ATH::Controller`s to not need the `public: true` `ADI::Register` field ([#213]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.6.0` ([#205]) (George Dietrich)\n\n### Added\n\n- Add trace logging to `ATH::Listeners::CORS` to aid in debugging ([#265]) (George Dietrich)\n- Introduce new `framework.debug` parameter that is `true` if the binary was _not_ built with the `--release` flag ([#249]) (George Dietrich)\n- Add built-in [HTTP Expectation](https://athenaframework.org/Framework/Spec/Expectations/HTTP) methods to `ATH::Spec::WebTestCase` ([#249]) (George Dietrich)\n- Add `#response` and `#request` methods to `ATH::Spec::AbstractBrowser` types ([#249]) (George Dietrich)\n- Add [ATHR](https://athenaframework.org/Framework/aliases/#ATHR) alias to make using value resolver annotations easier ([#243]) (George Dietrich)\n- Add [ATH::Commands::Commands::DebugEventDispatcher](https://athenaframework.org/Framework/Commands/DebugEventDispatcher) framework CLI command to aid in debugging the event dispatcher ([#241]) (George Dietrich)\n- Add [ATH::Commands::Commands::DebugRouter](https://athenaframework.org/Framework/Commands/DebugRouter) and [ATH::Commands::Commands::DebugRouterMatch](https://athenaframework.org/Framework/Commands/DebugRouterMatch) framework CLI commands to aid in debugging the router ([#224]) (George Dietrich)\n- Add integration for the [Athena::Console](https://athenaframework.org/Console/) component ([#218]) (George Dietrich)\n\n### Fixed\n\n- Correctly populate `content-length` based on the response content's size ([#267]) (George Dietrich)\n- Prevent wildcard CORS `expose_headers` value when `allow_credentials` is `true` ([#264]) (George Dietrich)\n- Correctly handle `JSON::Serializable` values within `Hash`/`NamedTuple` controller action return types ([#253]) (George Dietrich)\n- Fix [ATH::ParameterBag#get?](https://athenaframework.org/Framework/ParameterBag/#Athena::Framework::ParameterBag#get?(name,_type)) not returning `nil` if it could not convert the value to the desired type ([#243]) (George Dietrich)\n\n[0.18.0]: https://github.com/athena-framework/framework/releases/tag/v0.18.0\n[#205]: https://github.com/athena-framework/athena/pull/205\n[#213]: https://github.com/athena-framework/athena/pull/213\n[#218]: https://github.com/athena-framework/athena/pull/218\n[#224]: https://github.com/athena-framework/athena/pull/224\n[#241]: https://github.com/athena-framework/athena/pull/241\n[#243]: https://github.com/athena-framework/athena/pull/243\n[#249]: https://github.com/athena-framework/athena/pull/249\n[#250]: https://github.com/athena-framework/athena/pull/250\n[#253]: https://github.com/athena-framework/athena/pull/253\n[#264]: https://github.com/athena-framework/athena/pull/264\n[#265]: https://github.com/athena-framework/athena/pull/265\n[#267]: https://github.com/athena-framework/athena/pull/267\n\n## [0.17.1] - 2022-09-05\n\n### Changed\n\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n[0.17.1]: https://github.com/athena-framework/framework/releases/tag/v0.17.1\n[#188]: https://github.com/athena-framework/athena/pull/188\n\n## [0.17.0] - 2022-05-14\n\n_Checkout [this](https://forum.crystal-lang.org/t/athena-0-17-0/4624) forum thread for an overview of changes within the ecosystem._\n\n### Added\n\n- Add `pcre2` library dependency to `shard.yml` ([#159]) (George Dietrich)\n- Add [ATH::Arguments::Resolvers::Enum](https://athenaframework.org/Framework/Arguments/Resolvers/Enum/) to allow resolving `Enum` members directly to controller actions ([#173]) (George Dietrich)\n- Add [ATH::Arguments::Resolvers::UUID](https://athenaframework.org/Framework/Arguments/Resolvers/UUID/) to allow resolving `UUID`s directly to controller actions by ([#176]) (George Dietrich)\n- Add [ATH::ParameterBag#has(name, type)](https://athenaframework.org/Framework/ParameterBag/#Athena::Framework::ParameterBag#has?(name,type)) that checks if a parameter with the provided name exists, and that is of the provided type ([#176]) (George Dietrich)\n- Add [ATH::Arguments::Resolvers::DefaultValue](https://athenaframework.org/Framework/Arguments/Resolvers/DefaultValue/) to allow resolving an action parameter's default value if no other value was provided ([#177]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** rename `ATH::Arguments::Resolvers::ArgumentValueResolverInterface` to `ATH::Arguments::Resolvers::Interface` ([#176]) (George Dietrich)\n- **Breaking:** bump `athena-framework/serializer` to `~> 0.3.0` ([#181]) (George Dietrich)\n- **Breaking:** bump `athena-framework/validator` to `~> 0.2.0` ([#181]) (George Dietrich)\n- Expose the default value of an [ATH::Arguments::ArgumentMetadata](https://athenaframework.org/Framework/Arguments/ArgumentMetadata/) ([#176]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Fix error when two controller share a common action name ([#146]) (George Dietrich)\n- Fix release badge to use correct repo ([#161]) (George Dietrich)\n- Fix query/request param docs to use new error responses ([#167]) (George Dietrich)\n- Fix incorrect `Athena::Framework` `Log` name ([#175]) (George Dietrich)\n\n[0.17.0]: https://github.com/athena-framework/framework/releases/tag/v0.17.0\n[#146]: https://github.com/athena-framework/athena/pull/146\n[#159]: https://github.com/athena-framework/athena/pull/159\n[#161]: https://github.com/athena-framework/athena/pull/161\n[#167]: https://github.com/athena-framework/athena/pull/167\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#173]: https://github.com/athena-framework/athena/pull/173\n[#175]: https://github.com/athena-framework/athena/pull/175\n[#176]: https://github.com/athena-framework/athena/pull/176\n[#177]: https://github.com/athena-framework/athena/pull/177\n[#181]: https://github.com/athena-framework/athena/pull/181\n\n## [0.16.0] - 2022-01-22\n\n_First release in the [athena-framework/framework](https://github.com/athena-framework/framework) repo, post monorepo._\n\n### Added\n\n- Add dependency on `athena-framework/routing` ([#141]) (George Dietrich)\n- Allow prepending [HTTP::Handlers](https://crystal-lang.org/api/HTTP/Handler.html) to the Athena server ([#133]) (George Dietrich)\n- Add common HTTP methods (get, post, put, delete) to [ATH::Spec::APITestCase](https://athenaframework.org//Framework/Spec/APITestCase/#Athena::Framework::Spec::APITestCase-methods) ([#134]) (George Dietrich)\n- Add overload of [ATH::Spec::APITestCase#request](https://athenaframework.org/Framework/Spec/APITestCase/#Athena::Framework::Spec::APITestCase#request(method,path,body,headers)) that accepts an [ATH::Request](https://athenaframework.org/Framework/Request/) or [HTTP::Request](https://crystal-lang.org/api/HTTP/Request.html) ([#134]) (George Dietrich)\n- Allow running an HTTPS server via passing an [OpenSSL::SSL::Context::Server](https://crystal-lang.org/api/OpenSSL/SSL/Context/Server.html) to `ATH.run` ([#135], [#136]) (George Dietrich)\n- Add [ATH::ParameterBag#set(hash)](https://athenaframework.org/Framework/ParameterBag/#Athena::Framework::ParameterBag#set(name,value,type)) that allows setting a hash of key/value pairs ([#141]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** integrate the [Athena::Routing](https://athenaframework.org/Routing/) component ([#141]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** remove dependency on [amberframework/amber-router](https://github.com/amberframework/amber-router) ([#141]) (George Dietrich)\n\n[0.16.0]: https://github.com/athena-framework/framework/releases/tag/v0.16.0\n[#133]: https://github.com/athena-framework/athena/pull/133\n[#134]: https://github.com/athena-framework/athena/pull/134\n[#135]: https://github.com/athena-framework/athena/pull/135\n[#136]: https://github.com/athena-framework/athena/pull/136\n[#141]: https://github.com/athena-framework/athena/pull/141\n\n## [0.15.1] - 2021-12-13\n\n### Changed\n\n- Include error list in `ATH::Exception::InvalidParameter` ([#124]) (George Dietrich)\n- Set the base path of parameter errors to the name of the parameter ([#124]) (George Dietrich)\n\n[0.15.1]: https://github.com/athena-framework/athena/releases/tag/v0.15.1\n[#124]: https://github.com/athena-framework/athena/pull/124\n\n## [0.15.0] - 2021-10-30\n\n_Last release in the [athena-framework/athena](https://github.com/athena-framework/athena) repo, pre monorepo._\n\n### Added\n\n- Expose the raw [HTTP::Request](https://crystal-lang.org/api/HTTP/Request.html) method from an `ATH::Request` ([#115]) (George Dietrich)\n- Add built in [ATH::RequestBodyConverter](https://athenaframework.org/Framework/RequestBodyConverter) param converter ([#116]) (George Dietrich)\n- Add `VERSION` constant to `Athena::Framework` namespace ([#120]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** rename base param converter type to `ATH::ParamConverter` and make it a class ([#116]) (George Dietrich)\n- **Breaking:** rename the component from `Athena::Routing` to `Athena::Framework` ([#120]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect parameter type restriction on `ATH::ParameterBag#set` ([#116]) (George Dietrich)\n- Fix incorrect ivar type on `AVD::Exception::Exceptions::ValidationFailed#violations` ([#116]) (George Dietrich)\n- Correctly reject requests with whitespace when converting numeric inputs ([#117]) (George Dietrich)\n\n[0.15.0]: https://github.com/athena-framework/athena/releases/tag/v0.15.0\n[#115]: https://github.com/athena-framework/athena/pull/115\n[#116]: https://github.com/athena-framework/athena/pull/116\n[#117]: https://github.com/athena-framework/athena/pull/117\n[#120]: https://github.com/athena-framework/athena/pull/120\n\n"
  },
  {
    "path": "src/components/framework/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/framework/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020 George Dietrich\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": "src/components/framework/README.md",
    "content": "# Athena Framework\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/framework.svg)](https://github.com/athena-framework/framework/releases)\n\nA web framework comprised of reusable, independent components.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/getting_started).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/framework/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.22.0\n\n### Dedicated service for accessing annotations defined on a controller action and/or parameter\n\nThe `#annotation_configurations` getter on `ATH::Action` and `ATH::Controller::ParameterMetadata` has been removed.\nInject and use the new [ATH::AnnotationResolver](https://athenaframework.org/Framework/AnnotationResolver) service to access the annotations:\n\nBefore:\n\n```crystal\n# Action\nrequest.action.annotation_configurations\n\n# Parameter\nparameter.annotation_configurations\n```\n\nAfter:\n\n```crystal\n# Action\n@annotation_resolver.action_annotations(request)\n\n# Parameter\n@annotation_resolver.action_parameter_annotations(request, parameter.name)\n```\n\n### HTTP types extracted to dedicated component\n\nThe following types have been extracted from the `framework` component into the dedicated `http` component.\nAny references will need their namespace updated from `Athena::Framework` (`ATH`) to `Athena::HTTP` (`AHTTP`):\n\n- `Request`\n- `Response`\n- `ResponseHeaders`\n- `RedirectResponse`\n- `StreamedResponse`\n- `BinaryFileResponse`\n- `ParameterBag`\n- `HeaderUtils`\n- `IPUtils`\n- `RequestStore`\n- `AbstractFile`\n- `UploadedFile`\n- `RequestMatcher`\n- `RequestMatcher::Attributes`\n- `RequestMatcher::Header`\n- `RequestMatcher::Hostname`\n- `RequestMatcher::Method`\n- `RequestMatcher::Path`\n- `RequestMatcher::QueryParameter`\n- `Exception::ConflictingHeaders`\n- `Exception::SuspiciousOperation`\n- `Exception::RequestExceptionInterface`\n- `Exception::File`\n- `Exception::FileNotFound`\n- `Exception::FileSizeLimitExceeded`\n- `Exception::Logic`\n\n### Request/Response handling types extracted to dedicated component\n\nThe following types have been extracted from the `framework` component into the dedicated `http_kernel` component.\nAny references will need their namespace updated from `Athena::Framework` (`ATH`) to `Athena::HTTPKernel` (`AHK`):\n\n- `Action`\n- `ActionBase`\n- `Controller::ArgumentResolver`\n- `Controller::ArgumentResolverInterface`\n- `Controller::ParameterMetadata`\n- `Controller::ValueResolvers::DefaultValue`\n- `Controller::ValueResolvers::Request`\n- `Controller::ValueResolvers::RequestAttribute`\n- `ErrorRenderer`\n- `ErrorRendererInterface`\n- `Events::Action`\n- `Events::Exception`\n- `Events::Request`\n- `Events::RequestAware`\n- `Events::Response`\n- `Events::SettableResponse`\n- `Events::Terminate`\n- `Events::View`\n- `Exception::BadGateway`\n- `Exception::BadRequest`\n- `Exception::Conflict`\n- `Exception::Forbidden`\n- `Exception::Gone`\n- `Exception::HTTPException`\n- `Exception::LengthRequired`\n- `Exception::Logic`\n- `Exception::MethodNotAllowed`\n- `Exception::NotAcceptable`\n- `Exception::NotFound`\n- `Exception::NotImplemented`\n- `Exception::PreconditionFailed`\n- `Exception::ServiceUnavailable`\n- `Exception::TooManyRequests`\n- `Exception::Unauthorized`\n- `Exception::UnprocessableEntity`\n- `Exception::UnsupportedMediaType`\n- `Listeners::Error`\n- `Listeners::Routing`\n\n### Change how the `ATH::Action` is accessed\n\nThe `AHTTP::Request#action` getter used to access the matched `ATH::Action` instance has been removed.\nThe action is now stored within the request's attributes as `\"_action\"` and must be accessed via:\n```crystal\nrequest.attributes.get(\"_action\", AHK::ActionBase)\n````\n\n`#get?` may be used in place of `#action?` if it's not guaranteed the action exists.\n\n## Upgrade to 0.20.0\n\n### Change how query parameters are represented\n\nThe `ATHA::QueryParam` annotation applied to the controller action is replaced with the `ATHA::MapQueryParameter` annotation applied directly to the parameter.\n\nBefore:\n\n```crystal\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"/\")]\n  @[ATHA::QueryParam(\"page\")]\n  def index(page : Int32) : Int32\n    page\n  end\nend\n```\n\nAfter:\n\n```crystal\nclass ExampleController < ATH::Controller\n  @[ARTA::Get(\"/\")]\n  def index(@[ATHA::MapQueryParameter] page : Int32) : Int32\n    page\n  end\nend\n```\n\nSee the [API Docs](https://athenaframework.org/Framework/Controller/ValueResolvers/QueryParameter/#Athena::Framework::Controller::ValueResolvers::QueryParameter) for more information.\n\n### Change how request parameters are handled\n\nThe `ATHA::RequestParam` annotation that allowed mapping `x-www-form-urlencoded` form data within the request body to particular controller action parameters has been removed in favor of `ATHR::RequestBody`, which now supports deserializing form data request bodies into a DTO type.\n\nBefore:\n\n```crystal\nclass ExampleController < ATH::Controller\n  @[ARTA::Post(\"/login\")]\n  @[ATHA::RequestParam(\"username\")]\n  @[ATHA::RequestParam(\"password\")]\n  def login(username : String, password : String) : Nil\n    # ...\n  end\nend\n```\n\nAfter:\n\n```crystal\nrecord LoginDTO, username : String, password : String do\n  include URI::Params::Serializable\nend\n\nclass ExampleController < ATH::Controller\n  @[ARTA::Post(\"/login\")]\n  def login(@[ATHA::MapRequestBody] login : LoginDTO) : Nil\n    # ...\n  end\nend\n```\n\nThis provides better consistency and additional features such as adding validation constraints to the request parameters.\n\n### Normalization of Exception types\n\nThe namespace exception types live in has changed from `ATH::Exceptions` to `ATH::Exception`.\nAny usages of `framework` exception types will need to be updated.\n\nIf using a `rescue` statement with a parent exception type, either from the `framework` component or Crystal stdlib, double check it to ensure it'll still rescue what you are expecting it will.\n\n### `ATHR::Interface.configuration` scoping\n\nPreviously if you had a value resolver using the `configuration` macro:\n\n```cr\nstruct Multiply\n  include ATHR::Interface\n\n  configuration This\n\n  # ...\nend\n```\n\nThe `This` configuration would be scoped to the `Multiply` namespace, i.e. `@[Multiply::This]`.\nScoping is now handled separately, meaning the same resolver could define multiple configurations to an entirely different namespace.\nIf you wish to retain the same behavior, provide the FQN to the `configuration` macro: `configuration Multiply::This`.\nIf you wish to move the configuration to another namespace, prefix the FQN with `::`: `configuration ::MyApp::Annotations::Multiply`.\n\n## Upgrade to 0.19.0\n\n### Change how framework features are configured\n\nThis change is a pretty fundamental change and cannot really be easily captured in this upgrading guide. Instead, take a moment to review the updated [Configuration](https://athenaframework.org/getting_started/configuration/) section in the getting started guide.\n\nAt a high level, the `.configure` calls have been replaced with `ATH.configure` that handles both configuration and parameters.\n"
  },
  {
    "path": "src/components/framework/docs/.gitkeep",
    "content": ""
  },
  {
    "path": "src/components/framework/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Framework\nsite_url: https://athenaframework.org/Framework/\nrepo_url: https://github.com/athena-framework/framework\n\nnav:\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: index.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-clock/src/athena-clock.cr\n            - ./lib/athena-console/src/athena-console.cr\n            - ./lib/athena-contracts/src/athena-contracts.cr\n            - ./lib/athena-dependency_injection/src/athena-dependency_injection.cr\n            - ./lib/athena-event_dispatcher/src/athena-event_dispatcher.cr\n            - ./lib/athena-http/src/athena-http.cr\n            - ./lib/athena-http_kernel/src/athena-http_kernel.cr\n            - ./lib/athena-image_size/src/athena-image_size.cr\n            - ./lib/athena-mime/src/athena-mime.cr\n            - ./lib/athena-negotiation/src/athena-negotiation.cr\n            - ./lib/athena-routing/src/athena-routing.cr\n            - ./lib/athena-serializer/src/athena-serializer.cr\n            - ./lib/athena-validator/src/athena-validator.cr\n            - ./lib/athena/src/athena.cr\n            - ./lib/athena/src/spec.cr\n          source_locations:\n            lib/athena: https://github.com/athena-framework/framework/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/framework/shard.yml",
    "content": "name: athena\n\nversion: 0.22.0\n\ncrystal: ~> 1.19\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/athena\n\ndocumentation: https://athenaframework.org/Framework\n\ndescription: |\n  A web framework comprised of reusable, independent components.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\ndependencies:\n  athena-clock:\n    github: athena-framework/clock\n    version: ~> 0.3.0\n  athena-console:\n    github: athena-framework/console\n    version: ~> 0.4.0\n  athena-contracts:\n    github: athena-framework/contracts\n    version: ~> 0.1.0\n  athena-dependency_injection:\n    github: athena-framework/dependency-injection\n    version: ~> 0.4.0\n  athena-event_dispatcher:\n    github: athena-framework/event-dispatcher\n    version: ~> 0.4.0\n  athena-http:\n    github: athena-framework/http\n    version: ~> 0.1.0\n  athena-http_kernel:\n    github: athena-framework/http-kernel\n    version: ~> 0.1.0\n  athena-mime:\n    github: athena-framework/mime\n    version: ~> 0.2.0\n  athena-negotiation:\n    github: athena-framework/negotiation\n    version: ~> 0.2.0\n  athena-routing:\n    github: athena-framework/routing\n    version: ~> 0.2.0\n  athena-serializer:\n    github: athena-framework/serializer\n    version: ~> 0.4.0\n  athena-validator:\n    github: athena-framework/validator\n    version: ~> 0.5.0\n"
  },
  {
    "path": "src/components/framework/spec/argument_resolver_controller_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct ArgumentResolverControllerTest < ATH::Spec::APITestCase\n  def test_happy_path1 : Nil\n    self.post(\"/argument-resolvers/float\").body.should eq \"3.14\"\n  end\n\n  def test_happy_path2 : Nil\n    self.post(\"/argument-resolvers/string\").body.should eq %(\"fooo\")\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/assets/file-big.txt",
    "content": "I'm not big, but I'm big enough to carry more than 50 bytes inside me."
  },
  {
    "path": "src/components/framework/spec/assets/file-small.txt",
    "content": "I'm a file with less than 50 bytes."
  },
  {
    "path": "src/components/framework/spec/assets/foo.txt",
    "content": "foo\n"
  },
  {
    "path": "src/components/framework/spec/assets/greeting.ecr",
    "content": "Greetings, <%= name %>!\n"
  },
  {
    "path": "src/components/framework/spec/assets/layout.ecr",
    "content": "<h1>Content:</h1> <%= content -%>\n"
  },
  {
    "path": "src/components/framework/spec/assets/openssl/openssl.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKtJGQyJHN83MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\nBAYTAkFSMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\naWRnaXRzIFB0eSBMdGQwHhcNMTYwNTI5MTUwMzI1WhcNNDMxMDE0MTUwMzI1WjBF\nMQswCQYDVQQGEwJBUjETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\nZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAqz+M6CnLr8wJ5ooDiNU7D2hxfZqxculFP1y2wTDuxJfP8LqPO4o1NLpU\nE9H2idIn/iMLZrRpeK38lw8RmorEh0ykOQ2jXbw9Lw+xgQXjmsf0ZcXqSB82VD6q\n7JsGOF+Qq3I/YGegINfiOYMw60r8YEMTBJlz7tyeuJrCx2VUwBOa2Rtx7n0fzSom\n5jYAHEMQA6bAmShNOtCRn45NeVStQS1XTZ6XavmLiCUrgvEfWj+FlrpQQiTqoxGd\ndOTz1G0/0+FdJ7By/G/GbDBc2xuix7Fai7qhuLB5KAVd73Vy6T09U5TfDcUi+CNx\ncvJu0YPn9vVkRIuAoH3lMpprtzLaGwIDAQABo1AwTjAdBgNVHQ4EFgQU7DjYFtaT\nvnHQCfVWLOilVdco8mwwHwYDVR0jBBgwFoAU7DjYFtaTvnHQCfVWLOilVdco8mww\nDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAgApc4DjKd3x3lDeENS/s\nCZck6IkK+iErXeevIQFvi2poorTdoCdmnl4Hn+VgElTx8OL48WulDtqppEVY5I5t\nqT4AU7UXMBvmySw9cOB4nSMSYJVmtAYnVa61WICpQ8tIOunanRxwB32I/BUUc6rr\n0i+iAiW8x4aPsG5SGafIwtfNhY1pJa4nyo/VAJKxEKIl5jgeITBGHCO8ZHHqTcBu\nbj/DuWC3vGN5pVR3mb+O7Q1X+nhOZaSJYkB0nfLBKpWdMx0jrp1ZvbwDH5RrzzdD\nZRtrL2CVb3uWpbiCS8UaFcd1PaD92yT0IkxEhdIWH6WyDqs1/DZzbKDzxAlDiaCS\nZA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "src/components/framework/spec/assets/openssl/openssl.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCrP4zoKcuvzAnm\nigOI1TsPaHF9mrFy6UU/XLbBMO7El8/wuo87ijU0ulQT0faJ0if+IwtmtGl4rfyX\nDxGaisSHTKQ5DaNdvD0vD7GBBeOax/RlxepIHzZUPqrsmwY4X5Crcj9gZ6Ag1+I5\ngzDrSvxgQxMEmXPu3J64msLHZVTAE5rZG3HufR/NKibmNgAcQxADpsCZKE060JGf\njk15VK1BLVdNnpdq+YuIJSuC8R9aP4WWulBCJOqjEZ105PPUbT/T4V0nsHL8b8Zs\nMFzbG6LHsVqLuqG4sHkoBV3vdXLpPT1TlN8NxSL4I3Fy8m7Rg+f29WREi4CgfeUy\nmmu3MtobAgMBAAECggEAHmOir7hrCwFcaGrpgajFWFCigzWmc8vtm/bp/5KdbIm8\nPu38aQZ3tqmyLeo+o+qFalXxugIeDWpivrPP3eruQUxagD1pVkMHYIiaaVkQMPF2\n73CVyMKxM3YDgwVnry1WUPZvRL5e7jUhUi9zyO1/p91/THum1SaVjBD6q8PRrFwD\n2hjbi1ZYuzTJE9/7EWnrIeJUUx/TbhTM1aseufCpCbTQA5MA7ZVJKiW9+ZWPUw4x\nhfaDJx/kzFWE3DHU0L3eU83AFPZR2rmLOQONB2BhsSr5V7BGLtcY/4HTcRlzTt2g\nZSPZiD7IhpFcOyWiqATmGtmuFOl9dOORHtgF6VxJsQKBgQDdlcRbEbBv+78GQosK\nVh8ByhiDE1GQ4G4MoxxdtainBfbg4Uy+A2GDO9SgrD2VX3CSn+ZtJ8EBOO3wh9io\n/+X4Eoitkfpo0ZyAQQbSwXjVCZNANw8T27lYRAjIGVlNf/A3c7k3XdkdvSqkifhx\ne8sUFKZVR+82g9s3nAJgGxI5VwKBgQDF2GPdOw64rT3GxYsFl4qp5fc7r5IAgR73\nZPWPVApYrimzpu/5AEPA/dgAcRqflP3HKCJE+gJxtUgOnyd6GXSApD6rkBx3PGd1\n1ZtAsQw3wzWwQxzrQfjv4OMHy1ky1OtORvT/g1zWUKJdE+cm0CmonSbYE2Fgjkh4\n+G8spDk23QKBgDMBvLdx9PlyK+DXBIaWmICi8s2Jbuc4olyKV4dCv9Xiy5eshSvg\nP1wkM6fgvjRaSeGWqUZLNmR/pFYQD1Gnxlo6effqeIgUaEAlt9pf6t6vW5QWmIPr\nuliVIKhfHW13m+ZH30Tdd5Me7mf90pDc/DxdHITZEDmuVJISeYGB+cn1AoGAHdHt\ny2yZXXCPPSSNPbyHo/ALga2G3hiYKEXJVV8faBpoIrHovakyjSY1pmtlzePRFHGS\nKL9eGvFt+PY4JwkrLDCVWZqRD8/E8FfP3MJSyxzbPMQA2dzJvq4wyf32Zdj91oCP\ncOvF1G+26TyUvJ7niIiXUD4rkTgg6ErZxurBzOkCgYAOLlXQC+GkDjOzksSwJ60e\nKtcI4/7Z0CA43Tp6rLxcheNAaTcmMtmoqGyp7kChOlcJ5IOl1uXCaWP9Xho7gJqu\nbIK9i6hum8b1uTBI2U7FN3pUD+mKgaqXXfHjvVtoB9OS9Rug9loRkyXFtD6EZhdJ\nuGKDTMXbzMWWKPpoYd55Vw==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "src/components/framework/spec/athena_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate class MockHandler\n  include ::HTTP::Handler\n\n  def call(context)\n  end\nend\n\ndescribe Athena::Framework do\n  describe ATH::Server do\n    describe \".new\" do\n      it \"creates a server with the provided args\" do\n        ATH::Server.new 1234, \"google.com\", false\n      end\n\n      it \"creates a server with a prepended ::HTTP::Handler\" do\n        ATH::Server.new prepend_handlers: [MockHandler.new]\n      end\n\n      it \"creates a server with SSL context\" do\n        context = OpenSSL::SSL::Context::Server.new\n        context.certificate_chain = \"#{__DIR__}/assets/openssl/openssl.crt\"\n        context.private_key = \"#{__DIR__}/assets/openssl/openssl.key\"\n\n        ATH::Server.new ssl_context: context\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/bundle_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"./spec_helper.cr\")\nend\n\nprivate def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compiles code, line: line, preamble: %(require \"./spec_helper.cr\")\nend\n\ndescribe ATH::Bundle, tags: \"compiled\" do\n  describe ATH::Listeners::CORS do\n    it \"wildcard allow_headers with allow_credentials\" do\n      assert_compile_time_error \"'expose_headers' cannot contain a wildcard ('*') when 'allow_credentials' is 'true'.\", <<-'CR'\n          ATH.configure({\n            framework: {\n              cors: {\n                enabled:  true,\n                defaults: {\n                  allow_credentials: true,\n                  expose_headers:    [\"*\"],\n                },\n              },\n            },\n          })\n        CR\n    end\n\n    it \"does not exist if not enabled\" do\n      assert_compile_time_error \"undefined method 'athena_framework_listeners_cors'\", <<-CR\n          ADI.container.athena_framework_listeners_cors\n        CR\n    end\n\n    it \"correctly wires up the listener based on its configuration\" do\n      assert_compiles <<-'CR'\n          ATH.configure({\n            framework: {\n              cors: {\n                enabled:  true,\n                defaults: {\n                  allow_credentials: true,\n                  allow_origin: [\"allow_origin\", /foo/],\n                  allow_headers: [\"allow_headers\", \"X-My-Header\"],\n                  allow_methods: [\"allow_methods\"],\n                  expose_headers: [\"expose_headers\", \"X-My-Header\"],\n                  max_age: 123\n                },\n              },\n            },\n          })\n\n          macro finished\n            macro finished\n              \\{%\n                 service = ADI::ServiceContainer::SERVICE_HASH[\"athena_framework_listeners_cors\"]\n                 arg = service[\"parameters\"][\"config\"][\"value\"]\n              %}\n              ASPEC.compile_time_assert(\\{{ arg =~ /allow_credentials: true/ }}, \"Expected allow_credentials: true\")\n              ASPEC.compile_time_assert(\\{{ arg =~ /allow_origin: \\[\"allow_origin\", \\/foo\\/\\]/ }}, \"Expected allow_origin\")\n              ASPEC.compile_time_assert(\\{{ arg =~ /allow_headers: \\[\"allow_headers\", \"x-my-header\"]/ }}, \"Expected allow_headers\")\n              ASPEC.compile_time_assert(\\{{ arg =~ /allow_methods: \\[\"allow_methods\"]/ }}, \"Expected allow_methods\")\n              ASPEC.compile_time_assert(\\{{ arg =~ /expose_headers: \\[\"expose_headers\", \"x-my-header\"]/ }}, \"Expected expose_headers\")\n              ASPEC.compile_time_assert(\\{{ arg =~ /max_age: 123/ }}, \"Expected max_age: 123\")\n            end\n          end\n        CR\n    end\n  end\n\n  describe ATH::Listeners::Format do\n    it \"correctly wires up the listener based on its configuration\" do\n      assert_compiles <<-'CR'\n        ATH.configure({\n          framework: {\n            format_listener: {\n              enabled: true,\n              rules:   [\n                {priorities: [\"json\", \"xml\"], host: /api\\.example\\.com/, fallback_format: \"json\"},\n                {path: /^\\/image/, priorities: [\"jpeg\", \"gif\"], fallback_format: false, stop: true},\n                {methods: [\"HEAD\"], priorities: [\"xml\", \"html\"], prefer_extension: false},\n                {path: /^\\/image/, priorities: [\"foo\"]},\n              ],\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n               service = ADI::ServiceContainer::SERVICE_HASH[\"athena_framework_listeners_format\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ service[\"parameters\"][\"format_negotiator\"][\"value\"].stringify == \"athena_framework_view_format_negotiator\" }}, \"Expected format_negotiator to be athena_framework_view_format_negotiator\")\n\n            \\{%\n               service = ADI::ServiceContainer::SERVICE_HASH[\"athena_framework_view_format_negotiator\"]\n               map = service[\"calls\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ map.size == 4 }}, \"Expected 4 format negotiator rules\")\n\n            # Hostname rule\n            \\{%\n               m0, rule = map[0][1]\n               matcher = ADI::ServiceContainer::SERVICE_HASH[m0.stringify][\"parameters\"][\"matchers\"][\"value\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ matcher.includes? %(AHTTP::RequestMatcher::Hostname.new(/api\\\\.example\\\\.com/)) }}, \"Expected hostname matcher for api.example.com\")\n            ASPEC.compile_time_assert(\\{{ rule.includes? \"ATH::View::FormatNegotiator::Rule.new\" }}, \"Expected hostname rule to be a FormatNegotiator::Rule\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /fallback_format: \"json\"/ }}, \"Expected hostname rule fallback_format: json\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /prefer_extension: true/ }}, \"Expected hostname rule prefer_extension: true\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /priorities: \\[\"json\", \"xml\"\\]/ }}, \"Expected hostname rule priorities: json, xml\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /stop: false/ }}, \"Expected hostname rule stop: false\")\n\n            # Path rule\n            \\{%\n               m1, rule = map[1][1]\n               matcher = ADI::ServiceContainer::SERVICE_HASH[m1.stringify][\"parameters\"][\"matchers\"][\"value\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ matcher.includes? %(AHTTP::RequestMatcher::Path.new(/^\\\\/image/)) }}, \"Expected path matcher for /image\")\n            ASPEC.compile_time_assert(\\{{ rule.includes? \"ATH::View::FormatNegotiator::Rule.new\" }}, \"Expected path rule to be a FormatNegotiator::Rule\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /fallback_format: false/ }}, \"Expected path rule fallback_format: false\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /prefer_extension: true/ }}, \"Expected path rule prefer_extension: true\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /priorities: \\[\"jpeg\", \"gif\"\\]/ }}, \"Expected path rule priorities: jpeg, gif\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /stop: true/ }}, \"Expected path rule stop: true\")\n\n            # Methods rule\n            \\{%\n               m2, rule = map[2][1]\n               matcher = ADI::ServiceContainer::SERVICE_HASH[m2.stringify][\"parameters\"][\"matchers\"][\"value\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ matcher.includes? %(AHTTP::RequestMatcher::Method.new([\"HEAD\"])) }}, \"Expected method matcher for HEAD\")\n            ASPEC.compile_time_assert(\\{{ rule.includes? \"ATH::View::FormatNegotiator::Rule.new\" }}, \"Expected methods rule to be a FormatNegotiator::Rule\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /fallback_format: \"json\"/ }}, \"Expected methods rule fallback_format: json\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /prefer_extension: false/ }}, \"Expected methods rule prefer_extension: false\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /priorities: \\[\"xml\", \"html\"\\]/ }}, \"Expected methods rule priorities: xml, html\")\n            ASPEC.compile_time_assert(\\{{ rule =~ /stop: false/ }}, \"Expected methods rule stop: false\")\n\n            # Tests matcher reuse logic\n            \\{%\n               m3, rule = map[3][1]\n            %}\n            ASPEC.compile_time_assert(\\{{ m3 == m1 }}, \"Expected matcher reuse for path rules\")\n          end\n        end\n      CR\n    end\n  end\n\n  describe ATH::Listeners::File do\n    it \"correctly wires up the services based on its configuration\" do\n      assert_compiles <<-'CR'\n        ATH.configure({\n          framework: {\n            file_uploads: {\n              enabled: true,\n              temp_dir: \"/tmp/dir\",\n              max_uploads: 12,\n              max_file_size: 1_000_i64,\n            },\n          },\n        })\n\n        macro finished\n          macro finished\n            \\{%\n               service = ADI::ServiceContainer::SERVICE_HASH[\"athena_framework_listeners_file\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ !service.nil? }}, \"Expected athena_framework_listeners_file service to exist\")\n\n            \\{%\n               service = ADI::ServiceContainer::SERVICE_HASH[\"athena_framework_file_parser\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ !service.nil? }}, \"Expected athena_framework_file_parser service to exist\")\n\n            \\{%\n               parameters = service[\"parameters\"]\n            %}\n            ASPEC.compile_time_assert(\\{{ parameters[\"temp_dir\"][\"value\"] == \"/tmp/dir\" }}, \"Expected temp_dir to be /tmp/dir\")\n            ASPEC.compile_time_assert(\\{{ parameters[\"max_uploads\"][\"value\"] == 12 }}, \"Expected max_uploads to be 12\")\n            ASPEC.compile_time_assert(\\{{ parameters[\"max_file_size\"][\"value\"] == 1000_i64 }}, \"Expected max_file_size to be 1000\")\n          end\n        end\n      CR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/commands/debug_event_dispatcher_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class MyEvent < AED::Event; end\n\nprivate class MyOtherEvent < AED::Event; end\n\nstruct DebugEventDispatcherCommandTest < ASPEC::TestCase\n  def test_specific_event : Nil\n    tester = self.command_tester\n    ret = tester.execute event: \"MyEvent\", decorated: false\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should contain \"Registered Listeners for the MyEvent Event\"\n    tester.display.should contain \"#1      unknown callable           0\"\n    tester.display.should contain \"#2      some_service#some_method   -1\"\n\n    tester.display.should_not contain \"GenericEvent\"\n  end\n\n  def test_specific_event_no_match : Nil\n    tester = self.command_tester\n    ret = tester.execute event: \"blah\", decorated: false, capture_stderr_separately: true\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should be_empty\n    tester.error_output(true).should contain \"[WARNING] The event 'blah' does not have any registered listeners.\"\n  end\n\n  def test_specific_event_partial_match_single : Nil\n    tester = self.command_tester\n    ret = tester.execute event: \"other\", decorated: false\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should contain \"Registered Listeners for the MyOtherEvent Event\"\n    tester.display.should contain \"#1      unknown callable   0\"\n\n    tester.display.should_not contain \"MyEvent\"\n    tester.display.should_not contain \"GenericEvent\"\n  end\n\n  def test_specific_event_partial_match_multiple : Nil\n    tester = self.command_tester\n    ret = tester.execute event: \"my\", decorated: false\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should contain \"Registered Listeners Grouped by Event\"\n\n    tester.display.should contain \"MyEvent event\"\n    tester.display.should contain \"#1      unknown callable           0\"\n    tester.display.should contain \"#2      some_service#some_method   -1\"\n\n    tester.display.should contain \"MyOtherEvent event\"\n    tester.display.should contain \"#1      unknown callable   0\"\n\n    tester.display.should_not contain \"GenericEvent\"\n  end\n\n  def test_all_events : Nil\n    tester = self.command_tester\n    ret = tester.execute decorated: false\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should contain \"Registered Listeners Grouped by Event\"\n\n    tester.display.should contain \"MyEvent event\"\n    tester.display.should contain \"#1      unknown callable           0\"\n    tester.display.should contain \"#2      some_service#some_method   -1\"\n\n    tester.display.should contain \"MyOtherEvent event\"\n    tester.display.should contain \"#1      unknown callable   0\"\n\n    tester.display.should contain \"Athena::EventDispatcher::GenericEvent(String, String) event\"\n    tester.display.should contain \"#1      generic-event   0\"\n  end\n\n  @[DataProvider(\"complete_provider\")]\n  def test_complete(input : Array(String), expected_suggestions : Array(String)) : Nil\n    tester = ACON::Spec::CommandCompletionTester.new self.command\n    suggestions = tester.complete input\n\n    suggestions.should eq expected_suggestions\n  end\n\n  def complete_provider : Hash\n    {\n      \"nothing\" => {[] of String, [\"Athena::EventDispatcher::GenericEvent(String, String)\", \"MyEvent\", \"MyOtherEvent\"]},\n      \"format\"  => {[\"--format\"], [\"txt\"]},\n    }\n  end\n\n  private def command : ATH::Commands::DebugEventDispatcher\n    ATH::Commands::DebugEventDispatcher.new self.dispatcher\n  end\n\n  private def command_tester : ACON::Spec::CommandTester\n    ACON::Spec::CommandTester.new self.command\n  end\n\n  private def dispatcher : AED::EventDispatcherInterface\n    dispatcher = AED::EventDispatcher.new\n    dispatcher.listener(AED::GenericEvent(String, String), name: \"generic-event\") { }\n    dispatcher.listener(MyEvent) { }\n    dispatcher.listener(MyEvent, priority: -1, name: \"some_service#some_method\") { }\n    dispatcher.listener(MyOtherEvent) { }\n\n    dispatcher\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/commands/debug_router_match_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct DebugRouterMatchCommandTest < ASPEC::TestCase\n  def test_matches : Nil\n    tester = self.command_tester\n    ret = tester.execute path_info: \"/foo\", decorated: false\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should contain \"Route Name   | foo\"\n  end\n\n  def test_no_match : Nil\n    tester = self.command_tester\n    ret = tester.execute path_info: \"/test\", decorated: false\n\n    ret.should eq ACON::Command::Status::FAILURE\n    tester.display(true).should contain \"None of the routes match the path '/test'\"\n  end\n\n  def test_partial : Nil\n    tester = self.command_tester\n    ret = tester.execute path_info: \"/bar/11\", decorated: false\n\n    ret.should eq ACON::Command::Status::FAILURE\n    tester.display.should contain \"Route 'bar' almost matches but requirement for 'id' does not match (10)\"\n    tester.display.should contain \"None of the routes match the path '/bar/11'\"\n  end\n\n  private def command_tester : ACON::Spec::CommandTester\n    application = ACON::Application.new \"Athena Specs\"\n    application.add ATH::Commands::DebugRouterMatch.new self.router\n    application.add ATH::Commands::DebugRouter.new self.router\n\n    ACON::Spec::CommandTester.new application.find \"debug:router:match\"\n  end\n\n  private def router : ART::RouterInterface\n    route_collection = ART::RouteCollection.new\n    route_collection.add \"foo\", ART::Route.new \"foo\"\n    route_collection.add \"bar\", ART::Route.new \"/bar/{id<10>}\"\n\n    context = ART::RequestContext.new\n\n    ART::Router.new route_collection, context: context\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/commands/debug_router_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct DebugRouterCommandTest < ASPEC::TestCase\n  @router : ART::Router\n\n  def initialize\n    routes = ART::RouteCollection.new\n    routes.add \"routerdebug_session_welcome\", ART::Route.new \"/session\"\n    routes.add \"routerdebug_session_welcome_name\", ART::Route.new \"/session/{name}\"\n    routes.add \"routerdebug_session_logout\", ART::Route.new \"/session_logout\"\n    routes.add \"routerdebug_test\", ART::Route.new \"/test\"\n\n    @router = ART::Router.new routes\n  end\n\n  def test_all_routes : Nil\n    tester = self.command_tester\n    ret = tester.execute\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should contain \"routerdebug_session_welcome\"\n    tester.display.should contain \"/session\"\n\n    tester.display.should contain \"routerdebug_session_welcome_name\"\n    tester.display.should contain \"/session/{name}\"\n\n    tester.display.should contain \"routerdebug_session_logout\"\n    tester.display.should contain \"/session_logout\"\n\n    tester.display.should contain \"routerdebug_test\"\n    tester.display.should contain \"/test\"\n  end\n\n  def test_single_route : Nil\n    tester = self.command_tester\n    ret = tester.execute name: \"routerdebug_session_welcome_name\"\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should contain \"routerdebug_session_welcome_name\"\n    tester.display.should contain \"/session/{name}\"\n  end\n\n  def test_multiple_matching_routes : Nil\n    tester = self.command_tester\n    tester.inputs \"3\"\n    ret = tester.execute name: \"routerdebug\", interactive: true\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should contain \"Select one of the matching routes:\"\n    tester.display.should contain \"routerdebug_test\"\n    tester.display.should contain \"/test\"\n  end\n\n  def test_multiple_matching_routes_no_interaction : Nil\n    tester = self.command_tester\n    ret = tester.execute name: \"routerdebug\", interactive: false\n\n    ret.should eq ACON::Command::Status::SUCCESS\n    tester.display.should_not contain \"Select one of the matching routes:\"\n\n    tester.display.should contain \"routerdebug_session_welcome\"\n    tester.display.should contain \"/session\"\n\n    tester.display.should contain \"routerdebug_session_welcome_name\"\n    tester.display.should contain \"/session/{name}\"\n\n    tester.display.should contain \"routerdebug_session_logout\"\n    tester.display.should contain \"/session_logout\"\n\n    tester.display.should contain \"routerdebug_test\"\n    tester.display.should contain \"/test\"\n  end\n\n  def test_missing_route : Nil\n    tester = self.command_tester\n\n    expect_raises ACON::Exception::InvalidArgument, \"The route 'blah' does not exist.\" do\n      tester.execute name: \"blah\", interactive: true\n    end\n  end\n\n  @[DataProvider(\"complete_provider\")]\n  def test_complete(input : Array(String), expected_suggestions : Array(String)) : Nil\n    tester = ACON::Spec::CommandCompletionTester.new self.command\n    suggestions = tester.complete input\n\n    suggestions.should eq expected_suggestions\n  end\n\n  def complete_provider : Hash\n    {\n      \"nothing\" => {[] of String, [\"routerdebug_session_welcome\", \"routerdebug_session_welcome_name\", \"routerdebug_session_logout\", \"routerdebug_test\"]},\n      \"format\"  => {[\"--format\"], [\"txt\"]},\n    }\n  end\n\n  private def command : ATH::Commands::DebugRouter\n    ATH::Commands::DebugRouter.new(@router)\n  end\n\n  private def command_tester\n    ACON::Spec::CommandTester.new self.command\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/compiler_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"./spec_helper.cr\"), postamble: \"ATH.run\"\nend\n\nprivate def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compiles code, line: line, preamble: %(require \"./spec_helper.cr\"), postamble: \"ATH.run\"\nend\n\ndescribe Athena::Framework do\n  describe \"compiler errors\", tags: \"compiled\" do\n    it \"action parameter missing type restriction\" do\n      assert_compile_time_error \"Route action parameter 'CompileController#action:id' must have a type restriction.\", <<-CR\n        class CompileController < ATH::Controller\n          @[ARTA::Get(path: \"/:id\")]\n          def action(id) : Int32\n            123\n          end\n        end\n      CR\n    end\n\n    it \"action missing return type\" do\n      assert_compile_time_error \"Route action return type must be set for 'CompileController#action'.\", <<-CR\n        class CompileController < ATH::Controller\n          @[ARTA::Get(path: \"/\")]\n          def action\n            123\n          end\n        end\n      CR\n    end\n\n    it \"class method action\" do\n      assert_compile_time_error \"Routes can only be defined as instance methods. Did you mean 'CompileController#class_method'?\", <<-CR\n        class CompileController < ATH::Controller\n          @[ARTA::Get(path: \"/\")]\n          def self.class_method : Int32\n            123\n          end\n        end\n      CR\n    end\n\n    it \"when action does not have a path\" do\n      assert_compile_time_error \"Route action 'CompileController#action' is missing its path.\", <<-CR\n        class CompileController < ATH::Controller\n          @[ARTA::Get]\n          def action : Int32\n            123\n          end\n        end\n      CR\n    end\n\n    describe \"when the controller type conflicts with internal ATH types\" do\n      it \"complies when the controller is a service\" do\n        assert_compiles <<-CR\n          @[ADI::Register]\n          class Controller::ExampleController < ATH::Controller\n            @[ARTA::Get(\"/name\")]\n            def name : Nil\n            end\n          end\n        CR\n      end\n\n      it \"compiles when the controller is not a service\" do\n        assert_compiles <<-CR\n          class Controller::ExampleController < ATH::Controller\n            @[ARTA::Get(\"/name\")]\n            def name : Nil\n            end\n          end\n        CR\n      end\n    end\n\n    describe \"when a controller action is mistakenly overridden\" do\n      it \"within the same controller\" do\n        assert_compile_time_error \"A controller action named '#action' already exists within 'CompileController'.\", <<-CR\n          class CompileController < ATH::Controller\n            @[ARTA::Get(path: \"/foo\")]\n            def action : String\n              \"foo\"\n            end\n\n            @[ARTA::Get(path: \"/bar\")]\n            def action : String\n              \"bar\"\n            end\n          end\n        CR\n      end\n\n      it \"within a different controller\" do\n        assert_compiles <<-CR\n          class ExampleController < ATH::Controller\n            @[ARTA::Get(path: \"/foo\")]\n            def action : String\n              \"foo\"\n            end\n          end\n\n          class CompileController < ATH::Controller\n            @[ARTA::Get(path: \"/bar\")]\n            def action : String\n              \"bar\"\n            end\n          end\n        CR\n      end\n    end\n\n    describe ARTA::Route do\n      it \"when there is a prefix for a controller action with a locale that does not have a route\" do\n        assert_compile_time_error \"Route action 'CompileController#action' is missing paths for locale(s) 'de'.\", <<-CR\n          @[ARTA::Route(path: {\"de\" => \"/german\", \"fr\" => \"/france\"})]\n          class CompileController < ATH::Controller\n            @[ARTA::Get(path: {\"fr\" => \"\"})]\n            def action : Nil\n            end\n          end\n        CR\n      end\n\n      it \"when a controller action has a locale that is missing a prefix\" do\n        assert_compile_time_error \"Route action 'CompileController#action' is missing a corresponding route prefix for the 'de' locale.\", <<-CR\n          @[ARTA::Route(path: {\"fr\" => \"/france\"})]\n          class CompileController < ATH::Controller\n            @[ARTA::Get(path: {\"de\" => \"/foo\", \"fr\" => \"/bar\"})]\n            def action : Nil\n            end\n          end\n        CR\n      end\n\n      it \"has an unexpected type as the #methods\" do\n        assert_compile_time_error \"Route action 'CompileController#action' expects a 'StringLiteral | ArrayLiteral | TupleLiteral' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.\", <<-CR\n          class CompileController < ATH::Controller\n            @[ARTA::Route(\"/\", methods: 123)]\n            def action : Nil\n            end\n          end\n        CR\n      end\n\n      it \"requires ARTA::Route to use 'methods'\" do\n        assert_compile_time_error \"Route action 'CompileController#action' cannot change the required methods when _NOT_ using the 'ARTA::Route' annotation.\", <<-CR\n          class CompileController < ATH::Controller\n            @[ARTA::Get(\"/\", methods: \"SEARCH\")]\n            def action : Nil; end\n          end\n        CR\n      end\n\n      describe \"invalid field types\" do\n        describe \"path\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Route#path' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(path: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Get#path' field, but got a 'NumberLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: 10)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"defaults\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Route#defaults' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(defaults: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Get#defaults' field, but got a 'NumberLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(defaults: 10)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"locale\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#locale' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(locale: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#locale' field, but got a 'NumberLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(locale: 10)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"format\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#format' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(format: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#format' field, but got a 'NumberLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(format: 10)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"stateless\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'BoolLiteral' for its 'ARTA::Route#stateless' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(stateless: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects a 'BoolLiteral' for its 'ARTA::Get#stateless' field, but got a 'NumberLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(stateless: 10)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"name\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'StringLiteral' for its 'ARTA::Route#name' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(name: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects a 'StringLiteral' for its 'ARTA::Get#name' field, but got a 'NumberLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\", name: 10)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"requirements\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Route#requirements' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(requirements: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Get#requirements' field, but got a 'NumberLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\", requirements: 10)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"schemes\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#schemes' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(schemes: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"methods\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#methods' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(methods: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"host\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Route#host' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(host: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Get#host' field, but got a 'NumberLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\", host: 10)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"condition\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects an 'ART::Route::Condition' for its 'ARTA::Route#condition' field, but got a 'NumberLiteral'.\", <<-CR\n              @[ARTA::Route(condition: 10)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects an 'ART::Route::Condition' for its 'ARTA::Get#condition' field, but got a 'NumberLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\", condition: 10)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n\n        describe \"priority\" do\n          it \"controller ann\" do\n            assert_compile_time_error \"Route action 'CompileController' expects a 'NumberLiteral' for its 'ARTA::Route#priority' field, but got a 'BoolLiteral'.\", <<-CR\n              @[ARTA::Route(priority: true)]\n              class CompileController < ATH::Controller\n                @[ARTA::Get(path: \"/\")]\n                def action : Nil; end\n              end\n            CR\n          end\n\n          it \"route ann\" do\n            assert_compile_time_error \"Route action 'CompileController#action' expects a 'NumberLiteral' for its 'ARTA::Get#priority' field, but got a 'BoolLiteral'.\", <<-CR\n              class CompileController < ATH::Controller\n                @[ARTA::Get(priority: false)]\n                def action : Nil; end\n              end\n            CR\n          end\n        end\n      end\n    end\n\n    describe ATHR::RequestBody do\n      it \"when the action parameter is not serializable\" do\n        assert_compile_time_error \" The annotation '@[ATHA::MapRequestBody]' cannot be applied to 'CompileController#action:foo : Foo' since the 'Athena::Framework::Controller::ValueResolvers::RequestBody' resolver only supports parameters of type 'Athena::Serializer::Serializable | JSON::Serializable | URI::Params::Serializable | (Athena::HTTP::UploadedFile | Nil) | Array(Athena::HTTP::UploadedFile)'.\", <<-CR\n          record Foo, text : String\n\n          class CompileController < ATH::Controller\n            @[ARTA::Get(path: \"/\")]\n            def action(@[ATHA::MapRequestBody] foo : Foo) : Foo\n              foo\n            end\n          end\n        CR\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controller/redirect_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct RedirectControllerTest < ASPEC::TestCase\n  def test_empty_route_permanent : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    controller = ATH::Controller::Redirect.new\n\n    ex = expect_raises AHK::Exception::HTTPException do\n      controller.redirect_url request, \"\", true\n    end\n\n    ex.status_code.should eq 410\n  end\n\n  def test_empty_route_non_permanent : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    controller = ATH::Controller::Redirect.new\n\n    ex = expect_raises AHK::Exception::HTTPException do\n      controller.redirect_url request, \"\"\n    end\n\n    ex.status_code.should eq 404\n  end\n\n  def test_full_url : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    controller = ATH::Controller::Redirect.new\n\n    response = controller.redirect_url request, \"http://foo.com/\"\n    self.assert_redirect_url response, \"http://foo.com/\"\n    response.status.found?.should be_true\n  end\n\n  def test_full_url_with_method_keep : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    controller = ATH::Controller::Redirect.new\n\n    response = controller.redirect_url request, \"http://foo.com/\", keep_request_method: true\n    self.assert_redirect_url response, \"http://foo.com/\"\n    response.status.temporary_redirect?.should be_true\n  end\n\n  def test_protocol_relative : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    controller = ATH::Controller::Redirect.new\n\n    response = controller.redirect_url request, \"//foo.bar/\"\n    self.assert_redirect_url response, \"http://foo.bar/\"\n    response.status.found?.should be_true\n  end\n\n  def test_url_redirect_default_ports : Nil\n    host = \"www.example.com\"\n    path = \"/redirect-path\"\n    http_port = 1080\n    https_port = 1443\n\n    expected_url = \"https://#{host}:#{https_port}#{path}\"\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\"host\" => \"#{host}:#{http_port}\"}\n    controller = ATH::Controller::Redirect.new https_port: https_port\n    response = controller.redirect_url request, path, scheme: \"https\"\n    self.assert_redirect_url response, expected_url\n\n    expected_url = \"http://#{host}:#{http_port}#{path}\"\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\"host\" => \"#{host}:#{http_port}\"}\n    controller = ATH::Controller::Redirect.new http_port\n    response = controller.redirect_url request, path, scheme: \"http\"\n    self.assert_redirect_url response, expected_url\n  end\n\n  @[DataProvider(\"url_redirect_provider\")]\n  def test_url_redirect(\n    scheme : String,\n    http_port : Int32?,\n    https_port : Int32?,\n    request_scheme : String,\n    request_port : Int32,\n    expected_port : String,\n  ) : Nil\n    host = \"www.example.com\"\n    path = \"/redirect-path\"\n    expected_url = \"#{scheme}://#{host}#{expected_port}#{path}\"\n\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\"host\" => \"#{host}:#{request_port}\"}\n    request.scheme = request_scheme\n    controller = ATH::Controller::Redirect.new\n\n    response = controller.redirect_url request, path, scheme: scheme, http_port: http_port, https_port: https_port\n    self.assert_redirect_url response, expected_url\n  end\n\n  def url_redirect_provider : Tuple\n    {\n      # Standard ports\n      {\"http\", nil, nil, \"http\", 80, \"\"},\n      {\"http\", 80, nil, \"http\", 80, \"\"},\n      {\"https\", nil, nil, \"http\", 80, \"\"},\n      {\"https\", 80, nil, \"http\", 80, \"\"},\n\n      {\"http\", nil, nil, \"https\", 443, \"\"},\n      {\"http\", nil, 443, \"https\", 443, \"\"},\n      {\"https\", nil, nil, \"https\", 443, \"\"},\n      {\"https\", nil, 443, \"https\", 443, \"\"},\n\n      # Non-standard ports\n      {\"http\", nil, nil, \"http\", 8080, \":8080\"},\n      {\"http\", 4080, nil, \"http\", 8080, \":4080\"},\n      {\"http\", 80, nil, \"http\", 8080, \"\"},\n      {\"https\", nil, nil, \"http\", 8080, \"\"},\n      {\"https\", nil, 8443, \"http\", 8080, \":8443\"},\n      {\"https\", nil, 443, \"http\", 8080, \"\"},\n\n      {\"https\", nil, nil, \"https\", 8443, \":8443\"},\n      {\"https\", nil, 4443, \"https\", 8443, \":4443\"},\n      {\"https\", nil, 443, \"https\", 8443, \"\"},\n      {\"http\", nil, nil, \"https\", 8443, \"\"},\n      {\"http\", 8080, 4443, \"https\", 8443, \":8080\"},\n      {\"http\", 80, 4443, \"https\", 8443, \"\"},\n    }\n  end\n\n  @[TestWith(\n    {\"http://www.example.com/redirect-path\", \"/redirect-path\", \"\"},\n    {\"http://www.example.com/redirect-path?foo=bar\", \"/redirect-path?foo=bar\", \"\"},\n    {\"http://www.example.com/redirect-path?f.o=bar\", \"/redirect-path\", \"f.o=bar\"},\n    {\"http://www.example.com/redirect-path?f.o=bar&a.c=example\", \"/redirect-path?f.o=bar\", \"a.c=example\"},\n    {\"http://www.example.com/redirect-path?f.o=bar&a.c=example&b.z=def\", \"/redirect-path?f.o=bar\", \"a.c=example&b.z=def\"},\n    {\"http://www.example.com/redirect-path?val=one&val=two\", \"/redirect-path?val=one\", \"val=two\"},\n  )]\n  def test_path_query_params(expected : String, path : String, query_string : String) : Nil\n    scheme = \"http\"\n    host = \"www.example.com\"\n    port = 80\n\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\"host\" => \"#{host}:#{port}\"}\n    request.query = query_string if query_string != \"\"\n\n    controller = ATH::Controller::Redirect.new\n\n    self.assert_redirect_url controller.redirect_url(request, path, scheme: scheme, http_port: port), expected\n  end\n\n  # TODO: For when we have a way to redirect to a route vs just a path\n\n  # def test_redirect_with_query : Nil\n  # end\n\n  # def test_redirect_with_query_with_route_params_overriding : Nil\n  # end\n\n  private def assert_redirect_url(response : AHTTP::Response, expected : String) : Nil\n    response.redirect?(expected).should be_true, failure_message: \"Expected: '#{expected}'\\n Got: '#{response.headers[\"location\"]}'.\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controller/value_resolvers/enum_spec.cr",
    "content": "require \"../../spec_helper\"\n\nenum TestEnum\n  A\n  B\n  C\nend\n\ndescribe ATHR::Enum do\n  describe \"#resolve\" do\n    it \"some other type\" do\n      ATHR::Enum.new.resolve(new_request, AHK::Controller::ParameterMetadata(Int32).new \"enum\").should be_nil\n      ATHR::Enum.new.resolve(new_request, AHK::Controller::ParameterMetadata(Int32?).new \"enum\").should be_nil\n      ATHR::Enum.new.resolve(new_request, AHK::Controller::ParameterMetadata(Bool | String).new \"enum\").should be_nil\n    end\n\n    it \"is not a string\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum).new \"enum\"\n      request = new_request\n      request.attributes.set \"enum\", 1\n\n      ATHR::Enum.new.resolve(request, parameter).should be_nil\n    end\n\n    it \"that does not exist in request attributes\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum).new \"enum\"\n\n      ATHR::Enum.new.resolve(new_request, parameter).should be_nil\n    end\n\n    it \"that is nilable and not exist in request attributes\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum?).new \"enum\"\n\n      ATHR::Enum.new.resolve(new_request, parameter).should be_nil\n    end\n\n    it \"that is a union of another type\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum | String).new \"enum\"\n      request = new_request\n      request.attributes.set \"enum\", \"1\"\n\n      ATHR::Enum.new.resolve(request, parameter).should eq TestEnum::B\n    end\n\n    it \"the enum member is nilable\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum?).new \"enum\"\n      request = new_request\n      request.attributes.set \"enum\", \"1\"\n\n      ATHR::Enum.new.resolve(request, parameter).should eq TestEnum::B\n    end\n\n    it \"with a numeric based value\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum).new \"enum\"\n\n      request = new_request\n      request.attributes.set \"enum\", \"2\"\n\n      ATHR::Enum.new.resolve(request, parameter).should eq TestEnum::C\n    end\n\n    it \"with a numeric based value with whitespace\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum).new \"enum\"\n\n      request = new_request\n      request.attributes.set \"enum\", \"2\"\n\n      ATHR::Enum.new.resolve(request, parameter).should eq TestEnum::C\n    end\n\n    it \"with a string based value\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum).new \"enum\"\n\n      request = new_request\n      request.attributes.set \"enum\", \"B\"\n\n      ATHR::Enum.new.resolve(request, parameter).should eq TestEnum::B\n    end\n\n    it \"with a string based nilable value\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum?).new \"enum\"\n\n      request = new_request\n      request.attributes.set \"enum\", \"B\"\n\n      ATHR::Enum.new.resolve(request, parameter).should eq TestEnum::B\n    end\n\n    it \"with an unknown member value\" do\n      parameter = AHK::Controller::ParameterMetadata(TestEnum).new \"enum\"\n\n      request = new_request\n      request.attributes.set \"enum\", \"  4  \"\n\n      expect_raises AHK::Exception::BadRequest, \"Parameter 'enum' of enum type 'TestEnum' has no valid member for '  4  '.\" do\n        ATHR::Enum.new.resolve request, parameter\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controller/value_resolvers/query_parameter_spec.cr",
    "content": "require \"../../spec_helper\"\n\nprivate def parameter(\n  klass : T.class = String,\n  *,\n  default : T? = nil,\n) forall T\n  AHK::Controller::ParameterMetadata(T).new(\n    \"foo\",\n    default_value: default,\n    has_default: !default.nil?,\n  )\nend\n\nprivate def resolver(\n  name : String? = nil,\n  validation_failed_status : ::HTTP::Status = :not_found,\n) forall T\n  ATHR::QueryParameter.new(\n    MockAnnotationResolver.new(\n      action_parameter_annotations: ADI::AnnotationConfigurations.new({\n        ATHA::MapQueryParameter => [\n          ATHA::MapQueryParameterConfiguration.new(name, validation_failed_status),\n        ] of ADI::AnnotationConfigurations::ConfigurationBase,\n      } of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase)),\n      expected_parameter_name: \"foo\"\n    )\n  )\nend\n\ndescribe ATHR::QueryParameter do\n  describe \"#resolve\" do\n    it \"does not have the annotation\" do\n      parameter = AHK::Controller::ParameterMetadata(String).new \"foo\"\n      ATHR::QueryParameter.new(MockAnnotationResolver.new).resolve(new_request, parameter).should be_nil\n    end\n\n    it \"valid scalar parameter\" do\n      resolver.resolve(new_request(query: \"foo=bar\"), parameter).should eq \"bar\"\n    end\n\n    it \"custom param name\" do\n      resolver(name: \"blah\").resolve(new_request(query: \"blah=bar\"), parameter).should eq \"bar\"\n    end\n\n    it \"valid array parameter\" do\n      resolver.resolve(new_request(query: \"foo=1&foo=2\"), parameter(Array(Int32))).should eq [1, 2]\n    end\n\n    it \"missing nilable\" do\n      resolver.resolve(new_request, parameter(Float64?)).should be_nil\n    end\n\n    it \"non-nilable with default\" do\n      resolver.resolve(new_request, parameter(Bool, default: false)).should be_nil\n    end\n\n    it \"missing non-nilable no default\" do\n      expect_raises AHK::Exception::NotFound, \"Missing query parameter: 'foo'.\" do\n        resolver.resolve new_request, parameter\n      end\n    end\n\n    it \"missing non-nilable no default custom status\" do\n      expect_raises AHK::Exception::UnprocessableEntity, \"Missing query parameter: 'foo'.\" do\n        resolver(validation_failed_status: :unprocessable_entity).resolve new_request, parameter\n      end\n    end\n\n    it \"invalid\" do\n      expect_raises AHK::Exception::NotFound, \"Invalid query parameter: 'foo'.\" do\n        resolver.resolve new_request(query: \"foo=bar\"), parameter(Int32)\n      end\n    end\n\n    it \"missing non-nilable no default custom status\" do\n      expect_raises AHK::Exception::UnprocessableEntity, \"Invalid query parameter: 'foo'.\" do\n        resolver(validation_failed_status: :unprocessable_entity).resolve new_request(query: \"foo=bar\"), parameter(Int32)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controller/value_resolvers/request_body_spec.cr",
    "content": "require \"../../spec_helper\"\n\nprivate record MockJSONSerializableEntity, id : Int32, name : String do\n  include JSON::Serializable\nend\n\nprivate record MockASRSerializableEntity, id : Int32, name : String do\n  include ASR::Serializable\nend\n\nprivate record MockValidatableASRSerializableEntity, id : Int32, name : String do\n  include ASR::Serializable\n  include AVD::Validatable\nend\n\nprivate record MockURISerializableEntity, id : Int32, name : String do\n  include URI::Params::Serializable\nend\n\nprivate record MockJSONAndURISerializableEntity, id : Int32, name : String do\n  include JSON::Serializable\n  include URI::Params::Serializable\nend\n\nstruct RequestBodyResolverTest < ASPEC::TestCase\n  @target : ATHR::RequestBody\n\n  @serializer : ASR::SerializerInterface\n  @validator : AVD::Validator::ValidatorInterface\n  @annotation_resolver : MockAnnotationResolver\n\n  def initialize\n    @validator = AVD::Spec::MockValidator.new\n    @serializer = DeserializableMockSerializer(Nil).new\n    @annotation_resolver = MockAnnotationResolver.new\n\n    @target = ATHR::RequestBody.new @serializer, @validator, @annotation_resolver\n  end\n\n  def test_no_annotation : Nil\n    ATHR::RequestBody.new(@serializer, @validator, @annotation_resolver).resolve(new_request, new_parameter).should be_nil\n  end\n\n  def test_raises_on_no_body : Nil\n    expect_raises AHK::Exception::BadRequest, \"Request does not have a body.\" do\n      @target.resolve new_request, self.get_config MockJSONSerializableEntity\n    end\n  end\n\n  def test_raises_on_empty_body : Nil\n    expect_raises AHK::Exception::BadRequest, \"Request does not have a body.\" do\n      @target.resolve new_request(body: \"\"), self.get_config(MockJSONSerializableEntity)\n    end\n  end\n\n  def test_raises_on_invalid_json : Nil\n    expect_raises AHK::Exception::BadRequest, \"Malformed JSON payload.\" do\n      @target.resolve new_request(body: %(<blah>)), self.get_config(MockJSONSerializableEntity)\n    end\n  end\n\n  def test_raises_on_invalid_nested_json : Nil\n    expect_raises AHK::Exception::BadRequest, \"Malformed JSON payload.\" do\n      @target.resolve new_request(body: %({\"id\": \"foo\"})), self.get_config(MockJSONSerializableEntity)\n    end\n  end\n\n  def test_raises_on_missing_json_data : Nil\n    expect_raises AHK::Exception::UnprocessableEntity, \"Missing JSON attribute: name\" do\n      @target.resolve new_request(body: %({\"id\":10})), self.get_config(MockJSONSerializableEntity)\n    end\n  end\n\n  def test_raises_on_missing_www_form_data : Nil\n    expect_raises AHK::Exception::UnprocessableEntity, \"Missing required property: 'name'.\" do\n      @target.resolve new_request(body: \"id=10\", format: \"form\"), self.get_config(MockURISerializableEntity)\n    end\n  end\n\n  def test_raises_on_missing_query_string_data : Nil\n    expect_raises AHK::Exception::UnprocessableEntity, \"Missing required property: 'name'.\" do\n      @target.resolve new_request(query: \"id=10\"), self.get_config(MockURISerializableEntity, ATHA::MapQueryString, ATHA::MapQueryStringConfiguration.new)\n    end\n  end\n\n  def test_it_raises_on_constraint_violations : Nil\n    serializer = DeserializableMockSerializer(MockValidatableASRSerializableEntity).new\n    serializer.deserialized_response = MockValidatableASRSerializableEntity.new 10, \"\"\n\n    validator = AVD::Spec::MockValidator.new(\n      AVD::Violation::ConstraintViolationList.new([\n        AVD::Violation::ConstraintViolation.new(\"error\", \"error\", Hash(String, String).new, \"\", \".name\", AVD::ValueContainer.new(\"\")),\n      ])\n    )\n\n    expect_raises AVD::Exception::ValidationFailed, \"Validation failed\" do\n      ATHR::RequestBody.new(serializer, validator, @annotation_resolver).resolve new_request(body: %({\"id\":10,\"name\":\"\"})), self.get_config(MockValidatableASRSerializableEntity)\n    end\n  end\n\n  def test_it_supports_json_serializable : Nil\n    request = new_request body: %({\"id\":10,\"name\":\"Fred\"})\n\n    object = @target.resolve request, self.get_config(MockJSONSerializableEntity)\n    object = object.should be_a MockJSONSerializableEntity\n\n    object.id.should eq 10\n    object.name.should eq \"Fred\"\n  end\n\n  def test_it_supports_asr_serializable : Nil\n    serializer = DeserializableMockSerializer(MockASRSerializableEntity).new\n    serializer.deserialized_response = MockASRSerializableEntity.new 10, \"Fred\"\n\n    request = new_request body: %({\"id\":10,\"name\":\"Fred\"})\n\n    object = ATHR::RequestBody.new(serializer, @validator, @annotation_resolver).resolve request, self.get_config(MockASRSerializableEntity)\n    object = object.should be_a MockASRSerializableEntity\n\n    object.id.should eq 10\n    object.name.should eq \"Fred\"\n  end\n\n  def test_it_supports_uri_params_serializable : Nil\n    serializer = DeserializableMockSerializer(MockURISerializableEntity).new\n    serializer.deserialized_response = MockURISerializableEntity.new 10, \"Fred\"\n\n    request = new_request body: \"id=10&name=Fred\", format: \"form\"\n\n    object = ATHR::RequestBody.new(serializer, @validator, @annotation_resolver).resolve request, self.get_config(MockURISerializableEntity)\n    object = object.should be_a MockURISerializableEntity\n\n    object.id.should eq 10\n    object.name.should eq \"Fred\"\n  end\n\n  def test_it_supports_specifying_accepted_formats : Nil\n    expect_raises AHK::Exception::UnsupportedMediaType, %(Unsupported format, expects one of: 'json, xml', but got 'form'.) do\n      @target.resolve(\n        new_request(body: \"id=10&name=Fred\", format: \"form\"),\n        self.get_config(MockURISerializableEntity, configuration: ATHA::MapRequestBodyConfiguration.new([\"json\", \"xml\"]))\n      )\n    end\n  end\n\n  def test_it_supports_query_string_serializable : Nil\n    serializer = DeserializableMockSerializer(MockURISerializableEntity).new\n    serializer.deserialized_response = MockURISerializableEntity.new 10, \"Fred\"\n\n    request = new_request query: \"id=10&name=Fred\"\n\n    object = ATHR::RequestBody.new(serializer, @validator, @annotation_resolver).resolve request, self.get_config(MockURISerializableEntity, ATHA::MapQueryString, ATHA::MapQueryStringConfiguration.new)\n    object = object.should be_a MockURISerializableEntity\n\n    object.id.should eq 10\n    object.name.should eq \"Fred\"\n  end\n\n  def test_it_supports_query_string_serializable_no_query_string : Nil\n    serializer = DeserializableMockSerializer(MockURISerializableEntity).new\n    serializer.deserialized_response = MockURISerializableEntity.new 10, \"Fred\"\n\n    ATHR::RequestBody\n      .new(serializer, @validator, @annotation_resolver)\n      .resolve(new_request, self.get_config(MockURISerializableEntity, ATHA::MapQueryString, ATHA::MapQueryStringConfiguration.new))\n      .should be_nil\n  end\n\n  def test_it_supports_multiple_serializable : Nil\n    serializer = DeserializableMockSerializer(MockJSONAndURISerializableEntity).new\n    serializer.deserialized_response = MockJSONAndURISerializableEntity.new 10, \"Fred\"\n\n    form_request = new_request body: \"id=10&name=Fred\", format: \"form\"\n    json_request = new_request body: %({\"id\":10,\"name\":\"Fred\"})\n\n    resolver = ATHR::RequestBody.new serializer, @validator, @annotation_resolver\n    form_object = resolver.resolve form_request, self.get_config(MockJSONAndURISerializableEntity)\n    form_object = form_object.should be_a MockJSONAndURISerializableEntity\n\n    json_object = resolver.resolve json_request, self.get_config(MockJSONAndURISerializableEntity)\n    json_object = json_object.should be_a MockJSONAndURISerializableEntity\n\n    form_object.id.should eq 10\n    form_object.name.should eq \"Fred\"\n\n    json_object.id.should eq 10\n    json_object.name.should eq \"Fred\"\n  end\n\n  def test_it_supports_avd_validatable : Nil\n    serializer = DeserializableMockSerializer(MockValidatableASRSerializableEntity).new\n    serializer.deserialized_response = MockValidatableASRSerializableEntity.new 10, \"Fred\"\n\n    request = new_request body: %({\"id\":10,\"name\":\"Fred\"})\n\n    object = ATHR::RequestBody.new(serializer, @validator, @annotation_resolver).resolve request, self.get_config(MockValidatableASRSerializableEntity)\n    object = object.should be_a MockValidatableASRSerializableEntity\n\n    object.id.should eq 10\n    object.name.should eq \"Fred\"\n  end\n\n  # File Uploads\n\n  @[DataProvider(\"uploaded_file_context\")]\n  def test_uploaded_file_single_defaults(request : AHTTP::Request) : Nil\n    object = @target.resolve request, self.get_config(AHTTP::UploadedFile, ATHA::MapUploadedFile, ATHA::MapUploadedFileConfiguration.new)\n\n    object = object.should be_a AHTTP::UploadedFile\n    object.basename.should eq \"file-small.txt\"\n    object.size.should eq 35\n  end\n\n  @[DataProvider(\"uploaded_file_context\")]\n  def test_uploaded_file_single_missing(request : AHTTP::Request) : Nil\n    object = @target.resolve request, self.get_config(AHTTP::UploadedFile, ATHA::MapUploadedFile, ATHA::MapUploadedFileConfiguration.new, property_name: \"empty\")\n    object.should be_nil\n  end\n\n  @[DataProvider(\"uploaded_file_context\")]\n  def test_uploaded_file_single_custom_name(request : AHTTP::Request) : Nil\n    object = @target.resolve request, self.get_config(AHTTP::UploadedFile, ATHA::MapUploadedFile, ATHA::MapUploadedFileConfiguration.new(name: \"bar\"))\n\n    object = object.should be_a AHTTP::UploadedFile\n    object.basename.should eq \"file-big.txt\"\n    object.size.should eq 70\n  end\n\n  @[DataProvider(\"uploaded_file_context\")]\n  def test_uploaded_file_single_constraints_no_violation(request : AHTTP::Request) : Nil\n    @target = ATHR::RequestBody.new @serializer, AVD.validator, @annotation_resolver\n\n    object = @target.resolve request, self.get_config(\n      AHTTP::UploadedFile,\n      ATHA::MapUploadedFile,\n      ATHA::MapUploadedFileConfiguration.new(\n        name: \"bar\",\n        constraints: AVD::Constraints::File.new(max_size: 100),\n      )\n    )\n\n    object = object.should be_a AHTTP::UploadedFile\n    object.basename.should eq \"file-big.txt\"\n    object.size.should eq 70\n  end\n\n  @[DataProvider(\"uploaded_file_context\")]\n  def test_uploaded_file_single_constraints_with_violation(request : AHTTP::Request) : Nil\n    @target = ATHR::RequestBody.new @serializer, AVD.validator, @annotation_resolver\n\n    ex = expect_raises AVD::Exception::ValidationFailed do\n      @target.resolve request, self.get_config(\n        AHTTP::UploadedFile,\n        ATHA::MapUploadedFile,\n        ATHA::MapUploadedFileConfiguration.new(\n          name: \"bar\",\n          constraints: AVD::Constraints::File.new(max_size: 50),\n        )\n      )\n    end\n\n    ex.violations.size.should eq 1\n    ex.violations[0].message.should eq \"The file is too large (70.0 bytes). Allowed maximum size is 50.0 bytes.\"\n  end\n\n  @[DataProvider(\"uploaded_file_context\")]\n  def test_uploaded_file_array_of_files_empty(request : AHTTP::Request) : Nil\n    object = @target.resolve request, self.get_config(Array(AHTTP::UploadedFile), ATHA::MapUploadedFile, ATHA::MapUploadedFileConfiguration.new, property_name: \"qux\")\n\n    object = object.should be_a Array(AHTTP::UploadedFile)\n    object.should be_empty\n  end\n\n  @[DataProvider(\"uploaded_file_context\")]\n  def test_uploaded_file_array_of_files_empty_nullable(request : AHTTP::Request) : Nil\n    object = @target.resolve request, self.get_config(Array(AHTTP::UploadedFile)?, ATHA::MapUploadedFile, ATHA::MapUploadedFileConfiguration.new, property_name: \"qux\")\n    object.should be_nil\n  end\n\n  @[DataProvider(\"uploaded_file_context\")]\n  def test_uploaded_file_array_of_files(request : AHTTP::Request) : Nil\n    object = @target.resolve request, self.get_config(Array(AHTTP::UploadedFile), ATHA::MapUploadedFile, ATHA::MapUploadedFileConfiguration.new, property_name: \"baz\")\n\n    object = object.should be_a Array(AHTTP::UploadedFile)\n    object.size.should eq 2\n\n    object[0].basename.should eq \"file-small.txt\"\n    object[0].size.should eq 35\n\n    object[1].basename.should eq \"file-big.txt\"\n    object[1].size.should eq 70\n  end\n\n  @[DataProvider(\"uploaded_file_context\")]\n  def test_uploaded_file_array_of_files_with_constraint(request : AHTTP::Request) : Nil\n    @target = ATHR::RequestBody.new @serializer, AVD.validator, @annotation_resolver\n\n    ex = expect_raises AVD::Exception::ValidationFailed do\n      @target.resolve request, self.get_config(\n        Array(AHTTP::UploadedFile),\n        ATHA::MapUploadedFile,\n        ATHA::MapUploadedFileConfiguration.new(\n          name: \"baz\",\n          constraints: AVD::Constraints::File.new(max_size: 50),\n        )\n      )\n    end\n\n    ex.violations.size.should eq 1\n    ex.violations[0].message.should eq \"The file is too large (70.0 bytes). Allowed maximum size is 50.0 bytes.\"\n  end\n\n  def uploaded_file_context : Hash\n    small = AHTTP::UploadedFile.new(\"#{__DIR__}/../../assets/file-small.txt\", \"fie-small.txt\", \"text/plain\", test: true)\n    big = AHTTP::UploadedFile.new(\"#{__DIR__}/../../assets/file-big.txt\", \"fie-big.txt\", \"text/plain\", test: true)\n\n    request = new_request(\n      path: \"/\",\n      method: \"POST\",\n      files: {\n        \"foo\"   => [small],\n        \"bar\"   => [big],\n        \"baz\"   => [small, big],\n        \"empty\" => [] of AHTTP::UploadedFile,\n      }\n    )\n\n    {\n      \"standard\" => {request},\n    }\n  end\n\n  private def get_config(type : T.class, ann = ATHA::MapRequestBody, configuration = ATHA::MapRequestBodyConfiguration.new, property_name : String = \"foo\") forall T\n    metadata = AHK::Controller::ParameterMetadata(T).new(\n      property_name,\n    )\n\n    @annotation_resolver.action_parameter_annotations = ADI::AnnotationConfigurations.new({\n      ann => [\n        configuration,\n      ] of ADI::AnnotationConfigurations::ConfigurationBase,\n    } of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase))\n\n    metadata\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controller/value_resolvers/time_spec.cr",
    "content": "require \"../../spec_helper\"\n\nprivate def resolver(\n  annotations = ADI::AnnotationConfigurations.new,\n) forall T\n  ATHR::Time.new(\n    MockAnnotationResolver.new(\n      action_parameter_annotations: annotations,\n      expected_parameter_name: \"foo\"\n    )\n  )\nend\n\ndescribe ATHR::Time do\n  describe \"#resolve\" do\n    it \"some other type parameter\" do\n      resolver.resolve(new_request, AHK::Controller::ParameterMetadata(Int32).new \"foo\").should be_nil\n      resolver.resolve(new_request, AHK::Controller::ParameterMetadata(Int32?).new \"foo\").should be_nil\n      resolver.resolve(new_request, AHK::Controller::ParameterMetadata(Bool | Float64).new \"foo\").should be_nil\n    end\n\n    it \"type is nilable and the value is nil\" do\n      parameter = AHK::Controller::ParameterMetadata(String?).new \"foo\"\n      request = new_request\n      request.attributes.set \"foo\", nil\n\n      resolver.resolve(request, parameter).should be_nil\n    end\n\n    it \"is not a Time parameter\" do\n      parameter = AHK::Controller::ParameterMetadata(String).new \"foo\"\n      request = new_request\n\n      resolver.resolve(request, parameter).should be_nil\n    end\n\n    it \"type is nilable\" do\n      parameter = AHK::Controller::ParameterMetadata(::Time?).new \"foo\"\n      request = new_request\n      request.attributes.set \"foo\", \"2020-04-07T12:34:56Z\"\n\n      resolver.resolve(request, parameter).should eq Time.utc 2020, 4, 7, 12, 34, 56\n    end\n\n    it \"type a union of another type\" do\n      parameter = AHK::Controller::ParameterMetadata(Int32 | ::Time).new \"foo\"\n      request = new_request\n      request.attributes.set \"foo\", \"2020-04-07T12:34:56Z\"\n\n      resolver.resolve(request, parameter).should eq Time.utc 2020, 4, 7, 12, 34, 56\n    end\n\n    it \"is missing from request attributes\" do\n      parameter = AHK::Controller::ParameterMetadata(::Time).new \"foo\"\n      request = new_request\n\n      resolver.resolve(request, parameter).should be_nil\n    end\n\n    it \"is is a ::Time instance already\" do\n      parameter = AHK::Controller::ParameterMetadata(::Time).new \"foo\"\n      request = new_request\n      request.attributes.set \"foo\", now = Time.utc\n\n      resolver.resolve(request, parameter).should eq now\n    end\n\n    it \"is not a string\" do\n      parameter = AHK::Controller::ParameterMetadata(::Time).new \"foo\"\n      request = new_request\n      request.attributes.set \"foo\", 100\n\n      resolver.resolve(request, parameter).should be_nil\n    end\n\n    it \"parses RFC 3339 by default\" do\n      parameter = AHK::Controller::ParameterMetadata(::Time).new \"foo\"\n      request = new_request\n      request.attributes.set \"foo\", \"2020-04-07T12:34:56Z\"\n\n      resolver.resolve(request, parameter).should eq Time.utc 2020, 4, 7, 12, 34, 56\n    end\n\n    it \"allows specifying a format\" do\n      parameter = AHK::Controller::ParameterMetadata(::Time).new(\"foo\")\n\n      request = new_request\n      request.attributes.set \"foo\", \"2020--04//07  12:34:56\"\n\n      resolver(\n        annotations: ADI::AnnotationConfigurations.new({\n          ATHA::MapTime => [\n            ATHA::MapTimeConfiguration.new(format: \"%Y--%m//%d  %T\"),\n          ] of ADI::AnnotationConfigurations::ConfigurationBase,\n        } of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase)),\n      )\n        .resolve(request, parameter).should eq Time.utc 2020, 4, 7, 12, 34, 56\n    end\n\n    it \"allows specifying a location to parse the format in\" do\n      parameter = AHK::Controller::ParameterMetadata(::Time).new(\"foo\")\n\n      request = new_request\n      request.attributes.set \"foo\", \"2020--04//07  12:34:56\"\n\n      resolver(\n        annotations: ADI::AnnotationConfigurations.new({\n          ATHA::MapTime => [\n            ATHA::MapTimeConfiguration.new(format: \"%Y--%m//%d  %T\", location: Time::Location.fixed(9001)),\n          ] of ADI::AnnotationConfigurations::ConfigurationBase,\n        } of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase)),\n      )\n        .resolve(request, parameter).should eq Time.local 2020, 4, 7, 12, 34, 56, location: Time::Location.fixed(9001)\n    end\n\n    it \"raises an AHK::Exception::BadRequest if a time could not be parsed from the string\" do\n      parameter = AHK::Controller::ParameterMetadata(::Time).new \"foo\"\n      request = new_request\n      request.attributes.set \"foo\", \"foo\"\n\n      expect_raises AHK::Exception::BadRequest, \"Invalid date(time) for parameter 'foo'.\" do\n        resolver.resolve request, parameter\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controller/value_resolvers/uuid_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe ATHR::UUID do\n  describe \"#resolve\" do\n    it \"does not exist in request attributes\" do\n      parameter = AHK::Controller::ParameterMetadata(UUID).new \"foo\"\n      ATHR::UUID.new.resolve(new_request, parameter).should be_nil\n    end\n\n    it \"some other type\" do\n      ATHR::UUID.new.resolve(new_request, AHK::Controller::ParameterMetadata(Int32).new \"foo\").should be_nil\n      ATHR::UUID.new.resolve(new_request, AHK::Controller::ParameterMetadata(Int32?).new \"foo\").should be_nil\n      ATHR::UUID.new.resolve(new_request, AHK::Controller::ParameterMetadata(Bool | String).new \"foo\").should be_nil\n    end\n\n    it \"attribute exists but is not a string\" do\n      parameter = AHK::Controller::ParameterMetadata(UUID).new \"foo\"\n      request = new_request\n      request.attributes.set \"foo\", 100\n\n      ATHR::UUID.new.resolve(request, parameter).should be_nil\n    end\n\n    it \"attribute exists but is nil with a nullable parameter\" do\n      parameter = AHK::Controller::ParameterMetadata(UUID?).new \"foo\"\n      request = new_request\n      request.attributes.set \"foo\", nil\n\n      ATHR::UUID.new.resolve(request, parameter).should be_nil\n    end\n\n    it \"with a valid value\" do\n      parameter = AHK::Controller::ParameterMetadata(UUID).new \"foo\"\n\n      uuid = UUID.random\n\n      request = new_request\n      request.attributes.set \"foo\", uuid.to_s\n\n      ATHR::UUID.new.resolve(request, parameter).should eq uuid\n    end\n\n    it \"type a union of another type\" do\n      parameter = AHK::Controller::ParameterMetadata(UUID | Int32).new \"foo\"\n      request = new_request\n\n      uuid = UUID.random\n\n      request.attributes.set \"foo\", uuid.to_s\n\n      ATHR::UUID.new.resolve(request, parameter).should eq uuid\n    end\n\n    it \"with a valid nilable value\" do\n      parameter = AHK::Controller::ParameterMetadata(UUID?).new \"foo\"\n\n      uuid = UUID.random\n\n      request = new_request\n      request.attributes.set \"foo\", uuid.to_s\n\n      ATHR::UUID.new.resolve(request, parameter).should eq uuid\n    end\n\n    it \"with an invalid value\" do\n      parameter = AHK::Controller::ParameterMetadata(UUID).new \"foo\"\n\n      request = new_request\n      request.attributes.set \"foo\", \"foo\"\n\n      expect_raises AHK::Exception::BadRequest, \"Parameter 'foo' with value 'foo' is not a valid 'UUID'.\" do\n        ATHR::UUID.new.resolve request, parameter\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controller_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe ATH::Controller do\n  describe \".render\" do\n    it \"creates a proper response for the template\" do\n      # ameba:disable Lint/UselessAssign\n      name = \"TEST\"\n      response = ATH::Controller.render \"#{__DIR__}/assets/greeting.ecr\"\n\n      response.status.should eq ::HTTP::Status::OK\n      response.headers[\"content-type\"].should eq \"text/html\"\n      response.content.chomp.should eq \"Greetings, TEST!\"\n    end\n\n    it \"creates a proper response for the template with a layout\" do\n      # ameba:disable Lint/UselessAssign\n      name = \"TEST\"\n      response = ATH::Controller.render \"#{__DIR__}/assets/greeting.ecr\", \"#{__DIR__}/assets/layout.ecr\"\n\n      response.status.should eq ::HTTP::Status::OK\n      response.headers[\"content-type\"].should eq \"text/html\"\n      response.content.chomp.should eq \"<h1>Content:</h1> Greetings, TEST!\"\n    end\n  end\n\n  describe \"#redirect\" do\n    it \"creates an AHTTP::RedirectResponse\" do\n      response = TestController.new.redirect \"URL\"\n\n      response.status.should eq ::HTTP::Status::FOUND\n      response.headers[\"location\"].should eq \"URL\"\n      response.content.should be_empty\n    end\n\n    it \"allows passing a `Path` instance\" do\n      response = TestController.new.redirect Path[\"/app/assets/foo.txt\"]\n\n      response.status.should eq ::HTTP::Status::FOUND\n      response.headers[\"location\"].should eq \"/app/assets/foo.txt\"\n      response.content.should be_empty\n    end\n  end\n\n  it \"#redirect_view\" do\n    response = TestController.new.redirect_view \"URL\", :im_a_teapot\n    view = response.should be_a ATH::View(Nil)\n    view.location.should eq \"URL\"\n    view.status.should eq ::HTTP::Status::IM_A_TEAPOT\n  end\n\n  it \"#route_redirect_view\" do\n    response = TestController.new.route_redirect_view \"get_user_me\"\n    view = response.should be_a ATH::View(Nil)\n    view.route.should eq \"get_user_me\"\n    view.route_params.should be_empty\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controllers/argument_resolver_controller.cr",
    "content": "@[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 101}])]\nclass GenericAnnotationEnabledCustomResolver\n  include ATHR::Interface\n\n  configuration ::MyResolverAnnotation\n\n  def initialize(\n    @annotation_resolver : ATH::AnnotationResolver,\n  ); end\n\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata(Float64)) : Float64?\n    return unless @annotation_resolver.action_parameter_annotations(request, parameter.name).has? MyResolverAnnotation\n\n    3.14\n  end\n\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata(String)) : String?\n    return unless @annotation_resolver.action_parameter_annotations(request, parameter.name).has? MyResolverAnnotation\n\n    \"fooo\"\n  end\n\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : Nil\n  end\nend\n\n@[ARTA::Route(path: \"/argument-resolvers\")]\nclass ArgumentResolverController < ATH::Controller\n  @[ARTA::Post(\"/float\")]\n  def happy_path1(\n    @[MyResolverAnnotation]\n    value : Float64,\n  ) : Float64\n    value\n  end\n\n  @[ARTA::Post(\"/string\")]\n  def happy_path2(\n    @[MyResolverAnnotation]\n    value : String,\n  ) : String\n    value\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controllers/custom_annotation_controller.cr",
    "content": "require \"../spec_helper\"\n\nADI.configuration_annotation SpecAnnotation\nADI.configuration_annotation CustomAnn, id : Int32\nADI.configuration_annotation TopParameterAnn\nADI.configuration_annotation MyApp::NestedParameterAnn\n\n@[ADI::Register]\nstruct CustomAnnotationListener\n  def initialize(\n    @annotation_resolver : ATH::AnnotationResolver,\n  ); end\n\n  @[AEDA::AsEventListener]\n  def on_response(event : AHK::Events::Response) : Nil\n    action_annotations = @annotation_resolver.action_annotations event.request\n\n    if action_annotations.has?(SpecAnnotation)\n      event.response.headers[\"ANNOTATION\"] = \"true\"\n    end\n\n    if custom_ann = action_annotations[CustomAnn]?\n      event.response.headers[\"ANNOTATION_VALUE\"] = custom_ann.id.to_s\n    end\n  end\nend\n\n@[CustomAnn(1)]\nclass AnnotationController < ATH::Controller\n  @[SpecAnnotation]\n  get(\"/with-ann\", return_type: Nil) { }\n  get(\"/without-ann\", return_type: Nil) { }\n\n  @[CustomAnn(2)]\n  get(\"/with-ann-override\", return_type: Nil) { }\n\n  @[ARTA::Get(\"/top-parameter-ann/{id}\")]\n  def top_parameter_ann(@[TopParameterAnn] id : Int32) : Nil\n  end\n\n  @[ARTA::Get(\"/nested-parameter-ann/{id}\")]\n  def nested_parameter_ann(@[MyApp::NestedParameterAnn] id : Int32) : Nil\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controllers/file_upload_controller.cr",
    "content": "class FileUploadController < ATH::Controller\n  @[ARTA::Post(\"/required_single_file_present\")]\n  def required_single_file_present(@[ATHA::MapUploadedFile] file : AHTTP::UploadedFile) : String?\n    file.client_original_name\n  end\n\n  @[ARTA::Post(\"/required_single_file_missing\")]\n  def required_single_file_missing(@[ATHA::MapUploadedFile] file : AHTTP::UploadedFile) : String?\n    file.client_original_name\n  end\n\n  @[ARTA::Post(\"/required_single_file_missing_with_constraint\")]\n  def required_single_file_missing_with_constraint(\n    @[ATHA::MapUploadedFile(constraints: AVD::Constraints::File.new(mime_types: [\"text/plain\"]))]\n    file : AHTTP::UploadedFile,\n  ) : String?\n    file.client_original_name\n  end\n\n  @[ARTA::Post(\"/required_array_present\")]\n  def required_array_present(@[ATHA::MapUploadedFile] file : Array(AHTTP::UploadedFile)) : String?\n    file.first.client_original_name\n  end\n\n  @[ARTA::Post(\"/required_array_empty\")]\n  def required_array_empty(@[ATHA::MapUploadedFile] file : Array(AHTTP::UploadedFile)) : String?\n    file.first.client_original_name\n  end\n\n  @[ARTA::Post(\"/optional_single_file_present\")]\n  def optional_single_file_present(@[ATHA::MapUploadedFile] file : AHTTP::UploadedFile?) : String?\n    file.try &.client_original_name\n  end\n\n  @[ARTA::Post(\"/optional_single_file_missing\")]\n  def optional_single_file_missing(@[ATHA::MapUploadedFile] file : AHTTP::UploadedFile?) : String?\n    file.try &.client_original_name\n  end\n\n  @[ARTA::Post(\"/optional_single_file_missing_with_constraint\")]\n  def optional_single_file_missing_with_constraint(@[ATHA::MapUploadedFile] file : AHTTP::UploadedFile?) : String?\n    file.try &.client_original_name\n  end\n\n  @[ARTA::Post(\"/optional_array_present\")]\n  def optional_array_present(@[ATHA::MapUploadedFile] file : AHTTP::UploadedFile?) : String?\n    file.try &.client_original_name\n  end\n\n  @[ARTA::Post(\"/optional_array_empty\")]\n  def optional_array_empty(@[ATHA::MapUploadedFile] file : AHTTP::UploadedFile?) : String?\n    file.try &.client_original_name\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controllers/prefix_controller.cr",
    "content": "CLASS_PREFIX  = \"/prefix\"\nMETHOD_PREFIX = \"/index\"\n\n@[ARTA::Route(path: CLASS_PREFIX)]\nclass PrefixController < ATH::Controller\n  @[ARTA::Get(path: METHOD_PREFIX)]\n  def index : String\n    \"foo\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controllers/routing_controller.cr",
    "content": "require \"../spec_helper\"\n\n@[ADI::Register]\nclass RoutingController < ATH::Controller\n  def initialize(@request_store : AHTTP::RequestStore); end\n\n  @[ARTA::Get(\"get/safe\")]\n  def safe_request_check : String\n    initial_query = @request_store.request.try &.query\n    sleep 250.milliseconds if initial_query == \"foo\"\n    check_query = @request_store.request.try &.query\n\n    initial_query == check_query ? \"safe\" : \"unsafe\"\n  end\n\n  get \"/container/id\", return_type: UInt64 do\n    ADI.container.object_id\n  end\n\n  @[ARTA::Route(path: \"/head-get/\", methods: {\"HEAD\", \"GET\"})]\n  def head_get_trailing_slash : String\n    \"HEAD-GET/\"\n  end\n\n  @[ARTA::Head(\"/head\")]\n  def head : String\n    \"HEAD\"\n  end\n\n  @[ARTA::Head(\"/get-head\")]\n  def get_head : ATH::View(String)\n    self.view \"GET-HEAD\", headers: ::HTTP::Headers{\"FOO\" => \"BAR\"}\n  end\n\n  get \"/cookies\", return_type: AHTTP::Response do\n    response = AHTTP::Response.new \"FOO\"\n    response.headers << ::HTTP::Cookie.new \"key\", \"value\"\n    response\n  end\n\n  @[ARTA::Post(\"unprocessable\")]\n  def unprocessable : AHTTP::Response\n    AHTTP::Response.new \"\", :unprocessable_entity\n  end\n\n  @[ARTA::Get(\"art/response\")]\n  def response : AHTTP::Response\n    AHTTP::Response.new \"FOO\", 418, ::HTTP::Headers{\"content-type\" => \"BAR\"}\n  end\n\n  @[ARTA::Get(\"art/streamed-response\")]\n  def streamed_response : AHTTP::Response\n    AHTTP::StreamedResponse.new 418, ::HTTP::Headers{\"content-type\" => \"BAR\"} do |io|\n      \"FOO\".to_json io\n    end\n  end\n\n  @[ARTA::Get(\"art/redirect\")]\n  def redirect : AHTTP::RedirectResponse\n    AHTTP::RedirectResponse.new \"https://crystal-lang.org\"\n  end\n\n  @[ARTA::Get(\"url\")]\n  def generate_url : String\n    self.generate_url \"routing_controller_response\"\n  end\n\n  @[ARTA::Get(\"url-hash\")]\n  def generate_url_hash : String\n    self.generate_url \"routing_controller_response\", {\"id\" => 10}\n  end\n\n  @[ARTA::Get(\"url-nt\")]\n  def generate_url_nt : String\n    self.generate_url \"routing_controller_response\", id: 10\n  end\n\n  @[ARTA::Get(\"url-nt-abso\")]\n  def generate_url_nt_absolute : String\n    self.generate_url \"routing_controller_response\", id: 10, reference_type: :absolute_url\n  end\n\n  @[ARTA::Get(\"redirect-url\")]\n  def redirect_url : AHTTP::RedirectResponse\n    self.redirect_to_route \"routing_controller_response\"\n  end\n\n  @[ARTA::Get(\"redirect-url-status\")]\n  def redirect_url_status : AHTTP::RedirectResponse\n    self.redirect_to_route \"routing_controller_response\", :permanent_redirect\n  end\n\n  @[ARTA::Get(\"redirect-url-hash\")]\n  def redirect_url_hash : AHTTP::RedirectResponse\n    self.redirect_to_route \"routing_controller_response\", {\"id\" => 10}\n  end\n\n  @[ARTA::Get(\"redirect-url-nt\")]\n  def redirect_url_nt : AHTTP::RedirectResponse\n    self.redirect_to_route \"routing_controller_response\", id: 10\n  end\n\n  @[ARTA::Get(\"default\")]\n  def default(id : Int32 = 10) : Int32\n    id\n  end\n\n  @[ARTA::Get(\"nilable\")]\n  def nilable(id : Int32?) : Int32?\n    id\n  end\n\n  @[ARTA::Route(\"/custom-method\", methods: \"FOO\")]\n  def custom_http_method : String\n    \"FOO\"\n  end\n\n  @[ATHA::View(status: :accepted)]\n  @[ARTA::Get(\"custom-status\")]\n  def custom_status : String\n    \"foo\"\n  end\n\n  @[ARTA::Post(\"/echo\")]\n  def post_echo(request : AHTTP::Request) : String\n    (request.body.should_not be_nil).gets_to_end\n  end\n\n  @[ARTA::Put(\"/echo\")]\n  def put_echo(request : AHTTP::Request) : String\n    (request.body.should_not be_nil).gets_to_end\n  end\n\n  get \"/macro/get-nil\", return_type: Nil do\n  end\n\n  get \"/macro/add/{num1}/{num2}\", num1 : Int32, num2 : Int32, return_type: Int32 do\n    num1 + num2\n  end\n\n  get \"/macro\" { \"GET\" }\n\n  get \"/macro/{foo<foo>}\", foo : String do\n    foo\n  end\n\n  post \"/macro\" do\n    \"POST\"\n  end\n\n  put \"/macro\" do\n    \"PUT\"\n  end\n\n  patch \"/macro\" do\n    \"PATCH\"\n  end\n\n  delete \"/macro\" do\n    \"DELETE\"\n  end\n\n  link \"/macro\" do\n    \"LINK\"\n  end\n\n  unlink \"/macro\" do\n    \"UNLINK\"\n  end\n\n  head \"/macro\" do\n    \"HEAD\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/controllers/view_controller.cr",
    "content": "require \"../spec_helper\"\n\nprivate record Unserializable\n\nrecord JSONSerializableModel, id : Int32, name : String do\n  include JSON::Serializable\nend\n\nprivate record BothSerializableModel, id : Int32, name : String do\n  include JSON::Serializable\n  include ASR::Serializable\n\n  @[ASRA::Groups(\"foo\")]\n  @name : String\nend\n\n@[ARTA::Route(path: \"view\")]\nclass ViewController < ATH::Controller\n  @[ARTA::Get(\"/unserializable\")]\n  def unserializable : Unserializable\n    Unserializable.new\n  end\n\n  @[ARTA::Get(\"/nil\")]\n  def nil_return : Nil\n  end\n\n  @[ARTA::Get(\"/json\")]\n  def json_serializable : JSONSerializableModel\n    JSONSerializableModel.new 10, \"Bob\"\n  end\n\n  @[ARTA::Get(\"/json-array\")]\n  def json_array_serializable : Array(JSONSerializableModel)\n    [\n      JSONSerializableModel.new(10, \"Bob\"),\n      JSONSerializableModel.new(20, \"Sally\"),\n    ] of JSONSerializableModel\n  end\n\n  @[ARTA::Get(\"/json-array-nested\")]\n  def json_nested_array_serializable : Array(Array(JSONSerializableModel))\n    [[\n      JSONSerializableModel.new(10, \"Bob\"),\n    ]]\n  end\n\n  @[ARTA::Get(\"/json-array-empty\")]\n  def json_empty_array_serializable : Array(JSONSerializableModel)\n    [] of JSONSerializableModel\n  end\n\n  @[ARTA::Get(\"/asr\")]\n  @[ATHA::View(serialization_groups: [\"default\"])]\n  def both_serializable : BothSerializableModel\n    BothSerializableModel.new 20, \"Jim\"\n  end\n\n  @[ARTA::Get(\"/asr-array\")]\n  @[ATHA::View(serialization_groups: [\"default\"])]\n  def both_serializable_array : Array(BothSerializableModel)\n    [\n      BothSerializableModel.new(10, \"Bob\"),\n      BothSerializableModel.new(20, \"Sally\"),\n    ]\n  end\n\n  @[ARTA::Get(\"/json-nested-hash-collection\")]\n  def nested_json_hash_collection : Hash(String, Int32 | JSONSerializableModel)\n    {\"foo\" => 10, \"obj\" => JSONSerializableModel.new(10, \"Bob\")}\n  end\n\n  @[ARTA::Get(\"/json-nested-nt-collection\")]\n  def nested_json_nt_collection : {foo: Int32, obj: JSONSerializableModel}\n    {foo: 10, obj: JSONSerializableModel.new(10, \"Bob\")}\n  end\n\n  @[ARTA::Get(\"/json-nested-hash-array-collection\")]\n  def nested_json_hash_array_collection : Hash(String, Int32 | Array(JSONSerializableModel))\n    {\"foo\" => 10, \"objs\" => [JSONSerializableModel.new(10, \"Bob\")]}\n  end\n\n  @[ARTA::Get(\"/json-nested-nt-array-collection\")]\n  def nested_json_nt_array_collection : {foo: Int32, objs: Array(JSONSerializableModel)}\n    {foo: 10, objs: [JSONSerializableModel.new(10, \"Bob\")]}\n  end\n\n  @[ARTA::Post(\"/status\")]\n  @[ATHA::View(status: :accepted)]\n  def custom_status_code : String\n    \"foo\"\n  end\n\n  @[ARTA::Get(\"\")]\n  def view : ATH::View(String)\n    self.view \"DATA\", :im_a_teapot\n  end\n\n  @[ARTA::Get(\"/array\")]\n  def view_array : ATH::View(Array(JSONSerializableModel))\n    self.view(\n      [\n        JSONSerializableModel.new(10, \"Bob\"),\n        JSONSerializableModel.new(20, \"Sally\"),\n      ],\n      :im_a_teapot\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/custom_annotation_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct CustomAnnotationControllerTest < ATH::Spec::APITestCase\n  def test_with_annotation : Nil\n    self.get \"/with-ann\"\n\n    self.assert_response_header_equals \"ANNOTATION\", \"true\"\n    self.assert_response_header_equals \"ANNOTATION_VALUE\", \"1\"\n  end\n\n  def test_without_annotation : Nil\n    self.get \"/without-ann\"\n\n    self.assert_response_not_has_header \"ANNOTATION\"\n    self.assert_response_header_equals \"ANNOTATION_VALUE\", \"1\"\n  end\n\n  def test_overriding_class_annotation : Nil\n    self.get \"/with-ann-override\"\n\n    self.assert_response_not_has_header \"ANNOTATION\"\n    self.assert_response_header_equals \"ANNOTATION_VALUE\", \"2\"\n  end\n\n  def test_top_level_parameter_ann : Nil\n    self.get \"/top-parameter-ann/10\"\n    self.assert_response_is_successful\n  end\n\n  def test_nested_level_parameter_ann : Nil\n    self.get \"/nested-parameter-ann/20\"\n    self.assert_response_is_successful\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/ext/console/register_commands_spec.cr",
    "content": "require \"../../spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, file: file, preamble: %(require \"../../spec_helper.cr\")\nend\n\n@[ADI::Register]\nclass EagerlyInitializedCommand < ACON::Command\n  class_getter initialized = false\n\n  def initialize\n    @@initialized = true\n    super\n  end\n\n  protected def configure : Nil\n    self\n      .name(\"eagerly-initialized\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n\n@[ADI::Register]\n@[ACONA::AsCommand(\"lazy-initialized\")]\nclass LazyInitializedCommand < ACON::Command\n  class_getter initialized = false\n\n  def initialize\n    @@initialized = true\n    super\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n\n@[ADI::Register]\n@[ACONA::AsCommand(\"annn|tset\", hidden: true, description: \"Test desc\")]\nclass AnnConfiguredCommand < ACON::Command\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n\n@[ADI::Register]\n@[ACONA::AsCommand(\"|empty-name\")]\nclass EmptyCommandName < ACON::Command\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    ACON::Command::Status::SUCCESS\n  end\nend\n\ndescribe ATH do\n  describe \"Console\", tags: \"compiled\" do\n    it \"errors if no name is provided\" do\n      assert_compile_time_error \"Console command 'TestCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.\", <<-CR\n        require \"../../spec_helper.cr\"\n\n        @[ADI::Register]\n        @[ACONA::AsCommand]\n        class TestCommand < ACON::Command\n        end\n      CR\n    end\n  end\n\n  # Fetching the console application initializes everything.\n  # Kinda hacky, but do this in a `before_all` to assert they both start off un-initialized.\n  before_all do\n    EagerlyInitializedCommand.initialized.should be_false\n    LazyInitializedCommand.initialized.should be_false\n  end\n\n  it \"is initialized eagerly if not configured via annotation\" do\n    application = ADI.container.athena_console_application\n    EagerlyInitializedCommand.initialized.should be_true\n    application.has?(\"eagerly-initialized\").should be_true\n    EagerlyInitializedCommand.initialized.should be_true\n  end\n\n  it \"is initialized lazily if configured via annotation\" do\n    application = ADI.container.athena_console_application\n    LazyInitializedCommand.initialized.should be_false\n    application.has?(\"lazy-initialized\").should be_true\n    LazyInitializedCommand.initialized.should be_false # Lazy command wrapper\n    application.get(\"lazy-initialized\").help.should be_empty\n    LazyInitializedCommand.initialized.should be_true\n  end\n\n  it \"applies data from annotation\" do\n    application = ADI.container.athena_console_application\n    application.has?(\"tset\").should be_true\n\n    command = application.get \"annn\"\n    command.hidden?.should be_true\n    command.description.should eq \"Test desc\"\n    command.aliases.should eq [\"tset\"]\n  end\n\n  it \"applies hidden status via empty command name\" do\n    application = ADI.container.athena_console_application\n    command = application.get \"empty-name\"\n    command.name.should eq \"empty-name\"\n    command.hidden?.should be_true\n    command.aliases.should be_empty\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/ext/routing/annotation_route_loader_spec.cr",
    "content": "require \"../../spec_helper\"\n\nprivate def assert_route(\n  route_collection : ART::RouteCollection,\n  file : String = __FILE__,\n  line : Int32 = __LINE__,\n  **args,\n)\n  route_collection.size.should eq 1\n\n  self.assert_route route_collection.first, **args, file: file, line: line\nend\n\nprivate def assert_route(\n  route : Tuple(String, ART::Route),\n  *,\n  path : String = \"/\",\n  methods : Set(String) = Set{\"GET\"},\n  defaults : Hash(String, String?) = Hash(String, String?).new,\n  requirements : Hash(String, String | Regex) = Hash(String, String | Regex).new,\n  host : String? = nil,\n  schemes : Set(String)? = nil,\n  condition : ART::Route::Condition? = nil,\n  name : String? = nil,\n  file : String = __FILE__,\n  line : Int32 = __LINE__,\n) : Nil\n  route_name, route = route\n\n  route_name.should eq(name), line: line, file: file if name\n\n  route.path.should eq(path), line: line, file: file\n  route.methods.should eq(methods), line: line, file: file\n  route.schemes.should eq(schemes), line: line, file: file\n  route.host.should eq(host), line: line, file: file\n  route.requirements.should eq(requirements), line: line, file: file\n\n  route_defaults = route.defaults.dup\n  route_defaults.delete \"_controller\" unless defaults.has_key?(\"_controller\")\n\n  route_defaults.raw?(\"_action\").should be_a AHK::ActionBase\n  route_defaults.delete \"_action\"\n\n  route_defaults.should eq(defaults), line: line, file: file\n\n  if condition\n    route.condition.should_not be_nil, line: line, file: file\n  else\n    route.condition.should be_nil, line: line, file: file\n  end\nend\n\nclass App::CompileController < ATH::Controller\n  @[ARTA::Get(path: \"/\")]\n  def action : Nil; end\nend\n\nclass CompileController < ATH::Controller\n  @[ARTA::Get(\"/\", name: \"action\")]\n  def action : Nil; end\nend\n\n@[ARTA::Route(\n  path: \"parent\",\n  locale: \"de\",\n  format: \"json\",\n  stateless: true,\n  name: \"parent\",\n  requirements: {\"foo\" => \"bar\"},\n  defaults: {\"foo\" => \"bar\"},\n  schemes: [\"https\", \"ftp\"],\n  methods: [\"foo\"],\n  condition: ART::Route::Condition.new { false },\n  priority: 16,\n)]\nclass GlobalsController < ATH::Controller\n  @[ARTA::Route(\"/child\")]\n  def action : Nil; end\nend\n\n@[ARTA::Route(\n  path: \"prefix\",\n)]\nclass PrefixedController < ATH::Controller\n  @[ARTA::Post(\"\")]\n  def empty : Nil; end\n\n  @[ARTA::Post(\"/\")]\n  def slash : Nil; end\n\n  @[ARTA::Get(\"{id}\")]\n  def empty_param(id : String) : Nil; end\n\n  @[ARTA::Get(\"/{id}\")]\n  def slash_param(id : String) : Nil; end\nend\n\n@[ARTA::Route(\"pos-prefix\")]\nclass PositionalPrefixedController < ATH::Controller\n  @[ARTA::Post(\"\")]\n  def empty : Nil; end\n\n  @[ARTA::Post(\"/\")]\n  def slash : Nil; end\n\n  @[ARTA::Get(\"{id}\")]\n  def empty_param(id : String) : Nil; end\n\n  @[ARTA::Get(\"/{id}\")]\n  def slash_param(id : String) : Nil; end\nend\n\n@[ARTA::Route(\n  schemes: [\"BAR\", \"foo\", \"baz\"],\n  methods: [\"foo\", \"baz\", \"bar\"],\n  requirements: {\"foo\" => \"bar\"},\n  defaults: {\"foo\" => \"bar\"},\n  stateless: false\n)]\nclass GlobalsMerges < ATH::Controller\n  @[ARTA::Route(\n    path: \"/\",\n    methods: [\"bar\", \"biz\"],\n    schemes: [\"foo\", \"biz\"],\n    requirements: {\"biz\" => \"baz\"},\n    defaults: {\"biz\" => \"baz\"},\n  )]\n  def action : Nil; end\nend\n\nclass CustomMethodsString < ATH::Controller\n  @[ARTA::Route(\"/\", methods: \"FOO\")]\n  def action : Nil; end\nend\n\nclass CustomMethodsArray < ATH::Controller\n  @[ARTA::Route(\"/\", methods: {\"BAR\"})]\n  def action : Nil; end\nend\n\nclass LocalizedAction < ATH::Controller\n  @[ARTA::Get({\"en\" => \"/USA\", \"de\" => \"/Germany\"})]\n  def action : Nil; end\nend\n\n@[ARTA::Route(path: \"prefix\")]\nclass LocalizedPrefixedAction < ATH::Controller\n  @[ARTA::Get({\"en\" => \"/USA\", \"de\" => \"/Germany\"})]\n  def action : Nil; end\n\n  @[ARTA::Get(path: {\"en\" => \"{id}/USA\", \"de\" => \"/{id}/Germany\"})]\n  def index(id : String) : Nil; end\nend\n\n@[ARTA::Route(path: {\"en\" => \"/USA\", \"de\" => \"/Germany\"})]\nclass LocalizedClass < ATH::Controller\n  @[ARTA::Get(\"\")]\n  def action : Nil; end\n\n  @[ARTA::Get(\"{id}\")]\n  def index(id : String) : Nil; end\nend\n\n@[ARTA::Route(path: {\"en\" => \"/parent\", \"de\" => \"/parent\"})]\nclass LocalizedClassAction < ATH::Controller\n  @[ARTA::Get(path: {\"en\" => \"/USA\", \"de\" => \"/Germany\"})]\n  def action : Nil; end\n\n  @[ARTA::Get(path: {\"en\" => \"{id}/USA\", \"de\" => \"/{id}/Germany\"})]\n  def index(id : String) : Nil; end\nend\n\nclass DefaultArgs < ATH::Controller\n  @[ARTA::Get(\"/{slug}\")]\n  def action(id : Int32, slug : String = \"foo\", blah : Bool = false) : Nil; end\nend\n\nclass RouteDefaultHelpers < ATH::Controller\n  @[ARTA::Get(\"/\", stateless: false, locale: \"de\", format: \"json\")]\n  def action : Nil; end\nend\n\nenum StringificationColor\n  Red\n  Green\n  Blue\nend\n\nclass StringificationController < ATH::Controller\n  @[ARTA::Get(\"/color/{color}\", requirements: {\"color\" => ART::Requirement::Enum(StringificationColor).new, \"foo\" => /foo/, \"bar\" => \"bar\"})]\n  def get_color(color : StringificationColor) : StringificationColor\n    color\n  end\nend\n\nclass MultipleRoutesSingleMethod < ATH::Controller\n  @[ARTA::Get(\"/multiple-routes\")]\n  @[ARTA::Post(\"/multiple-routes\")]\n  @[ARTA::Route(\"/multiple-routes\", methods: \"PATCH\")]\n  def action : Nil; end\nend\n\ndescribe ATH::Routing::AnnotationRouteLoader do\n  describe \".route_collection\" do\n    it \"simple route\" do\n      assert_route(\n        ATH::Routing::AnnotationRouteLoader.populate_collection(App::CompileController),\n        name: \"app_compile_controller_action\",\n        defaults: {\"_controller\" => \"App::CompileController#action\"}\n      )\n    end\n\n    it \"custom route name\" do\n      assert_route(\n        ATH::Routing::AnnotationRouteLoader.populate_collection(CompileController),\n        name: \"action\",\n        defaults: {\"_controller\" => \"CompileController#action\"}\n      )\n    end\n\n    it \"applies defaults from method arguments to defaults\" do\n      assert_route(\n        ATH::Routing::AnnotationRouteLoader.populate_collection(DefaultArgs),\n        path: \"/{slug}\",\n        defaults: {\"slug\" => \"foo\"}\n      )\n    end\n\n    it \"with helper default values\" do\n      assert_route(\n        ATH::Routing::AnnotationRouteLoader.populate_collection(RouteDefaultHelpers),\n        defaults: {\"_stateless\" => \"false\", \"_locale\" => \"de\", \"_format\" => \"json\"}\n      )\n    end\n\n    it \"with a stringable route requirement\" do\n      assert_route(\n        ATH::Routing::AnnotationRouteLoader.populate_collection(StringificationController),\n        path: \"/color/{color}\",\n        requirements: {\"color\" => /red|green|blue/, \"foo\" => /foo/, \"bar\" => /bar/}\n      )\n    end\n\n    it \"allows multiple route annotations on a single method\" do\n      routes = ATH::Routing::AnnotationRouteLoader.populate_collection(MultipleRoutesSingleMethod).routes.to_a\n\n      routes.size.should eq 3\n\n      route = routes[0]\n\n      assert_route(\n        route,\n        name: \"multiple_routes_single_method_action\",\n        path: \"/multiple-routes\",\n        methods: Set{\"PATCH\"}\n      )\n\n      route = routes[1]\n\n      assert_route(\n        route,\n        name: \"multiple_routes_single_method_action_1\",\n        path: \"/multiple-routes\",\n        methods: Set{\"GET\"}\n      )\n\n      route = routes[2]\n\n      assert_route(\n        route,\n        name: \"multiple_routes_single_method_action_2\",\n        path: \"/multiple-routes\",\n        methods: Set{\"POST\"}\n      )\n    end\n\n    describe \"custom route methods\" do\n      it String do\n        assert_route(\n          ATH::Routing::AnnotationRouteLoader.populate_collection(CustomMethodsString),\n          methods: Set{\"FOO\"}\n        )\n      end\n\n      it Enumerable do\n        assert_route(\n          ATH::Routing::AnnotationRouteLoader.populate_collection(CustomMethodsArray),\n          methods: Set{\"BAR\"}\n        )\n      end\n    end\n\n    describe \"localized routes\" do\n      describe \"only on a method\" do\n        it \"without a prefix\" do\n          routes = ATH::Routing::AnnotationRouteLoader.populate_collection(LocalizedAction).routes.to_a\n\n          routes.size.should eq 2\n\n          route = routes[0]\n\n          assert_route(\n            route,\n            name: \"localized_action_action.en\",\n            path: \"/USA\",\n            defaults: {\"_locale\" => \"en\", \"_canonical_route\" => \"localized_action_action\"},\n            requirements: {\"_locale\" => /en/}\n          )\n\n          route = routes[1]\n\n          assert_route(\n            route,\n            name: \"localized_action_action.de\",\n            path: \"/Germany\",\n            defaults: {\"_locale\" => \"de\", \"_canonical_route\" => \"localized_action_action\"},\n            requirements: {\"_locale\" => /de/}\n          )\n        end\n\n        it \"with prefix\" do\n          routes = ATH::Routing::AnnotationRouteLoader.populate_collection(LocalizedPrefixedAction).routes.to_a\n\n          routes.size.should eq 4\n\n          route = routes[0]\n\n          assert_route(\n            route,\n            name: \"localized_prefixed_action_action.en\",\n            path: \"/prefix/USA\",\n            defaults: {\"_locale\" => \"en\", \"_canonical_route\" => \"localized_prefixed_action_action\"},\n            requirements: {\"_locale\" => /en/}\n          )\n\n          route = routes[1]\n\n          assert_route(\n            route,\n            name: \"localized_prefixed_action_action.de\",\n            path: \"/prefix/Germany\",\n            defaults: {\"_locale\" => \"de\", \"_canonical_route\" => \"localized_prefixed_action_action\"},\n            requirements: {\"_locale\" => /de/}\n          )\n\n          route = routes[2]\n\n          assert_route(\n            route,\n            name: \"localized_prefixed_action_index.en\",\n            path: \"/prefix/{id}/USA\",\n            defaults: {\"_locale\" => \"en\", \"_canonical_route\" => \"localized_prefixed_action_index\"},\n            requirements: {\"_locale\" => /en/}\n          )\n\n          route = routes[3]\n\n          assert_route(\n            route,\n            name: \"localized_prefixed_action_index.de\",\n            path: \"/prefix/{id}/Germany\",\n            defaults: {\"_locale\" => \"de\", \"_canonical_route\" => \"localized_prefixed_action_index\"},\n            requirements: {\"_locale\" => /de/}\n          )\n        end\n      end\n\n      it \"only on the class\" do\n        routes = ATH::Routing::AnnotationRouteLoader.populate_collection(LocalizedClass).routes.to_a\n\n        routes.size.should eq 4\n\n        route = routes[0]\n\n        assert_route(\n          route,\n          name: \"localized_class_action.en\",\n          path: \"/USA\",\n          defaults: {\"_locale\" => \"en\", \"_canonical_route\" => \"localized_class_action\"},\n          requirements: {\"_locale\" => /en/}\n        )\n\n        route = routes[1]\n\n        assert_route(\n          route,\n          name: \"localized_class_action.de\",\n          path: \"/Germany\",\n          defaults: {\"_locale\" => \"de\", \"_canonical_route\" => \"localized_class_action\"},\n          requirements: {\"_locale\" => /de/}\n        )\n\n        route = routes[2]\n\n        assert_route(\n          route,\n          name: \"localized_class_index.en\",\n          path: \"/USA/{id}\",\n          defaults: {\"_locale\" => \"en\", \"_canonical_route\" => \"localized_class_index\"},\n          requirements: {\"_locale\" => /en/}\n        )\n\n        route = routes[3]\n\n        assert_route(\n          route,\n          name: \"localized_class_index.de\",\n          path: \"/Germany/{id}\",\n          defaults: {\"_locale\" => \"de\", \"_canonical_route\" => \"localized_class_index\"},\n          requirements: {\"_locale\" => /de/}\n        )\n      end\n\n      it \"on both class and action\" do\n        routes = ATH::Routing::AnnotationRouteLoader.populate_collection(LocalizedClassAction).routes.to_a\n\n        routes.size.should eq 4\n\n        route = routes[0]\n\n        assert_route(\n          route,\n          name: \"localized_class_action_action.en\",\n          path: \"/parent/USA\",\n          defaults: {\"_locale\" => \"en\", \"_canonical_route\" => \"localized_class_action_action\"},\n          requirements: {\"_locale\" => /en/}\n        )\n\n        route = routes[1]\n\n        assert_route(\n          route,\n          name: \"localized_class_action_action.de\",\n          path: \"/parent/Germany\",\n          defaults: {\"_locale\" => \"de\", \"_canonical_route\" => \"localized_class_action_action\"},\n          requirements: {\"_locale\" => /de/}\n        )\n\n        route = routes[2]\n\n        assert_route(\n          route,\n          name: \"localized_class_action_index.en\",\n          path: \"/parent/{id}/USA\",\n          defaults: {\"_locale\" => \"en\", \"_canonical_route\" => \"localized_class_action_index\"},\n          requirements: {\"_locale\" => /en/}\n        )\n\n        route = routes[3]\n\n        assert_route(\n          route,\n          name: \"localized_class_action_index.de\",\n          path: \"/parent/{id}/Germany\",\n          defaults: {\"_locale\" => \"de\", \"_canonical_route\" => \"localized_class_action_index\"},\n          requirements: {\"_locale\" => /de/}\n        )\n      end\n    end\n\n    describe \"globals\" do\n      it \"applies to child routes\" do\n        assert_route(\n          ATH::Routing::AnnotationRouteLoader.populate_collection(GlobalsController),\n          name: \"parent_globals_controller_action\",\n          path: \"/parent/child\",\n          methods: Set{\"FOO\"},\n          condition: ART::Route::Condition.new { false },\n          schemes: Set{\"https\", \"ftp\"},\n          requirements: {\"foo\" => /bar/},\n          defaults: {\"foo\" => \"bar\", \"_locale\" => \"de\", \"_format\" => \"json\", \"_stateless\" => \"true\"}\n        )\n      end\n\n      it \"merges methods and schemes with the child route\" do\n        assert_route(\n          ATH::Routing::AnnotationRouteLoader.populate_collection(GlobalsMerges),\n          path: \"/\",\n          schemes: Set{\"bar\", \"foo\", \"baz\", \"biz\"},\n          methods: Set{\"FOO\", \"BAZ\", \"BAR\", \"BIZ\"},\n          requirements: {\"foo\" => /bar/, \"biz\" => /baz/},\n          defaults: {\"foo\" => \"bar\", \"biz\" => \"baz\", \"_stateless\" => \"false\"}\n        )\n      end\n\n      it \"normalizes prefixed route paths\" do\n        routes = ATH::Routing::AnnotationRouteLoader.populate_collection(PrefixedController).routes.to_a\n\n        routes.size.should eq 4\n\n        route = routes[0]\n\n        assert_route(\n          route,\n          name: \"prefixed_controller_empty\",\n          path: \"/prefix\",\n          methods: Set{\"POST\"}\n        )\n\n        route = routes[1]\n\n        assert_route(\n          route,\n          name: \"prefixed_controller_slash\",\n          path: \"/prefix/\",\n          methods: Set{\"POST\"}\n        )\n\n        route = routes[2]\n\n        assert_route(\n          route,\n          name: \"prefixed_controller_empty_param\",\n          path: \"/prefix/{id}\",\n        )\n\n        route = routes[3]\n\n        assert_route(\n          route,\n          name: \"prefixed_controller_slash_param\",\n          path: \"/prefix/{id}\",\n        )\n      end\n\n      it \"normalizes positional prefixed route paths\" do\n        routes = ATH::Routing::AnnotationRouteLoader.populate_collection(PositionalPrefixedController).routes.to_a\n\n        routes.size.should eq 4\n\n        route = routes[0]\n\n        assert_route(\n          route,\n          name: \"positional_prefixed_controller_empty\",\n          path: \"/pos-prefix\",\n          methods: Set{\"POST\"}\n        )\n\n        route = routes[1]\n\n        assert_route(\n          route,\n          name: \"positional_prefixed_controller_slash\",\n          path: \"/pos-prefix/\",\n          methods: Set{\"POST\"}\n        )\n\n        route = routes[2]\n\n        assert_route(\n          route,\n          name: \"positional_prefixed_controller_empty_param\",\n          path: \"/pos-prefix/{id}\",\n        )\n\n        route = routes[3]\n\n        assert_route(\n          route,\n          name: \"positional_prefixed_controller_slash_param\",\n          path: \"/pos-prefix/{id}\",\n        )\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/file_parser_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct FileParserTest < ASPEC::TestCase\n  def test_parse_happy_path : Nil\n    file1 : AHTTP::UploadedFile? = nil\n    file2 : AHTTP::UploadedFile? = nil\n\n    request = new_request(\n      body: String.build do |io|\n        ::HTTP::FormData.build io, \"boundary\" do |form|\n          # Non HTML file input types have a `nil` filename.\n          form.field(\"age\", 12)\n          form.file(\n            \"success\",\n            File.open(\"#{__DIR__}/assets/foo.txt\"),\n            ::HTTP::FormData::FileMetadata.new(\n              \"foo.txt\"\n            ),\n            headers: ::HTTP::Headers{\n              \"content-type\" => \"text/plain\",\n            }\n          )\n\n          # Skipped because optional HTML file input types have a `\"\"` filename if no file was selected.\n          form.file(\n            \"optional\",\n            IO::Memory.new,\n            ::HTTP::FormData::FileMetadata.new(\n              \"\"\n            ),\n            headers: ::HTTP::Headers{\n              \"content-type\" => \"text/plain\",\n            }\n          )\n\n          form.file(\n            \"too_big\",\n            File.open(\"#{__DIR__}/assets/file-big.txt\"),\n            ::HTTP::FormData::FileMetadata.new(\n              \"file-big.txt\"\n            ),\n            headers: ::HTTP::Headers{\n              \"content-type\" => \"text/plain\",\n            }\n          )\n\n          # Skipped due to max_uploads == 2\n          form.file(\n            \"skipped\",\n            File.open(\"#{__DIR__}/assets/foo.txt\"),\n            ::HTTP::FormData::FileMetadata.new(\n              \"foo.txt\"\n            ),\n            headers: ::HTTP::Headers{\n              \"content-type\" => \"text/plain\",\n            }\n          )\n        end\n      end,\n      headers: ::HTTP::Headers{\n        \"content-type\" => \"multipart/form-data; boundary=\\\"boundary\\\"\",\n      },\n    )\n\n    file_parser = self.target\n    file_parser.parse request\n\n    request.files.keys.should eq [\"success\", \"too_big\"]\n\n    files = request.files[\"success\"]\n    files.size.should eq 1\n    file1 = files[0]\n    file1.status.ok?.should be_true\n    file1.client_original_name.should eq \"foo.txt\"\n    file1.client_original_path.should eq \"foo.txt\"\n    file1.client_mime_type.should eq \"text/plain\"\n    file1.path.should match /file_upload\\.\\w+/\n    file_parser.uploaded_file?(file1.path).should be_true\n\n    files = request.files[\"too_big\"]\n    files.size.should eq 1\n    file2 = files[0]\n    file2.status.size_limit_exceeded?.should be_true\n    file2.client_original_name.should eq \"file-big.txt\"\n    file2.client_original_path.should eq \"file-big.txt\"\n    file2.client_mime_type.should eq \"text/plain\"\n    file2.path.should be_empty\n    file_parser.uploaded_file?(file2.path).should be_false\n\n    request.attributes.get(\"age\", String).should eq \"12\"\n    request.attributes.has?(\"optional\").should be_false\n\n    file_parser.clear\n\n    ::File.exists?(file1.path).should be_false\n  ensure\n    file1.try { |f| ::File.delete? f.path }\n  end\n\n  private def target(max_uploads : Int32 = 2, max_file_size : Int64 = 50) : ATH::FileParser\n    ATH::FileParser.new(\n      nil,\n      max_uploads,\n      max_file_size\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/file_upload_controller_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct FileUploadControllerTest < ATH::Spec::APITestCase\n  def test_required_single_file_present : Nil\n    self.upload_file \"/required_single_file_present\"\n\n    self.assert_response_is_successful\n  end\n\n  def test_optional_single_file_present : Nil\n    self.upload_file \"/optional_single_file_present\"\n\n    self.assert_response_is_successful\n  end\n\n  def test_required_single_file_missing : Nil\n    self.upload_file \"/required_single_file_missing\", \"missing\"\n\n    self.assert_response_has_status :internal_server_error\n    URI.decode(self.response.headers[\"x-debug-exception-message\"]).should contain \"requires that you provide a value for the 'file' parameter.\"\n  end\n\n  def test_optional_single_file_missing : Nil\n    self.upload_file \"/optional_single_file_missing\"\n\n    self.assert_response_is_successful\n  end\n\n  def test_required_single_file_missing_with_constraint : Nil\n    self.upload_file \"/required_single_file_missing_with_constraint\", \"missing\"\n\n    self.assert_response_has_status :internal_server_error\n    URI.decode(self.response.headers[\"x-debug-exception-message\"]).should contain \"requires that you provide a value for the 'file' parameter.\"\n  end\n\n  def test_optional_single_file_missing_with_constraint : Nil\n    self.upload_file \"/optional_single_file_missing_with_constraint\", \"missing\"\n\n    self.assert_response_is_successful\n  end\n\n  def test_required_array_present : Nil\n    self.upload_file \"/required_array_present\"\n\n    self.assert_response_is_successful\n  end\n\n  def test_optional_array_present : Nil\n    self.upload_file \"/optional_array_present\"\n\n    self.assert_response_is_successful\n  end\n\n  def test_required_array_empty : Nil\n    self.upload_file \"/required_array_empty\"\n\n    self.assert_response_is_successful\n  end\n\n  def test_optional_array_empty : Nil\n    self.upload_file \"/optional_array_empty\"\n\n    self.assert_response_is_successful\n  end\n\n  private def upload_file(route : String, name : String = \"file\") : Nil\n    self.post(\n      route,\n      headers: ::HTTP::Headers{\n        \"content-type\" => \"multipart/form-data; boundary=\\\"boundary\\\"\",\n      },\n      body: self.build_payload name\n    )\n  end\n\n  private def build_payload(name : String = \"file\") : String\n    String.build do |io|\n      ::HTTP::FormData.build io, \"boundary\" do |form|\n        form.file(\n          name,\n          File.open(\"#{__DIR__}/assets/foo.txt\"),\n          ::HTTP::FormData::FileMetadata.new(\n            \"foo.txt\"\n          ),\n          headers: ::HTTP::Headers{\n            \"content-type\" => \"text/plain\",\n          }\n        )\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/listeners/cors_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate def new_response_event\n  new_response_event() { }\nend\n\nprivate def new_response_event(& : AHTTP::Request -> _)\n  request = new_request\n  yield request\n  AHK::Events::Response.new request, AHTTP::Response.new\nend\n\nprivate def assert_headers(response : AHTTP::Response, origin : String = \"https://example.com\") : Nil\n  response.headers[\"access-control-allow-credentials\"].should eq \"true\"\n  response.headers[\"access-control-allow-headers\"].should eq \"X-FOO\"\n  response.headers[\"access-control-allow-methods\"].should eq \"POST, GET\"\n  response.headers[\"access-control-allow-origin\"].should eq origin\n  response.headers[\"access-control-max-age\"].should eq \"123\"\nend\n\nprivate def assert_headers_with_wildcard_config_without_request_headers(response : AHTTP::Response) : Nil\n  response.headers[\"access-control-allow-credentials\"]?.should be_nil\n  response.headers[\"access-control-allow-headers\"]?.should be_nil\n  response.headers[\"access-control-allow-methods\"].should eq \"GET, POST, HEAD\"\n  response.headers[\"access-control-allow-origin\"].should eq \"https://example.com\"\n  response.headers[\"access-control-max-age\"].should eq \"123\"\nend\n\nprivate EMPTY_CONFIG    = ATH::Listeners::CORS::Config.new\nprivate WILDCARD_CONFIG = ATH::Listeners::CORS::Config.new(\n  allow_credentials: false,\n  allow_headers: %w(*),\n  allow_origin: %w(*),\n  expose_headers: %w(*),\n  max_age: 123,\n)\nprivate CONFIG = ATH::Listeners::CORS::Config.new(\n  allow_credentials: true,\n  allow_headers: %w(X-FOO),\n  allow_methods: %w(POST GET),\n  allow_origin: [\"https://example.com\", /https:\\/\\/(?:api|app)\\.example\\.com/],\n  expose_headers: %w(HEADER1 HEADER2),\n  max_age: 123\n)\n\ndescribe ATH::Listeners::CORS do\n  describe \"#on_request - request\" do\n    it \"without a configuration defined\" do\n      listener = ATH::Listeners::CORS.new\n      event = new_request_event\n\n      listener.on_request event\n\n      event.response.should be_nil\n      event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n    end\n\n    it \"without the origin header\" do\n      listener = ATH::Listeners::CORS.new EMPTY_CONFIG\n      event = new_request_event\n\n      listener.on_request event\n\n      event.response.should be_nil\n      event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n    end\n\n    describe \"preflight\" do\n      describe :defaults do\n        it \"should only set the default headers\" do\n          listener = ATH::Listeners::CORS.new EMPTY_CONFIG\n          event = new_request_event do |request|\n            request.method = \"OPTIONS\"\n            request.headers.add \"origin\", \"https://example.com\"\n            request.headers.add \"access-control-request-method\", \"GET\"\n          end\n\n          listener.on_request event\n\n          response = event.response.should_not be_nil\n          response.headers[\"vary\"].should eq \"origin\"\n          response.headers[\"access-control-allow-methods\"].should eq \"GET, POST, HEAD\"\n          event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n        end\n      end\n\n      it \"with an unsupported request method\" do\n        listener = ATH::Listeners::CORS.new CONFIG\n        event = new_request_event do |request|\n          request.method = \"OPTIONS\"\n          request.headers.add \"origin\", \"https://example.com\"\n          request.headers.add \"access-control-request-method\", \"LINK\"\n        end\n\n        listener.on_request event\n\n        response = event.response.should_not be_nil\n        response.status.should eq ::HTTP::Status::METHOD_NOT_ALLOWED\n        event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n\n        assert_headers response\n      end\n\n      it \"with an unsupported request header\" do\n        listener = ATH::Listeners::CORS.new CONFIG\n        event = new_request_event do |request|\n          request.method = \"OPTIONS\"\n          request.headers.add \"origin\", \"https://example.com\"\n          request.headers.add \"access-control-request-method\", \"GET\"\n          request.headers.add \"access-control-request-headers\", \"X-BAD\"\n        end\n\n        expect_raises AHK::Exception::Forbidden, \"Unauthorized header: 'X-BAD'\" do\n          listener.on_request event\n        end\n\n        event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n\n        event.response.should be_nil\n      end\n\n      it \"with an invalid origin\" do\n        listener = ATH::Listeners::CORS.new CONFIG\n        event = new_request_event do |request|\n          request.method = \"OPTIONS\"\n          request.headers.add \"origin\", \"https://admin.example.com\"\n          request.headers.add \"access-control-request-method\", \"GET\"\n        end\n\n        listener.on_request event\n\n        response = event.response.should_not be_nil\n        response.headers[\"vary\"].should eq \"origin\"\n        response.headers[\"access-control-allow-methods\"].should eq \"POST, GET\"\n        event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n      end\n\n      describe \"proper request\" do\n        it \"static origin\" do\n          listener = ATH::Listeners::CORS.new CONFIG\n          event = new_request_event do |request|\n            request.method = \"OPTIONS\"\n            request.headers.add \"origin\", \"https://example.com\"\n            request.headers.add \"access-control-request-method\", \"GET\"\n            request.headers.add \"access-control-request-headers\", \"X-FOO\"\n          end\n\n          listener.on_request event\n\n          event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n\n          assert_headers event.response.should_not be_nil\n        end\n\n        it \"regex origin\" do\n          listener = ATH::Listeners::CORS.new CONFIG\n          event = new_request_event do |request|\n            request.method = \"OPTIONS\"\n            request.headers.add \"origin\", \"https://api.example.com\"\n            request.headers.add \"access-control-request-method\", \"GET\"\n            request.headers.add \"access-control-request-headers\", \"X-FOO\"\n          end\n\n          listener.on_request event\n\n          event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n\n          assert_headers event.response.should_not(be_nil), \"https://api.example.com\"\n        end\n      end\n\n      it \"without the access-control-request-headers header\" do\n        listener = ATH::Listeners::CORS.new CONFIG\n        event = new_request_event do |request|\n          request.method = \"OPTIONS\"\n          request.headers.add \"origin\", \"https://example.com\"\n          request.headers.add \"access-control-request-method\", \"GET\"\n        end\n\n        listener.on_request event\n\n        event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n\n        assert_headers event.response.should_not be_nil\n      end\n\n      it \"without the access-control-request-headers header and wildcard in allow_headers config\" do\n        listener = ATH::Listeners::CORS.new WILDCARD_CONFIG\n        event = new_request_event do |request|\n          request.method = \"OPTIONS\"\n          request.headers.add \"origin\", \"https://example.com\"\n          request.headers.add \"access-control-request-method\", \"GET\"\n        end\n\n        listener.on_request event\n\n        event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n\n        assert_headers_with_wildcard_config_without_request_headers event.response.should_not be_nil\n      end\n    end\n\n    describe \"non-preflight\" do\n      it \"with an invalid domain\" do\n        listener = ATH::Listeners::CORS.new CONFIG\n        event = new_request_event do |request|\n          request.method = \"GET\"\n          request.headers.add \"origin\", \"https://example.net\"\n          request.headers.add \"access-control-request-method\", \"GET\"\n          request.headers.add \"access-control-request-headers\", \"X-FOO\"\n        end\n\n        listener.on_request event\n\n        event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_false\n        event.response.should be_nil\n      end\n\n      it \"with a proper request\" do\n        listener = ATH::Listeners::CORS.new CONFIG\n        event = new_request_event do |request|\n          request.method = \"GET\"\n          request.headers.add \"origin\", \"https://example.com\"\n          request.headers.add \"access-control-request-method\", \"GET\"\n          request.headers.add \"access-control-request-headers\", \"X-FOO\"\n        end\n\n        listener.on_request event\n\n        event.request.attributes.has?(ATH::Listeners::CORS::ALLOW_SET_ORIGIN).should be_true\n        event.response.should be_nil\n      end\n    end\n  end\n\n  describe \"#on_response - response\" do\n    describe \"with a proper request\" do\n      it \"static origin\" do\n        listener = ATH::Listeners::CORS.new CONFIG\n        event = new_response_event do |request|\n          request.method = \"GET\"\n          request.headers.add \"origin\", \"https://example.com\"\n          request.headers.add \"access-control-request-method\", \"GET\"\n          request.headers.add \"access-control-request-headers\", \"X-FOO\"\n\n          request.attributes.set ATH::Listeners::CORS::ALLOW_SET_ORIGIN, true\n        end\n\n        listener.on_response event\n\n        event.response.headers[\"access-control-allow-origin\"].should eq \"https://example.com\"\n        event.response.headers[\"access-control-allow-credentials\"].should eq \"true\"\n        event.response.headers[\"access-control-expose-headers\"].should eq \"HEADER1, HEADER2\"\n      end\n\n      it \"valid regex origin\" do\n        listener = ATH::Listeners::CORS.new CONFIG\n        event = new_response_event do |request|\n          request.method = \"GET\"\n          request.headers.add \"origin\", \"https://app.example.com\"\n          request.headers.add \"access-control-request-method\", \"GET\"\n          request.headers.add \"access-control-request-headers\", \"X-FOO\"\n\n          request.attributes.set ATH::Listeners::CORS::ALLOW_SET_ORIGIN, true\n        end\n\n        listener.on_response event\n\n        event.response.headers[\"access-control-allow-origin\"].should eq \"https://app.example.com\"\n        event.response.headers[\"access-control-allow-credentials\"].should eq \"true\"\n        event.response.headers[\"access-control-expose-headers\"].should eq \"HEADER1, HEADER2\"\n      end\n    end\n\n    it \"that should not allow setting origin\" do\n      listener = ATH::Listeners::CORS.new CONFIG\n      event = new_response_event do |request|\n        request.method = \"GET\"\n        request.headers.add \"origin\", \"https://example.com\"\n        request.headers.add \"access-control-request-method\", \"GET\"\n        request.headers.add \"access-control-request-headers\", \"X-FOO\"\n\n        request.attributes.set ATH::Listeners::CORS::ALLOW_SET_ORIGIN, false\n      end\n\n      listener.on_response event\n\n      event.response.headers.size.should eq 2\n    end\n\n    it \"without a configuration defined\" do\n      listener = ATH::Listeners::CORS.new\n      event = new_response_event\n\n      listener.on_response event\n\n      event.response.headers.size.should eq 2\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/listeners/file_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class MockFileParser < ATH::FileParser\n  getter? parse_called : Bool = false\n  getter? clear_called : Bool = false\n\n  def parse(request : AHTTP::Request) : Nil\n    @parse_called = true\n  end\n\n  def clear : Nil\n    @clear_called = true\n  end\nend\n\ndescribe ATH::Listeners::File do\n  describe \"#on_request\" do\n    it \"no-ops when the request is not `multipart/form-data`\" do\n      ATH::Listeners::File.new(file_parser = MockFileParser.new(nil, 1, 0)).on_request new_request_event\n\n      file_parser.parse_called?.should be_false\n    end\n\n    it \"calls parse when the request is `multipart/form-data`\" do\n      ATH::Listeners::File\n        .new(file_parser = MockFileParser.new(nil, 1, 0))\n        .on_request new_request_event(\n          headers: ::HTTP::Headers{\n            \"content-type\" => \"multipart/form-data\",\n          }\n        )\n\n      file_parser.parse_called?.should be_true\n    end\n  end\n\n  describe \"#on_terminate\" do\n    it \"calls clear\" do\n      ATH::Listeners::File\n        .new(file_parser = MockFileParser.new(nil, 1, 0))\n        .on_terminate AHK::Events::Terminate.new new_request, AHTTP::Response.new\n\n      file_parser.clear_called?.should be_true\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/listeners/format_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct FormatListenerTest < ASPEC::TestCase\n  def test_fallback_format : Nil\n    event = new_request_event\n\n    request_store = AHTTP::RequestStore.new\n    request_store.request = event.request\n\n    negotiator = ATH::View::FormatNegotiator.new request_store\n    negotiator.add self.request_matcher(/\\/test/), ATH::View::FormatNegotiator::Rule.new(fallback_format: \"xml\")\n\n    listener = ATH::Listeners::Format.new negotiator\n\n    listener.on_request event\n\n    event.request.request_format.should eq \"xml\"\n    event.request.attributes.get?(\"media_type\").should eq \"text/xml\"\n  end\n\n  # TODO: Supports zones?\n\n  def test_stop_listener : Nil\n    event = new_request_event\n    event.request.request_format = \"xml\"\n\n    request_store = AHTTP::RequestStore.new\n    request_store.request = event.request\n\n    negotiator = ATH::View::FormatNegotiator.new request_store\n    negotiator.add self.request_matcher(/\\/test/), ATH::View::FormatNegotiator::Rule.new(stop: true)\n    negotiator.add self.request_matcher(/\\/test/), ATH::View::FormatNegotiator::Rule.new(fallback_format: \"json\")\n\n    listener = ATH::Listeners::Format.new negotiator\n\n    listener.on_request event\n\n    event.request.request_format.should eq \"xml\"\n    event.request.attributes.get?(\"media_type\").should be_nil\n  end\n\n  def test_cannot_resolve_format : Nil\n    event = new_request_event\n\n    request_store = AHTTP::RequestStore.new\n    request_store.request = event.request\n\n    negotiator = ATH::View::FormatNegotiator.new request_store\n\n    listener = ATH::Listeners::Format.new negotiator\n\n    expect_raises AHK::Exception::NotAcceptable, \"No matching accepted Response format could be determined.\" do\n      listener.on_request event\n    end\n  end\n\n  @[DataProvider(\"format_provider\")]\n  # Doesn't override request format if it was already set.\n  def test_uses_specified_format(format : String?, expected : String, media_type : String?) : Nil\n    event = new_request_event\n\n    if format\n      event.request.request_format = format\n    end\n\n    request_store = AHTTP::RequestStore.new\n    request_store.request = event.request\n\n    negotiator = ATH::View::FormatNegotiator.new request_store\n    negotiator.add self.request_matcher(/\\/test/), ATH::View::FormatNegotiator::Rule.new(fallback_format: \"xml\")\n\n    listener = ATH::Listeners::Format.new negotiator\n\n    listener.on_request event\n\n    event.request.request_format.should eq expected\n    event.request.attributes.get?(\"media_type\").should eq media_type\n  end\n\n  def format_provider : Tuple\n    {\n      {nil, \"xml\", \"text/xml\"},\n      {\"html\", \"html\", nil},\n    }\n  end\n\n  private def request_matcher(path : Regex) : AHTTP::RequestMatcher::Interface\n    AHTTP::RequestMatcher.new AHTTP::RequestMatcher::Path.new path\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/listeners/view_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class MockViewHandler\n  include ATH::View::ViewHandlerInterface\n\n  getter! view : ATH::ViewBase\n\n  def register_handler(format : String, handler : ATH::View::FormatHandlerInterface | Proc(ATH::View::ViewHandlerInterface, ATH::ViewBase, AHTTP::Request, String, AHTTP::Response)) : Nil\n  end\n\n  def supports?(format : String) : Bool\n    true\n  end\n\n  def handle(view : ATH::ViewBase, request : AHTTP::Request? = nil) : AHTTP::Response\n    @view = view\n\n    AHTTP::Response.new\n  end\n\n  def create_redirect_response(view : ATH::ViewBase, location : String, format : String) : AHTTP::Response\n    AHTTP::Response.new\n  end\n\n  def create_response(view : ATH::ViewBase, request : AHTTP::Request, format : String) : AHTTP::Response\n    AHTTP::Response.new\n  end\nend\n\nprivate def get_ann_configs(config : ADI::AnnotationConfigurations::ConfigurationBase) : ADI::AnnotationConfigurations\n  ADI::AnnotationConfigurations.new ADI::AnnotationConfigurations::AnnotationHash{ATHA::View => [config] of ADI::AnnotationConfigurations::ConfigurationBase}\nend\n\ndescribe ATH::Listeners::View do\n  describe \"#call\" do\n    it \"non ATH::View\" do\n      request = new_request\n      event = AHK::Events::View.new request, \"FOO\"\n      view_handler = MockViewHandler.new\n\n      ATH::Listeners::View.new(view_handler, MockAnnotationResolver.new).on_view event\n\n      view_handler.view.data.should eq \"FOO\"\n      view_handler.view.format.should eq \"json\"\n      view_handler.view.context.groups.try &.should be_empty\n      view_handler.view.context.emit_nil?.should be_nil\n    end\n\n    it ATH::View do\n      request = new_request\n      view = ATH::View.new(\"BAR\")\n      view.format = \"xml\"\n      event = AHK::Events::View.new request, view\n      view_handler = MockViewHandler.new\n\n      ATH::Listeners::View.new(view_handler, MockAnnotationResolver.new).on_view event\n\n      view_handler.view.data.should eq \"BAR\"\n      view_handler.view.format.should eq \"xml\"\n      view_handler.view.context.groups.try &.should be_empty\n    end\n\n    it \"mutating response\" do\n      request = new_request\n      event = AHK::Events::View.new request, \"FOO\"\n      view_handler = MockViewHandler.new\n\n      event.action_result = \"BAR\"\n      ATH::Listeners::View.new(view_handler, MockAnnotationResolver.new).on_view event\n\n      view_handler.view.data.should eq \"BAR\"\n      view_handler.view.format.should eq \"json\"\n      view_handler.view.context.groups.try &.should be_empty\n    end\n\n    describe ATHA::View do\n      describe \"status\" do\n        it \"with status\" do\n          request = new_request\n          event = AHK::Events::View.new request, \"FOO\"\n          view_handler = MockViewHandler.new\n\n          ATH::Listeners::View.new(\n            view_handler,\n            MockAnnotationResolver.new(\n              action_annotations: get_ann_configs(ATHA::ViewConfiguration.new(status: :found))\n            )\n          ).on_view event\n\n          view_handler.view.status.should eq ::HTTP::Status::FOUND\n        end\n\n        it \"when the view already has a status\" do\n          request = new_request\n          view = ATH::View.new \"FOO\", status: :gone\n          event = AHK::Events::View.new request, view\n          view_handler = MockViewHandler.new\n\n          ATH::Listeners::View.new(\n            view_handler,\n            MockAnnotationResolver.new(\n              action_annotations: get_ann_configs(ATHA::ViewConfiguration.new(status: :found))\n            )\n          ).on_view event\n\n          view_handler.view.status.should eq ::HTTP::Status::GONE\n        end\n\n        it \"when the view already has a status, but it's OK\" do\n          request = new_request\n          view = ATH::View.new \"FOO\", status: :ok\n          event = AHK::Events::View.new request, view\n          view_handler = MockViewHandler.new\n\n          ATH::Listeners::View.new(\n            view_handler,\n            MockAnnotationResolver.new(\n              action_annotations: get_ann_configs(ATHA::ViewConfiguration.new(status: :found))\n            )\n          ).on_view event\n\n          view_handler.view.status.should eq ::HTTP::Status::FOUND\n        end\n      end\n\n      describe \"serialization_groups\" do\n        it \"and the view doesn't have any groups already\" do\n          request = new_request\n          event = AHK::Events::View.new request, \"FOO\"\n          view_handler = MockViewHandler.new\n\n          ATH::Listeners::View.new(\n            view_handler,\n            MockAnnotationResolver.new(\n              action_annotations: get_ann_configs(ATHA::ViewConfiguration.new(serialization_groups: [\"one\", \"two\"]))\n            )\n          ).on_view event\n\n          groups = view_handler.view.context.groups.should_not be_nil\n          groups.should eq Set{\"one\", \"two\"}\n        end\n\n        it \"and the view already has some groups\" do\n          request = new_request\n          view = ATH::View.new \"FOO\"\n          view.context.add_groups \"three\", \"four\"\n\n          event = AHK::Events::View.new request, view\n          view_handler = MockViewHandler.new\n\n          ATH::Listeners::View.new(\n            view_handler,\n            MockAnnotationResolver.new(\n              action_annotations: get_ann_configs(ATHA::ViewConfiguration.new(serialization_groups: [\"one\", \"two\"]))\n            )\n          ).on_view event\n\n          groups = view_handler.view.context.groups.should_not be_nil\n          groups.should eq Set{\"three\", \"four\", \"one\", \"two\"}\n        end\n      end\n\n      it \"emit_nil\" do\n        request = new_request\n        event = AHK::Events::View.new request, \"FOO\"\n        view_handler = MockViewHandler.new\n\n        ATH::Listeners::View.new(\n          view_handler,\n          MockAnnotationResolver.new(\n            action_annotations: get_ann_configs(ATHA::ViewConfiguration.new(emit_nil: true))\n          )\n        ).on_view event\n\n        view_handler.view.context.emit_nil?.should be_true\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/prefix_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct ControllerPrefixTest < ATH::Spec::APITestCase\n  def test_controller_with_prefix : Nil\n    self.get \"/prefix/index\"\n\n    self.assert_response_is_successful\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/routing_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct RoutingTest < ATH::Spec::APITestCase\n  def test_is_concurrently_safe : Nil\n    spawn do\n      sleep 100.milliseconds\n      self.get(\"/get/safe?bar\").body.should eq %(\"safe\")\n    end\n    self.get(\"/get/safe?foo\").body.should eq %(\"safe\")\n  end\n\n  def test_head_request : Nil\n    response = self.head \"/head\"\n    response.status.should eq ::HTTP::Status::OK\n    response.body.should be_empty\n    response.headers[\"content-length\"].should eq \"6\" # JSON encoding adds 2 extra `\"` chars\n  end\n\n  def test_head_request_on_get_endpoint : Nil\n    response = self.head \"/get-head\"\n    response.status.should eq ::HTTP::Status::OK\n    response.body.should be_empty\n    response.headers[\"FOO\"].should eq \"BAR\"           # Actually runs the controller action code\n    response.headers[\"content-length\"].should eq \"10\" # JSON encoding adds 2 extra `\"` chars\n  end\n\n  def test_does_not_reuse_container_with_keep_alive_connections : Nil\n    response1 = self.get(\"/container/id\", headers: ::HTTP::Headers{\"connection\" => \"keep-alive\"}).body\n\n    self.init_container\n\n    response2 = self.get(\"/container/id\", headers: ::HTTP::Headers{\"connection\" => \"keep-alive\"}).body\n\n    response1.should_not eq response2\n  end\n\n  def test_route_doesnt_exist : Nil\n    response = self.get \"/fake/route\"\n    response.status.should eq ::HTTP::Status::NOT_FOUND\n    response.body.should eq %({\"code\":404,\"message\":\"No route found for 'GET /fake/route'.\"})\n  end\n\n  def test_route_doesnt_exist_with_referrer : Nil\n    # This is misspelled on purpose, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer.\n    response = self.get \"/fake/route\", headers: ::HTTP::Headers{\"referer\" => \"somebody\"} # spellchecker:disable-line\n    response.status.should eq ::HTTP::Status::NOT_FOUND\n    response.body.should eq %({\"code\":404,\"message\":\"No route found for 'GET /fake/route' (from: 'somebody').\"})\n  end\n\n  def test_invalid_method : Nil\n    response = self.post \"/art/response\"\n    response.status.should eq ::HTTP::Status::METHOD_NOT_ALLOWED\n    response.body.should eq %({\"code\":405,\"message\":\"No route found for 'POST /art/response': Method Not Allowed (Allow: GET).\"})\n  end\n\n  def test_allows_returning_an_athena_response : Nil\n    response = self.get \"/art/response\"\n    response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n    response.headers[\"content-type\"].should eq \"BAR\"\n    response.headers[\"content-length\"].should eq \"3\"\n    response.headers.has_key?(\"transfer-encoding\").should be_false\n    response.body.should eq \"FOO\"\n  end\n\n  def test_allows_returning_a_streamed_response : Nil\n    response = self.get \"/art/streamed-response\"\n    response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n    response.headers[\"content-type\"].should eq \"BAR\"\n    response.headers.has_key?(\"content-length\").should be_false\n    response.headers[\"transfer-encoding\"].should eq \"chunked\"\n    response.body.should eq %(\"FOO\")\n  end\n\n  def test_it_supports_redirects : Nil\n    response = self.get \"/art/redirect\"\n    response.status.should eq ::HTTP::Status::FOUND\n    response.headers[\"location\"].should eq \"https://crystal-lang.org\"\n    response.body.should be_empty\n  end\n\n  def test_it_supports_custom_http_methods : Nil\n    self.request(\"FOO\", \"/custom-method\").body.should eq %(\"FOO\")\n  end\n\n  def test_custom_response_status_get : Nil\n    self.get \"/custom-status\"\n\n    self.assert_response_has_status :accepted\n  end\n\n  def test_custom_response_status_head : Nil\n    self.head \"/custom-status\"\n\n    self.assert_response_has_status :accepted\n  end\n\n  def test_uses_default_value_if_no_other_value_provided : Nil\n    self.get(\"/default\").body.should eq \"10\"\n  end\n\n  def test_uses_nil_if_no_other_value_provided_and_is_nilable : Nil\n    self.get(\"/nilable\").body.should eq \"null\"\n  end\n\n  def test_macro_dsl_nil_return_type : Nil\n    response = self.get \"/macro/get-nil\"\n    response.status.should eq ::HTTP::Status::NO_CONTENT\n    response.body.should be_empty\n  end\n\n  def test_macro_dsl_with_arguments : Nil\n    self.get(\"/macro/add/50/25\").body.should eq \"75\"\n  end\n\n  def test_macro_dsl_get : Nil\n    response = self.get \"/macro\"\n    response.status.should eq ::HTTP::Status::OK\n    response.body.should eq %(\"GET\")\n  end\n\n  def test_macro_dsl_head : Nil\n    response = self.head \"/macro\"\n    response.status.should eq ::HTTP::Status::OK\n    response.body.should be_empty\n  end\n\n  {% for method in [\"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"LINK\", \"UNLINK\"] %}\n    def test_macro_dsl_{{method.downcase.id}} : Nil\n      self.request({{method}}, \"/macro\").body.should eq %({{method}})\n      self.{{method.downcase.id}}(\"/macro\").body.should eq %({{method}})\n    end\n  {% end %}\n\n  def test_get_helper_method : Nil\n    self.get(\"/macro\").body.should eq %(\"GET\")\n  end\n\n  def test_post_helper_method : Nil\n    self.post(\"/macro\").body.should eq %(\"POST\")\n    self.post(\"/echo\", \"BODY\").body.should eq %(\"BODY\")\n  end\n\n  def test_put_helper_method : Nil\n    self.put(\"/macro\").body.should eq %(\"PUT\")\n    self.put(\"/echo\", \"BODY\").body.should eq %(\"BODY\")\n  end\n\n  def test_delete_helper_method : Nil\n    self.delete(\"/macro\").body.should eq %(\"DELETE\")\n  end\n\n  def test_athena_request : Nil\n    self.request(AHTTP::Request.new(\"GET\", \"/macro\")).body.should eq %(\"GET\")\n  end\n\n  def test_http_request : Nil\n    self.request(::HTTP::Request.new(\"GET\", \"/macro\")).body.should eq %(\"GET\")\n  end\n\n  def test_constraints_404_if_no_match : Nil\n    response = self.get \"/macro/bar\"\n    response.status.should eq ::HTTP::Status::NOT_FOUND\n    response.body.should eq %({\"code\":404,\"message\":\"No route found for 'GET /macro/bar'.\"})\n  end\n\n  def test_constraints_routes_if_match : Nil\n    self.get(\"/macro/foo\").body.should eq %(\"foo\")\n  end\n\n  def test_generate_url_no_args : Nil\n    self.get(\"/url\").body.should eq %(\"/art/response\")\n  end\n\n  def test_generate_url_hash : Nil\n    self.get(\"/url-hash\").body.should eq %(\"/art/response?id=10\")\n  end\n\n  def test_generate_url_named_tuple : Nil\n    self.get(\"/url-nt\").body.should eq %(\"/art/response?id=10\")\n  end\n\n  def test_generate_url_named_tuple_abso : Nil\n    self.get(\"/url-nt-abso\", headers: ::HTTP::Headers{\"host\" => \"crystal-lang.org\"}).body.should eq %(\"http://crystal-lang.org/art/response?id=10\")\n  end\n\n  def test_redirect_to_route : Nil\n    self.get \"/redirect-url\"\n\n    self.assert_response_redirects \"/art/response\", :found\n  end\n\n  def test_redirect_to_route_status : Nil\n    self.get \"/redirect-url-status\"\n\n    self.assert_response_redirects \"/art/response\", :permanent_redirect\n  end\n\n  def test_redirect_to_route_hash : Nil\n    self.get \"/redirect-url-hash\"\n\n    self.assert_response_redirects \"/art/response?id=10\", :found\n  end\n\n  def test_redirect_to_route_nt : Nil\n    self.get \"/redirect-url-nt\"\n\n    self.assert_response_redirects \"/art/response?id=10\", :found\n  end\n\n  def test_using_route_handler_directly_with_http_request : Nil\n    response = self.client.container.athena_http_kernel.handle ::HTTP::Request.new \"GET\", \"/art/response\"\n    response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n    response.content.should eq \"FOO\"\n  end\n\n  def test_applies_cookies_to_actual_response : Nil\n    self.get \"/cookies\"\n\n    self.assert_cookie_has_value \"key\", \"value\"\n  end\n\n  def test_redirects_get_request_to_route_without_trailing_slash : Nil\n    self.get \"/macro/get-nil/\", headers: ::HTTP::Headers{\"host\" => \"localhost\"}\n\n    self.assert_response_redirects \"http://localhost/macro/get-nil\"\n  end\n\n  def test_redirects_head_request_to_route_without_trailing_slash : Nil\n    self.head \"/head/\", headers: ::HTTP::Headers{\"host\" => \"localhost\"}\n\n    self.assert_response_redirects \"http://localhost/head\"\n  end\n\n  def test_redirects_get_request_to_route_with_trailing_slash : Nil\n    self.get \"/head-get\", headers: ::HTTP::Headers{\"host\" => \"localhost\"}\n\n    self.assert_response_redirects \"http://localhost/head-get/\"\n  end\n\n  def test_redirects_head_request_to_route_with_trailing_slash : Nil\n    self.head \"/head-get\", headers: ::HTTP::Headers{\"host\" => \"localhost\"}\n\n    self.assert_response_redirects \"http://localhost/head-get/\"\n  end\n\n  def test_does_not_redirect_post_requests : Nil\n    self.post \"/art/response/\"\n\n    self.assert_response_has_status :not_found\n  end\n\n  def test_unprocessable : Nil\n    self.post \"/unprocessable\"\n\n    self.assert_response_is_unprocessable\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/request/attribute_equals_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct AttributeEqualsExpectationTest < ASPEC::TestCase\n  def test_match_valid : Nil\n    request = new_request\n    request.attributes.set \"foo\", \"bar\"\n\n    ATH::Spec::Expectations::Request::AttributeEquals.new(\"foo\", \"bar\").match(request).should be_true\n  end\n\n  def test_match_invalid : Nil\n    request = new_request\n    request.attributes.set \"foo\", \"bar\"\n\n    ATH::Spec::Expectations::Request::AttributeEquals.new(\"foo\", \"baz\").match(request).should be_false\n    ATH::Spec::Expectations::Request::AttributeEquals.new(\"bar\", \"bar\").match(request).should be_false\n  end\n\n  def test_failure_message : Nil\n    ATH::Spec::Expectations::Request::AttributeEquals.new(\"foo\", \"bar\")\n      .failure_message(new_request)\n      .should contain \"Failed asserting that the request has attribute 'foo' with value 'bar'.\"\n  end\n\n  def test_failure_message_with_description : Nil\n    ATH::Spec::Expectations::Request::AttributeEquals.new(\"foo\", \"bar\", description: \"Oh noes\")\n      .failure_message(new_request)\n      .should contain \"Oh noes\\n\\nFailed asserting that the request has attribute 'foo' with value 'bar'.\"\n  end\n\n  def test_negative_failure_message : Nil\n    ATH::Spec::Expectations::Request::AttributeEquals.new(\"foo\", \"bar\")\n      .negative_failure_message(new_request)\n      .should contain \"Failed asserting that the request does not have attribute 'foo' with value 'bar'.\"\n  end\n\n  def test_negative_failure_message_with_description : Nil\n    ATH::Spec::Expectations::Request::AttributeEquals.new(\"foo\", \"bar\", description: \"Oh noes\")\n      .negative_failure_message(new_request)\n      .should contain \"Oh noes\\n\\nFailed asserting that the request does not have attribute 'foo' with value 'bar'.\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/response/cookie_value_equals_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct CookieValueEqualsExpectationTest < ASPEC::TestCase\n  def test_match_valid : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\"\n\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\").match(response).should be_true\n  end\n\n  def test_match_valid_custom_path : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", path: \"/path\"\n\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/path\").match(response).should be_true\n  end\n\n  def test_match_valid_custom_domain : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", domain: \"example.com\"\n\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", domain: \"example.com\").match(response).should be_true\n  end\n\n  def test_match_valid_custom_path_and_domain : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", path: \"/path\", domain: \"example.com\"\n\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/path\", domain: \"example.com\").match(response).should be_true\n  end\n\n  def test_match_invalid : Nil\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\").match(new_response).should be_false\n  end\n\n  def test_match_invalid_diff_path\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", path: \"/path\"\n\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\").match(response).should be_false\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/\").match(response).should be_false\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/bar\").match(response).should be_false\n  end\n\n  def test_match_invalid_diff_domain\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", domain: \"example.com\"\n\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\").match(response).should be_false\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", domain: \"foo.example.com\").match(response).should be_false\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", domain: \"example.net\").match(response).should be_false\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", domain: \"domain.com\").match(response).should be_false\n  end\n\n  def test_match_invalid_diff_domain_and_path\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", path: \"/path\", domain: \"example.com\"\n\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\").match(response).should be_false\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/bar\", domain: \"example.com\").match(response).should be_false\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/path\", domain: \"domain.com\").match(response).should be_false\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/bar\", domain: \"domain.com\").match(response).should be_false\n  end\n\n  def test_failure_message : Nil\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has cookie 'foo' with value 'bar'.\"\n  end\n\n  def test_failure_message_with_path : Nil\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/path\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has cookie 'foo' with path '/path' with value 'bar'.\"\n  end\n\n  def test_failure_message_with_domain : Nil\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", domain: \"example.com\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has cookie 'foo' for domain 'example.com' with value 'bar'.\"\n  end\n\n  def test_failure_message_with_path_and_domain : Nil\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/path\", domain: \"example.com\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has cookie 'foo' with path '/path' for domain 'example.com' with value 'bar'.\"\n  end\n\n  def test_negative_failure_message : Nil\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have cookie 'foo' with value 'bar'.\"\n  end\n\n  def test_negative_failure_message_with_path : Nil\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/path\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have cookie 'foo' with path '/path' with value 'bar'.\"\n  end\n\n  def test_negative_failure_message_with_domain : Nil\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", domain: \"example.com\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have cookie 'foo' for domain 'example.com' with value 'bar'.\"\n  end\n\n  def test_negative_failure_message_with_path_and_domain : Nil\n    ATH::Spec::Expectations::Response::CookieValueEquals.new(\"foo\", \"bar\", path: \"/path\", domain: \"example.com\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have cookie 'foo' with path '/path' for domain 'example.com' with value 'bar'.\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/response/format_equals_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct FormatEqualsExpectationTest < ASPEC::TestCase\n  def test_match_valid : Nil\n    ATH::Spec::Expectations::Response::FormatEquals.new(new_request, \"json\").match(new_response headers: ::HTTP::Headers{\"content-type\" => \"application/json\"}).should be_true\n  end\n\n  def test_match_valid_no_format : Nil\n    ATH::Spec::Expectations::Response::FormatEquals.new(new_request).match(new_response headers: ::HTTP::Headers{\"content-type\" => \"\"}).should be_true\n  end\n\n  def test_match_invalid : Nil\n    ATH::Spec::Expectations::Response::FormatEquals.new(new_request).match(new_response).should be_false\n    ATH::Spec::Expectations::Response::FormatEquals.new(new_request, \"json\").match(new_response).should be_false\n    ATH::Spec::Expectations::Response::FormatEquals.new(new_request, \"json\").match(new_response headers: ::HTTP::Headers{\"content-type\" => \"text/html\"}).should be_false\n  end\n\n  def test_failure_message : Nil\n    ATH::Spec::Expectations::Response::FormatEquals.new(new_request, \"json\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response format is 'json':\\nHTTP/1.1 200\"\n  end\n\n  def test_failure_message_no_format : Nil\n    ATH::Spec::Expectations::Response::FormatEquals.new(new_request)\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response format is 'null':\\nHTTP/1.1 200\"\n  end\n\n  def test_negative_failure_message : Nil\n    ATH::Spec::Expectations::Response::FormatEquals.new(new_request, \"json\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response format is not 'json':\\nHTTP/1.1 200\"\n  end\n\n  def test_negative_failure_message_no_format : Nil\n    ATH::Spec::Expectations::Response::FormatEquals.new(new_request)\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response format is not 'null':\\nHTTP/1.1 200\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/response/has_cookie_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct HasCookieExpectationTest < ASPEC::TestCase\n  def test_match_valid : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\"\n\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\").match(response).should be_true\n  end\n\n  def test_match_valid_custom_path : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", path: \"/path\"\n\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/path\").match(response).should be_true\n  end\n\n  def test_match_valid_custom_domain : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", domain: \"example.com\"\n\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", domain: \"example.com\").match(response).should be_true\n  end\n\n  def test_match_valid_custom_path_and_domain : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", path: \"/path\", domain: \"example.com\"\n\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/path\", domain: \"example.com\").match(response).should be_true\n  end\n\n  def test_match_invalid : Nil\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\").match(new_response).should be_false\n  end\n\n  def test_match_invalid_diff_path\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", path: \"/path\"\n\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\").match(response).should be_false\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/\").match(response).should be_false\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/bar\").match(response).should be_false\n  end\n\n  def test_match_invalid_diff_domain\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", domain: \"example.com\"\n\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\").match(response).should be_false\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", domain: \"foo.example.com\").match(response).should be_false\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", domain: \"example.net\").match(response).should be_false\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", domain: \"domain.com\").match(response).should be_false\n  end\n\n  def test_match_invalid_diff_domain_and_path\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\", path: \"/path\", domain: \"example.com\"\n\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\").match(response).should be_false\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/bar\", domain: \"example.com\").match(response).should be_false\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/path\", domain: \"domain.com\").match(response).should be_false\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/bar\", domain: \"domain.com\").match(response).should be_false\n  end\n\n  def test_failure_message : Nil\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has cookie 'foo'.\"\n  end\n\n  def test_failure_message_with_path : Nil\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/path\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has cookie 'foo' with path '/path'.\"\n  end\n\n  def test_failure_message_with_domain : Nil\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", domain: \"example.com\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has cookie 'foo' for domain 'example.com'.\"\n  end\n\n  def test_failure_message_with_path_and_domain : Nil\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/path\", domain: \"example.com\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has cookie 'foo' with path '/path' for domain 'example.com'.\"\n  end\n\n  def test_negative_failure_message : Nil\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have cookie 'foo'.\"\n  end\n\n  def test_negative_failure_message_with_path : Nil\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/path\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have cookie 'foo' with path '/path'.\"\n  end\n\n  def test_negative_failure_message_with_domain : Nil\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", domain: \"example.com\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have cookie 'foo' for domain 'example.com'.\"\n  end\n\n  def test_negative_failure_message_with_path_and_domain : Nil\n    ATH::Spec::Expectations::Response::HasCookie.new(\"foo\", path: \"/path\", domain: \"example.com\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have cookie 'foo' with path '/path' for domain 'example.com'.\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/response/has_header_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct HasHeaderExpectationTest < ASPEC::TestCase\n  def test_match_valid : Nil\n    ATH::Spec::Expectations::Response::HasHeader.new(\"date\").match(new_response headers: ::HTTP::Headers{\"date\" => \"now\"}).should be_true\n  end\n\n  def test_match_invalid : Nil\n    ATH::Spec::Expectations::Response::HasHeader.new(\"foobar\").match(new_response).should be_false\n  end\n\n  def test_failure_message : Nil\n    ATH::Spec::Expectations::Response::HasHeader.new(\"date\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has header 'date'.\"\n  end\n\n  def test_failure_message_with_description : Nil\n    ATH::Spec::Expectations::Response::HasHeader.new(\"date\", description: \"Oh noes\")\n      .failure_message(new_response)\n      .should contain \"Oh noes\\n\\nFailed asserting that the response has header 'date'.\"\n  end\n\n  def test_negative_failure_message : Nil\n    ATH::Spec::Expectations::Response::HasHeader.new(\"date\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have header 'date'.\"\n  end\n\n  def test_negative_failure_message_with_description : Nil\n    ATH::Spec::Expectations::Response::HasHeader.new(\"date\", description: \"Oh noes\")\n      .negative_failure_message(new_response)\n      .should contain \"Oh noes\\n\\nFailed asserting that the response does not have header 'date'.\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/response/has_status_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct HasStatusExpectationTest < ASPEC::TestCase\n  def test_match_valid : Nil\n    ATH::Spec::Expectations::Response::HasStatus.new(:ok).match(new_response).should be_true\n    ATH::Spec::Expectations::Response::HasStatus.new(200).match(new_response).should be_true\n  end\n\n  def test_match_invalid : Nil\n    ATH::Spec::Expectations::Response::HasStatus.new(:ok).match(new_response status: :not_found).should be_false\n  end\n\n  def test_failure_message : Nil\n    ATH::Spec::Expectations::Response::HasStatus.new(:not_found)\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response status is 'NOT_FOUND':\\nHTTP/1.1 200 OK\"\n  end\n\n  def test_negative_failure_message : Nil\n    ATH::Spec::Expectations::Response::HasStatus.new(:ok)\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response status is not 'OK':\\nHTTP/1.1 200 OK\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/response/header_equals_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct HeaderEqualsExpectationTest < ASPEC::TestCase\n  def test_match_valid : Nil\n    ATH::Spec::Expectations::Response::HeaderEquals.new(\"date\", \"now\").match(new_response headers: ::HTTP::Headers{\"date\" => \"now\"}).should be_true\n  end\n\n  def test_match_invalid : Nil\n    ATH::Spec::Expectations::Response::HeaderEquals.new(\"foobar\", \"bizbaz\").match(new_response).should be_false\n    ATH::Spec::Expectations::Response::HeaderEquals.new(\"date\", \"now\").match(new_response headers: ::HTTP::Headers{\"date\" => \"yesterdar\"}).should be_false\n  end\n\n  def test_failure_message : Nil\n    ATH::Spec::Expectations::Response::HeaderEquals.new(\"date\", \"now\")\n      .failure_message(new_response)\n      .should contain \"Failed asserting that the response has header 'date' with value 'now'.\"\n  end\n\n  def test_negative_failure_message : Nil\n    ATH::Spec::Expectations::Response::HeaderEquals.new(\"date\", \"now\")\n      .negative_failure_message(new_response)\n      .should contain \"Failed asserting that the response does not have header 'date' with value 'now'.\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/response/is_redirected_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct IsRedirectedExpectationTest < ASPEC::TestCase\n  def initialize\n    @target = ATH::Spec::Expectations::Response::IsRedirected.new\n  end\n\n  def test_match_valid : Nil\n    @target.match(new_response status: :moved_permanently).should be_true\n  end\n\n  def test_match_invalid : Nil\n    @target.match(new_response status: :im_a_teapot).should be_false\n  end\n\n  def test_failure_message : Nil\n    @target.failure_message(new_response status: :not_found).should contain \"Failed asserting that the response is redirected:\\nHTTP/1.1 404 Not Found\"\n  end\n\n  def test_negative_failure_message : Nil\n    @target.negative_failure_message(new_response status: :moved_permanently).should contain \"Failed asserting that the response is not redirected:\\nHTTP/1.1 301 Moved Permanently\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/response/is_successful_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct IsSuccessfulExpectationTest < ASPEC::TestCase\n  def initialize\n    @target = ATH::Spec::Expectations::Response::IsSuccessful.new\n  end\n\n  def test_match_valid : Nil\n    @target.match(new_response).should be_true\n  end\n\n  def test_match_invalid : Nil\n    @target.match(new_response status: :im_a_teapot).should be_false\n  end\n\n  def test_failure_message : Nil\n    @target.failure_message(new_response status: :not_found).should contain \"Failed asserting that the response is successful:\\nHTTP/1.1 404 Not Found\"\n  end\n\n  def test_negative_failure_message : Nil\n    @target.negative_failure_message(new_response).should contain \"Failed asserting that the response is not successful:\\nHTTP/1.1 200 OK\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/expectations/response/is_unprocessable_spec.cr",
    "content": "require \"../../../spec_helper\"\n\nstruct IsUnprocessableExpectationTest < ASPEC::TestCase\n  def initialize\n    @target = ATH::Spec::Expectations::Response::IsUnprocessable.new\n  end\n\n  def test_match_valid : Nil\n    @target.match(new_response status: :unprocessable_entity).should be_true\n  end\n\n  def test_match_invalid : Nil\n    @target.match(new_response).should be_false\n  end\n\n  def test_failure_message : Nil\n    @target.failure_message(new_response status: :not_found).should contain \"Failed asserting that the response is unprocessable:\\nHTTP/1.1 404 Not Found\"\n  end\n\n  def test_negative_failure_message : Nil\n    @target.negative_failure_message(new_response status: :unprocessable_entity).should contain \"Failed asserting that the response is not unprocessable:\\nHTTP/1.1 422 Unprocessable Entity\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec/web_test_case_spec.cr",
    "content": "require \"../spec_helper\"\n\n@[ASPEC::TestCase::Skip]\nprivate struct MockWebTestCase < ATH::Spec::WebTestCase\n  def client=(@client : ATH::Spec::AbstractBrowser); end\nend\n\nprivate class MockClient < ATH::Spec::AbstractBrowser\n  setter request : AHTTP::Request?\n  setter response : ::HTTP::Server::Response?\n\n  def do_request(request : AHTTP::Request) : NoReturn\n    raise NotImplementedError.new \"BUG: Invoked do_request method of MockClient\"\n  end\nend\n\nstruct WebTestCaseTest < ASPEC::TestCase\n  protected def before_all : Nil\n    AHTTP::Request.register_format \"custom\", {\"application/vnd.myformat\"}\n  end\n\n  def test_assert_response_is_successful : Nil\n    self.response_tester(new_response).assert_response_is_successful\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response is successful:\\nHTTP/1.1 404 Not Found\" do\n      self.response_tester(new_response status: :not_found).assert_response_is_successful\n    end\n  end\n\n  def test_assert_response_has_status : Nil\n    self.response_tester(new_response).assert_response_has_status :ok\n    self.response_tester(new_response status: :not_found).assert_response_has_status :not_found\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response status is 'OK':\\nHTTP/1.1 404 Not Found\" do\n      self.response_tester(new_response status: :not_found).assert_response_has_status :ok\n    end\n  end\n\n  def test_assert_response_is_redirected : Nil\n    self.response_tester(new_response status: :moved_permanently).assert_response_redirects\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response is redirected:\\nHTTP/1.1 200 OK\" do\n      self.response_tester(new_response).assert_response_redirects\n    end\n  end\n\n  def test_assert_response_is_redirected_with_location : Nil\n    self.response_tester(new_response status: :moved_permanently, headers: ::HTTP::Headers{\"location\" => \"https://example.com\"}).assert_response_redirects \"https://example.com\"\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response has header 'location' with value 'https://example.com'.\" do\n      self.response_tester(new_response status: :moved_permanently).assert_response_redirects \"https://example.com\"\n    end\n  end\n\n  def test_assert_response_is_redirected_with_status : Nil\n    self.response_tester(new_response status: :moved_permanently).assert_response_redirects status: :moved_permanently\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response status is 'FOUND':\\nHTTP/1.1 301 Moved Permanently\" do\n      self.response_tester(new_response status: :moved_permanently).assert_response_redirects status: 302\n    end\n  end\n\n  def test_assert_response_format_equals : Nil\n    self.response_tester(new_response headers: ::HTTP::Headers{\"content-type\" => \"application/vnd.myformat\"}).assert_response_format_equals \"custom\"\n    self.response_tester(new_response headers: ::HTTP::Headers{\"content-type\" => \"application/json\"}).assert_response_format_equals \"json\"\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response format is 'json':\\nHTTP/1.1 200 OK\" do\n      self.response_tester(new_response headers: ::HTTP::Headers{\"content-type\" => \"text/html\"}).assert_response_format_equals \"json\"\n    end\n  end\n\n  def test_assert_response_has_header : Nil\n    self.response_tester(new_response headers: ::HTTP::Headers{\"foo\" => \"bar\"}).assert_response_has_header \"foo\"\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response has header 'baz'.\" do\n      self.response_tester(new_response).assert_response_has_header \"baz\"\n    end\n  end\n\n  def test_assert_response_not_has_header : Nil\n    self.response_tester(new_response).assert_response_not_has_header \"baz\"\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response does not have header 'foo'.\" do\n      self.response_tester(new_response headers: ::HTTP::Headers{\"foo\" => \"bar\"}).assert_response_not_has_header \"foo\"\n    end\n  end\n\n  def test_assert_response_header_equals : Nil\n    self.response_tester(new_response headers: ::HTTP::Headers{\"foo\" => \"bar\"}).assert_response_header_equals \"foo\", \"bar\"\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response has header 'foo' with value 'bar'\" do\n      self.response_tester(new_response).assert_response_header_equals \"foo\", \"bar\"\n    end\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response has header 'baz' with value 'blah'.\" do\n      self.response_tester(new_response headers: ::HTTP::Headers{\"baz\" => \"bar\"}).assert_response_header_equals \"baz\", \"blah\"\n    end\n  end\n\n  def test_assert_response_not_header_equals : Nil\n    self.response_tester(new_response headers: ::HTTP::Headers{\"foo\" => \"baz\"}).assert_response_header_not_equals \"foo\", \"bar\"\n\n    expect_raises Spec::AssertionFailed, \"ailed asserting that the response does not have header 'foo' with value 'bar'.\" do\n      self.response_tester(new_response headers: ::HTTP::Headers{\"foo\" => \"bar\"}).assert_response_header_not_equals \"foo\", \"bar\"\n    end\n  end\n\n  def test_assert_response_has_cookie : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\"\n\n    self.response_tester(response).assert_response_has_cookie \"foo\"\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response has cookie 'foo'.\" do\n      self.response_tester(new_response).assert_response_has_cookie \"foo\"\n    end\n  end\n\n  def test_assert_response_not_has_cookie : Nil\n    self.response_tester(new_response).assert_response_not_has_cookie \"foo\"\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response does not have cookie 'foo'.\" do\n      response = new_response\n      response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\"\n      self.response_tester(response).assert_response_not_has_cookie \"foo\"\n    end\n  end\n\n  def test_assert_cookie_has_value : Nil\n    response = new_response\n    response.cookies << ::HTTP::Cookie.new \"foo\", \"bar\"\n\n    self.response_tester(response).assert_cookie_has_value \"foo\", \"bar\"\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response has cookie 'foo'.\" do\n      self.response_tester(new_response).assert_cookie_has_value \"foo\", \"bar\"\n    end\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the response has cookie 'foo' with value 'bar'.\" do\n      response = new_response\n      response.cookies << ::HTTP::Cookie.new \"foo\", \"baz\"\n\n      self.response_tester(response).assert_cookie_has_value \"foo\", \"bar\"\n    end\n  end\n\n  def test_assert_request_attribute_equals : Nil\n    self.request_tester.assert_request_attribute_equals \"foo\", \"bar\"\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the request has attribute 'foo' with value 'baz'.\" do\n      self.request_tester.assert_request_attribute_equals \"foo\", \"baz\"\n    end\n  end\n\n  def test_assert_route_equals : Nil\n    self.request_tester.assert_route_equals \"index\", {\"foo\" => \"bar\"}\n\n    expect_raises Spec::AssertionFailed, \"Failed asserting that the request has attribute '_route' with value 'articles'.\" do\n      self.request_tester.assert_route_equals \"articles\"\n    end\n  end\n\n  def test_exception_on_server_error : Nil\n    response = new_response(\n      status: :internal_server_error,\n      headers: ::HTTP::Headers{\n        \"x-debug-exception-code\"    => \"500\",\n        \"x-debug-exception-file\"    => \"/path/to/file:123:4\",\n        \"x-debug-exception-class\"   => \"MyException\",\n        \"x-debug-exception-message\" => \"Oh noes!\",\n      }\n    )\n\n    expect_raises Spec::AssertionFailed, \"Caused By:\\n  Oh noes! (MyException)\\n    from /path/to/file:123:4\" do\n      self.response_tester(response).assert_response_is_successful\n    end\n  end\n\n  private def response_tester(response : ::HTTP::Server::Response) : ATH::Spec::WebTestCase\n    client = MockClient.new\n    client.response = response\n\n    client.request = AHTTP::Request.new \"GET\", \"/\"\n\n    self.tester client\n  end\n\n  private def request_tester : ATH::Spec::WebTestCase\n    client = MockClient.new\n\n    request = AHTTP::Request.new \"GET\", \"/\"\n    request.attributes.set \"foo\", \"bar\", String\n    request.attributes.set \"_route\", \"index\", String\n    client.request = request\n\n    self.tester client\n  end\n\n  private def tester(client : ATH::Spec::AbstractBrowser) : ATH::Spec::WebTestCase\n    obj = MockWebTestCase.new\n    obj.client = client\n    obj\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"log/spec\"\n\nrequire \"../src/athena\"\nrequire \"./controllers/*\"\n\nrequire \"../src/spec\"\n\nSpec.before_each do\n  ART.compile ATH::Routing::AnnotationRouteLoader.route_collection\nend\n\n# FIXME: Refactor these specs to not depend on calling a protected method.\ninclude Athena::Routing\n\nSpec.after_each do\n  ART::RouteProvider.reset\nend\n\nASPEC.run_all\n\n# TODO: Is there a better way to handle customizing the scheme of a request w/o monkey patching it?\nclass AHTTP::Request\n  property scheme : String = \"http\"\nend\n\nclass TestController < ATH::Controller\n  get \"test\" do\n    \"TEST\"\n  end\nend\n\nclass MockSerializer\n  include ASR::SerializerInterface\n\n  setter data : String? = \"SERIALIZED_DATA\"\n  setter context_assertion : Proc(ASR::SerializationContext, Nil)?\n\n  def initialize(@context_assertion : Proc(ASR::SerializationContext, Nil)? = nil); end\n\n  def serialize(data : _, format : ASR::Format | String, context : ASR::SerializationContext = ASR::SerializationContext.new, **named_args) : String\n    String.build do |str|\n      serialize data, format, str, context, **named_args\n    end\n  end\n\n  def serialize(data : _, format : ASR::Format | String, io : IO, context : ASR::SerializationContext = ASR::SerializationContext.new, **named_args) : Nil\n    @data.to_json io\n\n    @context_assertion.try &.call context\n  end\n\n  def deserialize(type : ASR::Model.class, data : String | IO, format : ASR::Format | String, context : ASR::DeserializationContext = ASR::DeserializationContext.new)\n  end\nend\n\nclass DeserializableMockSerializer(T) < MockSerializer\n  setter deserialized_response : T? = nil\n\n  def deserialize(type : ASR::Model.class, data : String | IO, format : ASR::Format | String, context : ASR::DeserializationContext = ASR::DeserializationContext.new)\n    @deserialized_response\n  end\nend\n\nclass MockAnnotationResolver < ATH::AnnotationResolver\n  property action_annotations : ADI::AnnotationConfigurations\n  property action_parameter_annotations : ADI::AnnotationConfigurations\n\n  def initialize(\n    @action_annotations : ADI::AnnotationConfigurations = ADI::AnnotationConfigurations.new,\n    @action_parameter_annotations : ADI::AnnotationConfigurations = ADI::AnnotationConfigurations.new,\n    *,\n    @expected_controller : String? = nil,\n    @expected_parameter_name : String? = nil,\n  ); end\n\n  def action_annotations(request : AHTTP::Request) : ADI::AnnotationConfigurations\n    if expected_controller = @expected_controller\n      request.attributes.get?(\"_controller\", String).should eq expected_controller\n    end\n\n    @action_annotations\n  end\n\n  def action_parameter_annotations(request : AHTTP::Request, parameter_name : String) : ADI::AnnotationConfigurations\n    if expected_controller = @expected_controller\n      request.attributes.get?(\"_controller\", String).should eq expected_controller\n    end\n\n    if expected_parameter_name = @expected_parameter_name\n      parameter_name.should eq expected_parameter_name\n    end\n\n    @action_parameter_annotations\n  end\nend\n\nmacro create_action(return_type = String, &)\n  AHK::Action.new(\n    Proc(typeof(Tuple.new), {{return_type}}).new { {{yield}} },\n    Tuple.new,\n    {{return_type}},\n  )\nend\n\ndef new_parameter : AHK::Controller::ParameterMetadata\n  AHK::Controller::ParameterMetadata(Int32).new \"id\"\nend\n\ndef new_action(\n  *,\n  arguments : Tuple = Tuple.new,\n) : AHK::ActionBase\n  AHK::Action.new(\n    Proc(typeof(Tuple.new), String).new { test_controller = TestController.new; test_controller.get_test },\n    arguments,\n    String,\n  )\nend\n\ndef new_request(\n  *,\n  path : String = \"/test\",\n  method : String = \"GET\",\n  action : AHK::ActionBase = new_action,\n  body : String | IO | Nil = nil,\n  query : String? = nil,\n  format : String = \"json\",\n  files : Hash(String, Array(AHTTP::UploadedFile)) = {} of String => Array(AHTTP::UploadedFile),\n  headers : ::HTTP::Headers = ::HTTP::Headers.new,\n) : AHTTP::Request\n  request = AHTTP::Request.new method, path, body: body\n  request.files.merge! files\n  request.attributes.set \"_controller\", \"TestController#test\", String\n  request.attributes.set \"_route\", \"test_controller_test\", String\n  request.attributes.set \"_action\", action\n  request.query = query\n  request.headers = ::HTTP::Headers{\n    \"content-type\" => AHTTP::Request::FORMATS[format].first,\n  }.merge! headers\n  request\nend\n\ndef new_request_event(headers : ::HTTP::Headers = ::HTTP::Headers.new)\n  new_request_event(headers) { }\nend\n\ndef new_request_event(headers : ::HTTP::Headers = ::HTTP::Headers.new, & : AHTTP::Request -> _)\n  request = new_request headers: headers\n  yield request\n  AHK::Events::Request.new request\nend\n\ndef new_response(\n  *,\n  io : IO = IO::Memory.new,\n  status : ::HTTP::Status = :ok,\n  headers : ::HTTP::Headers = ::HTTP::Headers.new,\n) : ::HTTP::Server::Response\n  ::HTTP::Server::Response.new(io).tap do |resp|\n    headers.each do |k, v|\n      resp.headers[k] = v\n    end\n\n    resp.status = status\n  end\nend\n\nATH.configure({\n  framework: {\n    file_uploads: {\n      enabled: true,\n    },\n  },\n})\n"
  },
  {
    "path": "src/components/framework/spec/view/context_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate struct IgnoreExclusionStrategy\n  include ASR::ExclusionStrategies::ExclusionStrategyInterface\n\n  # :inherit:\n  def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool\n    false\n  end\nend\n\nstruct ContextTest < ASPEC::TestCase\n  @context : ATH::View::Context\n\n  def initialize\n    @context = ATH::View::Context.new\n  end\n\n  def test_default_values : Nil\n    @context.version.should be_nil\n    @context.groups.should be_nil\n    @context.emit_nil?.should be_nil\n  end\n\n  def test_adding_groups : Nil\n    @context.add_groups \"one\", \"two\"\n    @context.add_groups({\"three\"})\n    @context.add_group \"four\"\n\n    @context.groups.should eq Set{\"one\", \"two\", \"three\", \"four\"}\n  end\n\n  def test_set_groups : Nil\n    @context.add_groups \"foo\", \"bar\"\n\n    @context.groups.should eq Set{\"foo\", \"bar\"}\n\n    @context.groups = {\"one\", \"two\"}\n\n    @context.groups.should eq Set{\"one\", \"two\"}\n  end\n\n  def test_does_not_allow_duplicate_groups : Nil\n    @context.add_group \"one\"\n    @context.add_group \"one\"\n    @context.add_group \"two\"\n\n    @context.groups.should eq Set{\"one\", \"two\"}\n  end\n\n  def test_version : Nil\n    @context.version = \"1.2.3\"\n\n    @context.version.should eq SemanticVersion.new 1, 2, 3\n\n    sem_ver = SemanticVersion.new 10, 9, 8\n\n    @context.version = sem_ver\n\n    @context.version.should eq sem_ver\n  end\n\n  def test_exclusion_strategies : Nil\n    @context.exclusion_strategies.should be_empty\n\n    strategy = IgnoreExclusionStrategy.new\n\n    @context.add_exclusion_strategy strategy\n\n    @context.exclusion_strategies.should eq [strategy]\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/view/format_negotiator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class MockRequestMatcher\n  include AHTTP::RequestMatcher::Interface\n\n  def initialize(@matches : Bool); end\n\n  def matches?(request : AHTTP::Request) : Bool\n    @matches\n  end\nend\n\nstruct FormatNegotiatorTest < ASPEC::TestCase\n  @request_store : AHTTP::RequestStore\n  @request : AHTTP::Request\n  @negotiator : ATH::View::FormatNegotiator\n\n  def initialize\n    @request_store = AHTTP::RequestStore.new\n    @request = AHTTP::Request.new \"GET\", \"/\"\n    @request_store.request = @request\n\n    @negotiator = ATH::View::FormatNegotiator.new(\n      @request_store,\n      {\"json\" => [\"application/json;version=1.0\"]}\n    )\n  end\n\n  def test_best_no_config : Nil\n    @negotiator.best(\"\").should be_nil\n  end\n\n  def test_best_stop_exception : Nil\n    self.add_rule false\n    self.add_rule stop: true\n\n    expect_raises AHK::Exception::StopFormatListener, \"Stopping format listener.\" do\n      @negotiator.best \"\"\n    end\n  end\n\n  def test_fallback_format : Nil\n    self.add_rule\n    @negotiator.best(\"\").should be_nil\n\n    self.add_rule fallback_format: \"html\"\n    @negotiator.best(\"\").should eq ANG::Accept.new \"text/html\"\n  end\n\n  def test_fallback_format_priorities : Nil\n    self.add_rule priorities: [\"json\", \"xml\"], fallback_format: nil\n    @negotiator.best(\"\").should be_nil\n\n    self.add_rule priorities: [\"json\", \"xml\"], fallback_format: \"json\"\n    @negotiator.best(\"\").should eq ANG::Accept.new \"application/json\"\n  end\n\n  def test_best : Nil\n    @request.headers[\"accept\"] = \"application/xhtml+xml, text/html, application/xml;q=0.9, */*;q=0.8\"\n    priorities = [\"text/html; charset=utf-8\", \"html\", \"application/json\"]\n    self.add_rule priorities: priorities\n\n    @negotiator.best(\"\").should eq ANG::Accept.new \"text/html;charset=utf-8\"\n\n    @request.headers[\"accept\"] = \"application/xhtml+xml, application/xml;q=0.9, */*;q=0.8\"\n    @negotiator.best(\"\", {\"html\", \"json\"}).should eq ANG::Accept.new \"application/xhtml+xml\"\n  end\n\n  def test_best_fallback : Nil\n    @request.headers[\"accept\"] = \"text/html\"\n    self.add_rule priorities: [\"application/json\"], fallback_format: \"xml\"\n    @negotiator.best(\"\").should eq ANG::Accept.new \"text/xml\"\n  end\n\n  def test_best_format_from_mime_types_hash : Nil\n    @request.headers[\"accept\"] = \"application/json;version=1.0\"\n    self.add_rule priorities: [\"json\"], fallback_format: \"xml\"\n    @negotiator.best(\"\").should eq ANG::Accept.new \"application/json;version=1.0\"\n  end\n\n  def test_best_format : Nil\n    @request.headers[\"accept\"] = \"application/json\"\n    self.add_rule priorities: [\"json\"], fallback_format: \"xml\"\n    @negotiator.best(\"\").should eq ANG::Accept.new \"application/json\"\n  end\n\n  def test_best_with_prefer_extension : Nil\n    priorities = [\"text/html\", \"application/json\"]\n    self.add_rule priorities: priorities, prefer_extension: true\n\n    @request.path = \"/file.json\"\n\n    # Without extension mime-type in accept header\n\n    @request.headers[\"accept\"] = \"text/html; q=1.0\"\n    @negotiator.best(\"\").should eq ANG::Accept.new \"application/json\"\n\n    # With low q extension mime-type in accept header\n\n    @request.headers[\"accept\"] = \"text/html; q=1.0, application/json; q=0.1\"\n    @negotiator.best(\"\").should eq ANG::Accept.new \"application/json\"\n  end\n\n  def test_best_with_prefer_extension_and_unknown_extension : Nil\n    priorities = [\"text/html\", \"application/json\"]\n    self.add_rule priorities: priorities, prefer_extension: true\n\n    @request.path = \"/file.123456789\"\n\n    # Without extension mime-type in accept header\n\n    @request.headers[\"accept\"] = \"text/html, application/json\"\n    @negotiator.best(\"\").should eq ANG::Accept.new \"text/html\"\n  end\n\n  private def add_rule(match : Bool = true, **args)\n    rule = ATH::View::FormatNegotiator::Rule.new **args\n    matcher = MockRequestMatcher.new match\n\n    @negotiator.add matcher, rule\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/view/view_handler_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class MockURLGenerator\n  include Athena::Routing::Generator::Interface\n\n  property context : ART::RequestContext = ART::RequestContext.new\n\n  setter expected_route, expected_reference_type, generated_url\n\n  def initialize(\n    @expected_route : String = \"some_route\",\n    @expected_reference_type : ART::Generator::ReferenceType = :absolute_path,\n    @generated_url : String = \"URL\",\n  ); end\n\n  def generate(route : String, params : Hash = Hash(String, String | ::Nil).new, reference_type : ART::Generator::ReferenceType = :absolute_path) : String\n    route.should eq @expected_route\n    reference_type.should eq @expected_reference_type\n\n    params.try &.each do |key, value|\n      @generated_url = @generated_url.gsub \"{{#{key}}}\", value\n    end\n\n    @generated_url\n  end\n\n  # :ditto:\n  def generate(route : String, reference_type : ART::Generator::ReferenceType = :absolute_path, **params) : String\n    self.generate route, params.to_h.transform_keys(&.to_s), reference_type\n  end\nend\n\nstruct ViewHandlerTest < ASPEC::TestCase\n  @url_generator : MockURLGenerator\n  @serializer : MockSerializer\n  @request_store : AHTTP::RequestStore\n\n  def initialize\n    @url_generator = MockURLGenerator.new\n    @serializer = MockSerializer.new\n    @request_store = AHTTP::RequestStore.new\n  end\n\n  @[DataProvider(\"format_provider\")]\n  def test_supports_format(expected : Bool, custom_format_name : String, format : String?) : Nil\n    view_handler = self.create_view_handler\n    view_handler.register_handler custom_format_name do\n      AHTTP::Response.new\n    end\n\n    view_handler.supports?(format || \"html\").should eq expected\n  end\n\n  def format_provider : Tuple\n    {\n      {false, \"xml\", nil},\n      {true, \"html\", nil},\n      {true, \"html\", \"json\"},\n    }\n  end\n\n  @[DataProvider(\"status_provider\")]\n  def test_status(expected_status : ::HTTP::Status, view_status : ::HTTP::Status?, data : String?, empty_content_status : ::HTTP::Status?) : Nil\n    view = if data\n             ATH::View(String).new data: data, status: view_status\n           else\n             ATH::View(Nil).new status: view_status\n           end\n\n    view_handler = if empty_content_status\n                     self.create_view_handler empty_content_status: empty_content_status\n                   else\n                     self.create_view_handler\n                   end\n\n    view_handler.create_response(view, AHTTP::Request.new(\"GET\", \"/\"), \"json\").status.should eq expected_status\n  end\n\n  def status_provider : Hash\n    {\n      \"custom view status\"                 => {::HTTP::Status::IM_A_TEAPOT, ::HTTP::Status::IM_A_TEAPOT, nil, nil},\n      \"non empty content\"                  => {::HTTP::Status::OK, nil, \"DATA\", nil},\n      \"empty content default empty status\" => {::HTTP::Status::NO_CONTENT, nil, nil, nil},\n      \"empty content custom empty status\"  => {::HTTP::Status::IM_A_TEAPOT, nil, nil, ::HTTP::Status::IM_A_TEAPOT},\n    }\n  end\n\n  def test_create_response_with_location : Nil\n    view_handler = self.create_view_handler empty_content_status: ::HTTP::Status::USE_PROXY\n\n    view = ATH::View(String?).new nil\n    view.location = \"location\"\n\n    response = view_handler.create_response view, AHTTP::Request.new(\"GET\", \"/\"), \"json\"\n\n    response.status.should eq ::HTTP::Status::USE_PROXY\n    response.headers[\"location\"].should eq \"location\"\n  end\n\n  def test_create_response_with_location_and_data : Nil\n    view_handler = self.create_view_handler\n\n    view = ATH::View(String).new \"DATA\", status: ::HTTP::Status::CREATED\n    view.location = \"location\"\n\n    response = view_handler.create_response view, AHTTP::Request.new(\"GET\", \"/\"), \"json\"\n\n    response.status.should eq ::HTTP::Status::CREATED\n    response.headers[\"location\"].should eq \"location\"\n    response.content.should eq %(\"SERIALIZED_DATA\")\n  end\n\n  def test_create_response_with_route : Nil\n    view_handler = self.create_view_handler\n\n    @url_generator.generated_url = \"/foo/{{foo}}\"\n    @url_generator.expected_reference_type = ART::Generator::ReferenceType::ABSOLUTE_URL\n\n    view = ATH::View(String).new \"DATA\", status: ::HTTP::Status::CREATED\n    view.route = \"some_route\"\n    view.route_params = {\"foo\" => \"bar\"} of String => String?\n\n    response = view_handler.create_response view, AHTTP::Request.new(\"GET\", \"/\"), \"json\"\n\n    response.status.should eq ::HTTP::Status::CREATED\n    response.headers[\"location\"].should eq \"/foo/bar\"\n  end\n\n  def test_create_response_without_location : Nil\n    view_handler = self.create_view_handler\n\n    view = ATH::View.new \"DATA\"\n\n    response = view_handler.create_response view, AHTTP::Request.new(\"GET\", \"/\"), \"json\"\n\n    response.status.should eq ::HTTP::Status::OK\n    response.content.should eq %(\"SERIALIZED_DATA\")\n  end\n\n  @[DataProvider(\"serialize_nil_provider\")]\n  def test_serialize_nil_view_handler(emit_nil : Bool) : Nil\n    view_handler = self.create_view_handler emit_nil: emit_nil\n\n    @serializer.context_assertion = ->(context : ASR::SerializationContext) do\n      context.emit_nil?.should eq emit_nil\n    end\n\n    view_handler.create_response ATH::View(Nil).new, AHTTP::Request.new(\"GET\", \"/\"), \"json\"\n  end\n\n  def serialize_nil_provider : Tuple\n    {\n      {true},\n      {false},\n    }\n  end\n\n  def test_handle_unsupported_format : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    request.request_format = \"rss\"\n\n    expect_raises AHK::Exception::NotAcceptable, \"The server is unable to return a response in the requested format: 'rss'.\" do\n      self.create_view_handler.handle ATH::View(Nil).new, request\n    end\n  end\n\n  def test_handle_custom_handler : Nil\n    response = AHTTP::Response.new\n\n    view_handler = self.create_view_handler\n    view_handler.register_handler \"rss\" do\n      response\n    end\n\n    request = AHTTP::Request.new \"GET\", \"/\"\n    request.request_format = \"rss\"\n\n    view_handler.handle(ATH::View(Nil).new, request).should be response\n  end\n\n  def test_configurable_values : Nil\n    view_handler = self.create_view_handler\n    view_handler.serialization_groups = {\"one\", \"two\"}\n    view_handler.serialization_version = \"1.2.3\"\n    view_handler.serialization_version = SemanticVersion.new 4, 5, 6\n    view_handler.emit_nil = true\n\n    @serializer.context_assertion = ->(context : ASR::SerializationContext) do\n      context.emit_nil?.should be_true\n      context.version.should eq SemanticVersion.new 4, 5, 6\n      context.groups.should eq Set{\"one\", \"two\"}\n    end\n\n    view_handler.create_response ATH::View(Nil).new, AHTTP::Request.new(\"GET\", \"/\"), \"json\"\n  end\n\n  private def create_view_handler(**args) : ATH::View::ViewHandler\n    ATH::View::ViewHandler.new(\n      @url_generator,\n      @serializer,\n      @request_store,\n      ([] of Athena::Framework::View::FormatHandlerInterface),\n      **args\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/view/view_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct ViewTest < ASPEC::TestCase\n  def test_location : Nil\n    url = \"users\"\n    status = ::HTTP::Status::OK\n\n    view = ATH::View.create_redirect url, status\n    view.location.should eq url\n    view.route.should be_nil\n    view.response.status.should eq status\n\n    view = ATH::View(Nil).new\n    view.location = \"bar\"\n    view.location.should eq \"bar\"\n    view.route.should be_nil\n  end\n\n  def test_route : Nil\n    route = \"users\"\n    status = ::HTTP::Status::OK\n\n    view = ATH::View.create_route_redirect route, status: status\n    view.location.should be_nil\n    view.route.should eq route\n    view.response.status.should eq status\n\n    view = ATH::View(Nil).new\n    view.route = \"bar\"\n    view.route.should eq \"bar\"\n    view.location.should be_nil\n  end\n\n  @[DataProvider(\"data_provider\")]\n  def test_data(data) : Nil\n    view = ATH::View(Hash(String, String | Int32)?).new\n    view.data = data\n    view.data.should eq data\n  end\n\n  def data_provider : Tuple\n    {\n      {nil},\n      { {\"foo\" => \"bar\", \"baz\" => 10} },\n    }\n  end\n\n  def test_format : Nil\n    view = ATH::View(Nil).new\n    view.format = \"format\"\n    view.format.should eq \"format\"\n  end\n\n  def test_headers : Nil\n    view = ATH::View(Nil).new\n    view.headers = ::HTTP::Headers{\"foo\" => \"bar\"}\n\n    headers = view.response.headers\n    view.headers.has_key?(\"foo\").should be_true\n    headers[\"foo\"].should eq \"bar\"\n\n    view.set_header \"string\", \"str\"\n    view.set_header \"non-string\", 10\n\n    headers[\"string\"].should eq \"str\"\n    headers[\"non-string\"].should eq \"10\"\n  end\n\n  def test_status : Nil\n    view = ATH::View(Nil).new\n    view.status = :not_found\n    view.status.should eq ::HTTP::Status::NOT_FOUND\n    view.response.status.should eq ::HTTP::Status::NOT_FOUND\n  end\n\n  def test_default_status_from_response : Nil\n    view = ATH::View(Nil).new\n    view.status.should be_nil\n    view.response.status.should eq ::HTTP::Status::OK\n  end\nend\n"
  },
  {
    "path": "src/components/framework/spec/view_controller_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct ViewControllerTest < ATH::Spec::APITestCase\n  def test_unserializable_object : Nil\n    self.get \"/view/unserializable\"\n    self.assert_response_has_status :internal_server_error\n  end\n\n  def test_nil : Nil\n    self.get \"/view/nil\"\n    self.assert_response_has_status :no_content\n  end\n\n  def test_json_serializable_object : Nil\n    self.get(\"/view/json\").body.should eq %({\"id\":10,\"name\":\"Bob\"})\n  end\n\n  def test_json_serializable_array : Nil\n    self.get(\"/view/json-array\").body.should eq %([{\"id\":10,\"name\":\"Bob\"},{\"id\":20,\"name\":\"Sally\"}])\n  end\n\n  def test_json_serializable_nested_array : Nil\n    self.get(\"/view/json-array-nested\").body.should eq %([[{\"id\":10,\"name\":\"Bob\"}]])\n  end\n\n  def test_json_serializable_empty_array : Nil\n    self.get(\"/view/json-array-empty\").body.should eq %([])\n  end\n\n  def test_json_nested_hash_collection : Nil\n    self.get(\"/view/json-nested-hash-collection\").body.should eq %({\"foo\":10,\"obj\":{\"id\":10,\"name\":\"Bob\"}})\n  end\n\n  def test_json_nested_nt_collection : Nil\n    self.get(\"/view/json-nested-nt-collection\").body.should eq %({\"foo\":10,\"obj\":{\"id\":10,\"name\":\"Bob\"}})\n  end\n\n  def test_json_nested_hash_array_collection : Nil\n    self.get(\"/view/json-nested-hash-array-collection\").body.should eq %({\"foo\":10,\"objs\":[{\"id\":10,\"name\":\"Bob\"}]})\n  end\n\n  def test_json_nested_nt_array_collection : Nil\n    self.get(\"/view/json-nested-nt-array-collection\").body.should eq %({\"foo\":10,\"objs\":[{\"id\":10,\"name\":\"Bob\"}]})\n  end\n\n  def test_asr_serializable_object : Nil\n    self.get(\"/view/asr\").body.should eq %({\"id\":20})\n  end\n\n  def test_asr_serializable_array : Nil\n    self.get(\"/view/asr-array\").body.should eq %([{\"id\":10},{\"id\":20}])\n  end\n\n  def test_custom_status : Nil\n    self.post(\"/view/status\").status.accepted?.should be_true\n  end\n\n  def test_view : Nil\n    response = self.get(\"/view\")\n    response.body.should eq %(\"DATA\")\n    response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n  end\n\n  def test_view_json_serializable_array : Nil\n    response = self.get(\"/view/array\")\n    response.body.should eq %([{\"id\":10,\"name\":\"Bob\"},{\"id\":20,\"name\":\"Sally\"}])\n    response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/annotation_resolver.cr",
    "content": "@[ADI::Register]\n# Allows resolving [custom annotations](/getting_started/configuration/#custom-annotations) defined on an `ATH::Controller` class or `ATH::Action` method, or one of its parameters.\nclass Athena::Framework::AnnotationResolver\n  # :nodoc:\n  ACTION_ANNOTATIONS = {} of String => ADI::AnnotationConfigurations\n\n  # :nodoc:\n  ACTION_PARAMETER_ANNOTATIONS = {} of String => Hash(String, ADI::AnnotationConfigurations)\n\n  # Returns an `ADI::AnnotationConfigurations` instance representing the custom annotations applied on an `ATH::Controller` class or `AHK::Action` method.\n  # An empty instance is returned if there are no custom annotations applied.\n  def action_annotations(request : AHTTP::Request) : ADI::AnnotationConfigurations\n    return ADI::AnnotationConfigurations.new unless controller = request.attributes.get? \"_controller\", String\n\n    ACTION_ANNOTATIONS[controller]? || ADI::AnnotationConfigurations.new\n  end\n\n  # Returns an `ADI::AnnotationConfigurations` instance representing the custom annotations applied on an `ATH::Action` method parameter.\n  # An empty instance is returned if there are no custom annotations applied.\n  def action_parameter_annotations(request : AHTTP::Request, parameter_name : String) : ADI::AnnotationConfigurations\n    return ADI::AnnotationConfigurations.new unless controller = request.attributes.get? \"_controller\", String\n\n    ACTION_PARAMETER_ANNOTATIONS[controller]?.try(&.[parameter_name]?) || ADI::AnnotationConfigurations.new\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/annotations.cr",
    "content": "# Contains all the `Athena::Framework` based annotations.\n# See each annotation for more information.\nmodule Athena::Framework::Annotations\n  # Configures how the `ATH::View::ViewHandlerInterface` should render the related controller action.\n  #\n  # ## Fields\n  #\n  # * status : `HTTP::Status` - The `::HTTP::Status` the endpoint should return. Defaults to `HTTP::Status::OK` (200).\n  # * serialization_groups : `Array(String)?` - The serialization groups to use for this route as part of `ASR::ExclusionStrategies::Groups`.\n  # * validation_groups : `Array(String)?` - Groups that should be used to validate any objects related to this route; see `AVD::Constraint@validation-groups`.\n  # * emit_nil : `Bool` - If `nil` values should be serialized. Defaults to `false`.\n  #\n  # ## Example\n  #\n  # ```\n  # @[ARTA::Post(path: \"/publish/{id}\")]\n  # @[ATHA::View(status: :accepted, serialization_groups: [\"default\", \"detailed\"])]\n  # def publish(id : Int32) : Article\n  #   article = Article.find id\n  #   article.published = true\n  #   article\n  # end\n  # ```\n  ADI.configuration_annotation ::Athena::Framework::Annotations::View,\n    status : ::HTTP::Status? = nil,\n    serialization_groups : Array(String)? = nil,\n    validation_groups : Array(String)? = nil,\n    emit_nil : Bool? = nil\nend\n"
  },
  {
    "path": "src/components/framework/src/athena.cr",
    "content": "require \"ecr\"\nrequire \"http/server\"\nrequire \"json\"\n\nrequire \"athena-contracts/event_dispatcher\"\n\nrequire \"athena-clock\"\nrequire \"athena-console\"\nrequire \"athena-dependency_injection\"\nrequire \"athena-http_kernel\"\nrequire \"athena-negotiation\"\n\nrequire \"./annotation_resolver\"\nrequire \"./annotations\"\nrequire \"./bundle\"\nrequire \"./controller\"\nrequire \"./file_parser\"\nrequire \"./logging\"\n\nrequire \"./ext/http\"\nrequire \"./ext/http_kernel\"\nrequire \"./ext/serializer\"\n\nrequire \"./commands/*\"\nrequire \"./controller/**\"\nrequire \"./compiler_passes/*\"\nrequire \"./listeners/*\"\nrequire \"./view/*\"\n\nrequire \"./ext/clock\"\nrequire \"./ext/console\"\nrequire \"./ext/event_dispatcher\"\nrequire \"./ext/routing\"\nrequire \"./ext/validator\"\n\n# Convenience alias to make referencing `Athena::Framework` types easier.\nalias ATH = Athena::Framework\n\n# Convenience alias to make referencing `Athena::Framework::Annotations` types easier.\nalias ATHA = ATH::Annotations\n\n# Convenience alias to make referencing `ATH::Controller::ValueResolvers` types easier.\nalias ATHR = ATH::Controller::ValueResolvers\n\nmodule Athena::Framework\n  VERSION = \"0.22.0\"\n\n  # The name of the environment variable used to determine Athena's current environment.\n  ENV_NAME = \"ATHENA_ENV\"\n\n  # Primary entrypoint for configuring Athena Framework applications.\n  #\n  # See the [Getting Started](/getting_started/configuration) docs for more information.\n  #\n  # NOTE: This is an alias of [ADI.configure](/DependencyInjection/top_level/#Athena::DependencyInjection:configure(config)).\n  macro configure(config)\n    ADI.configure({{config}})\n  end\n\n  # Registers the provided *bundle*.\n  #\n  # See the [Getting Started](/getting_started/configuration) docs for more information.\n  #\n  # NOTE: This is an alias of [ADI.register_bundle](/DependencyInjection/top_level/#Athena::DependencyInjection:register_bundle(bundle)).\n  macro register_bundle(bundle)\n    ADI.register_bundle({{bundle}})\n  end\n\n  # Returns the current environment Athena is in based on `ENV_NAME`.  Defaults to `development` if not defined.\n  def self.environment : String\n    ENV[ENV_NAME]? || \"development\"\n  end\n\n  # This type includes all of the built-in resolvers that Athena uses to try and resolve an argument for a particular controller action parameter.\n  # They run in the following order:\n  #\n  # 1. `ATHR::QueryParameter` (110) - Attempts to resolve a value from the `AHTTP::Request` query parameters.\n  #\n  # 1. `ATHR::Enum` (105) - Attempts to resolve a value from [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes) into an enum member of the related type.\n  # Works well in conjunction with `ART::Requirement::Enum`.\n  #\n  # 1. `ATHR::Time` (105) - Attempts to resolve a value from the request attributes into a `::Time` instance,\n  # defaulting to [RFC 3339](https://crystal-lang.org/api/Time.html#parse_rfc3339%28time:String%29-class-method).\n  # Format/location can be customized via the `ATHA::MapTime` annotation.\n  #\n  # 1. `ATHR::UUID` (105) - Attempts to resolve a value from the request attributes into a `::UUID` instance.\n  #\n  # 1. `ATHR::RequestBody` (105) - If enabled, attempts to deserialize the request body/query string into the type of the related parameter, running any defined validations if applicable.\n  #\n  # 1. `AHK::Controller::ValueResolvers::RequestAttribute` (100) - Provides a value stored in [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes) if one with the same name as the action parameter exists.\n  #\n  # 1. `AHK::Controller::ValueResolvers::Request` (50) - Provides the current `AHTTP::Request` if the related parameter is typed as such.\n  #\n  # 1. `AHK::Controller::ValueResolvers::DefaultValue` (-100) - Provides the default value of the parameter if it has one, or `nil` if it is nilable.\n  #\n  # See each resolver for more detailed information.\n  # Custom resolvers may also be defined.\n  # See `ATHR::Interface` for more information.\n  module Controller::ValueResolvers; end\n\n  # The event listeners that act upon `AHK::Events` to handle a request.\n  # Custom listeners can also be defined, see `AEDA::AsEventListener`.\n  #\n  # See each listener and the [Getting Started](/getting_started/middleware) docs for more information.\n  module Listeners; end\n\n  # :nodoc:\n  module CompilerPasses; end\n\n  # Namespace for the built in `Athena::Console` commands that come bundled with the framework.\n  # Currently it provides:\n  #\n  # - `ATH::Commands::DebugEventDispatcher` - Display configured listeners for an application\n  # - `ATH::Commands::DebugRouter` - Display current routes for an application\n  # - `ATH::Commands::DebugRouterMatch` - Simulate a path match to see which route, if any, would handle it\n  #\n  # See each command class for more information.\n  module Commands; end\n\n  # Runs an `HTTP::Server` listening on the given *port* and *host*.\n  #\n  # ```\n  # require \"athena\"\n  #\n  # class ExampleController < ATH::Controller\n  #   @[ARTA::Get(\"/\")]\n  #   def root : String\n  #     \"At the index\"\n  #   end\n  # end\n  #\n  # ATH.run\n  # ```\n  #\n  # *prepend_handlers* can be used to execute an array of `HTTP::Handler` _before_ Athena takes over.\n  # This can be useful to provide backwards compatibility with existing handlers until they can ported to Athena concepts,\n  # or for supporting things Athena does not support, such as WebSockets.\n  #\n  # See `ATH::Controller` for more information on defining controllers/route actions.\n  def self.run(\n    port : Int32 = 3000,\n    host : String = \"0.0.0.0\",\n    reuse_port : Bool = false,\n    ssl_context : OpenSSL::SSL::Context::Server? = nil,\n    *,\n    prepend_handlers : Array(::HTTP::Handler) = [] of ::HTTP::Handler,\n  ) : Nil\n    ATH::Server.new(port, host, reuse_port, ssl_context, prepend_handlers).start\n  end\n\n  # Runs an `ATH::Console::Application` as the entrypoint of `Athena::Console`.\n  #\n  # Checkout the [Getting Started](/getting_started/commands) docs for more information.\n  def self.run_console : Nil\n    ADI.container.athena_console_application.run\n  end\n\n  # :nodoc:\n  #\n  # Currently an implementation detail. In the future could be exposed to allow having separate \"groups\" of controllers that a `Server` instance handles.\n  struct Server\n    def initialize(\n      @port : Int32 = 3000,\n      @host : String = \"0.0.0.0\",\n      @reuse_port : Bool = false,\n      @ssl_context : OpenSSL::SSL::Context::Server? = nil,\n      prepend_handlers handlers : Array(::HTTP::Handler) = [] of ::HTTP::Handler,\n    )\n      handler_proc = ::HTTP::Handler::HandlerProc.new do |context|\n        # Reinitialize the container since keep-alive requests reuse the same fiber.\n        Fiber.current.container = ADI::ServiceContainer.new\n\n        handler = ADI.container.athena_http_kernel\n\n        # Convert the raw `HTTP::Request` into an `AHTTP::Request` instance.\n        request = AHTTP::Request.new context.request\n\n        # Handle the request.\n        athena_response = handler.handle request\n\n        # Send the response based on the current context.\n        athena_response.send request, context.response\n\n        # Emit the terminate event now that the response has been sent.\n        handler.terminate request, athena_response\n      end\n\n      @server = if handlers.empty?\n                  ::HTTP::Server.new &handler_proc\n                else\n                  ::HTTP::Server.new handlers, &handler_proc\n                end\n    end\n\n    def stop : Nil\n      @server.close unless @server.closed?\n    end\n\n    def start : Nil\n      # TODO: Is there a better place to do this?\n      {% if (trusted_hosts = ADI::CONFIG[\"framework\"][\"trusted_hosts\"]) && !trusted_hosts.empty? %}\n        AHTTP::Request.set_trusted_hosts({{trusted_hosts}})\n      {% end %}\n\n      {% if (trusted_proxies = ADI::CONFIG[\"framework\"][\"trusted_proxies\"]) && (trusted_headers = ADI::CONFIG[\"framework\"][\"trusted_headers\"]) %}\n        AHTTP::Request.set_trusted_proxies({{trusted_proxies}}, {{trusted_headers}})\n      {% end %}\n\n      {% for header, name in ADI::CONFIG[\"framework\"][\"trusted_header_overrides\"] %}\n        AHTTP::Request.override_trusted_header({{header}}, {{name}})\n      {% end %}\n\n      {% if (file_uploads = ADI::CONFIG[\"framework\"][\"file_uploads\"]) && file_uploads[\"enabled\"] %}\n        AHTTP::UploadedFile.max_file_size = {{file_uploads[\"max_file_size\"]}}\n      {% end %}\n\n      {% if flag?(:without_openssl) %}\n        @server.bind_tcp @host, @port, reuse_port: @reuse_port\n      {% else %}\n        if ssl = @ssl_context\n          @server.bind_tls @host, @port, ssl, @reuse_port\n        else\n          @server.bind_tcp @host, @port, reuse_port: @reuse_port\n        end\n      {% end %}\n\n      # Handle exiting correctly on interrupt signals\n      Process.on_terminate { self.stop }\n\n      Log.info { %(Server has started and is listening at #{@ssl_context ? \"https\" : \"http\"}://#{@server.addresses.first}) }\n\n      @server.listen\n    end\n  end\nend\n\nATH.register_bundle ATH::Bundle\n"
  },
  {
    "path": "src/components/framework/src/bundle.cr",
    "content": "@[ADI::Bundle(\"framework\")]\n# The Athena Framework Bundle is responsible for integrating the various Athena components into the Athena Framework.\n# This primarily involves wiring up various types as services, and other DI related tasks.\nstruct Athena::Framework::Bundle < ADI::AbstractBundle\n  # :nodoc:\n  PASSES = [\n    {Athena::Framework::CompilerPasses::MakeControllerServicesPublicPass, nil, nil},\n    {Athena::Framework::Console::CompilerPasses::RegisterCommands, :before_removing, nil},\n    {Athena::Framework::EventDispatcher::CompilerPasses::RegisterEventListenersPass, :before_removing, nil},\n  ]\n\n  # Represents the possible properties used to configure and customize Athena Framework features.\n  # See the [Getting Started](/getting_started/configuration) docs for more information.\n  module Schema\n    include ADI::Extension::Schema\n\n    # The default locale is used if no [_locale](/Routing/Route/#Athena::Routing::Route--special-parameters) routing parameter has been set.\n    property default_locale : String = \"en\"\n\n    # Controls the IP addresses of trusted proxies that'll be used to get precise information about the client.\n    #\n    # See the [external documentation](/guides/proxies) for more information.\n    property trusted_proxies : Array(String)? = nil\n\n    # Controls which headers your `#trusted_proxies` use.\n    #\n    # See the [external documentation](/guides/proxies) for more information.\n    property trusted_headers : Athena::HTTP::Request::ProxyHeader = Athena::HTTP::Request::ProxyHeader[:forwarded_for, :forwarded_port, :forwarded_proto]\n\n    # By default the application can handle requests from any host.\n    # This property allows configuring regular expression patterns to control what hostnames the application is allowed to serve.\n    # This effectively prevents [host header attacks](https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html).\n    #\n    # If there is at least one pattern defined, requests whose hostname does _NOT_ match any of the patterns, will receive a 400 response.\n    property trusted_hosts : Array(Regex) = [] of Regex\n\n    # Allows overriding the header name to use for a given `AHTTP::Request::ProxyHeader`.\n    #\n    # See the [external documentation](/guides/proxies/#custom-headers) for more information.\n    property trusted_header_overrides : Hash(Athena::HTTP::Request::ProxyHeader, String) = {} of NoReturn => NoReturn\n\n    # Configuration related to the `ATH::Listeners::Format` listener.\n    #\n    # If enabled, the rules are used to determine the best format for the current request based on its\n    # [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) header.\n    #\n    # [AHTTP::Request::FORMATS](/HTTP/Request/#Athena::HTTP::Request::FORMATS) is used to map the request's `MIME` type to its format.\n    module FormatListener\n      include ADI::Extension::Schema\n\n      # If `false`, the format listener will be disabled and not included in the resulting binary.\n      property enabled : Bool = false\n\n      # The rules used to determine the best format.\n      # Rules should be defined in priority order, with the highest priority having index 0.\n      #\n      # ### Example\n      #\n      # ```\n      # ATH.configure({\n      #   framework: {\n      #     format_listener: {\n      #       enabled: true,\n      #       rules:   [\n      #         {priorities: [\"json\", \"xml\"], host: /api\\.example\\.com/, fallback_format: \"json\"},\n      #         {path: /^\\/image/, priorities: [\"jpeg\", \"gif\"], fallback_format: false},\n      #         {path: /^\\/admin/, priorities: [\"xml\", \"html\"]},\n      #         {priorities: [\"text/html\", \"*/*\"], fallback_format: \"html\"},\n      #       ],\n      #     },\n      #   },\n      # })\n      # ```\n      #\n      # Assuming an `accept` header with the value `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json`,\n      # a request made to `/foo` from the `api.example.com` hostname; the request format would be `json`.\n      # If the request was not made from that hostname; the request format would be `html`.\n      # The rules can be as complex or as simple as needed depending on the use case of your application.\n      #\n      # ---\n      # >>path: Use this rules configuration if the request's path matches the regex.\n      # >>host: Use this rules configuration if the request's hostname matches the regex.\n      # >>methods: Use this rules configuration if the request's method is one of these configured methods.\n      # >>priorities: Defines the order of media types the application prefers. If a format is provided instead of a media type,\n      # the format is converted into a list of media types matching the format.\n      # >>fallback_format: If `nil` and the `path`, `host`, or `methods` did not match the current request, skip this rule and try the next one.\n      # If set to a format string, use that format. If `false`, return a `406` instead of considering the next rule.\n      # >>stop: If `true`, disables the format listener for this and any following rules.\n      # Can be used as a way to enable the listener on a subset of routes within the application.\n      # >>prefer_extension: Determines if the `accept` header, or route path `_format` parameter takes precedence.\n      # For example, say there is a routed defined as `/foo.{_format}`. When `false`, the format from `_format` placeholder is checked last against the defined `priorities`.\n      # Whereas if `true`, it would be checked first.\n      # ---\n      array_of rules,\n        path : Regex? = nil,\n        host : Regex? = nil,\n        methods : Array(String)? = nil,\n        priorities : Array(String)? = nil,\n        fallback_format : String | Bool | Nil = \"json\",\n        stop : Bool = false,\n        prefer_extension : Bool = true\n    end\n\n    # Configures how `ATH::Listeners::CORS` functions.\n    # If no configuration is provided, that listener is disabled and will not be invoked at all.\n    module Cors\n      include ADI::Extension::Schema\n\n      property enabled : Bool = false\n\n      # CORS defaults that affect all routes globally.\n      module Defaults\n        include ADI::Extension::Schema\n\n        # Indicates whether the request can be made using credentials.\n        #\n        # Maps to the access-control-allow-credentials header.\n        property allow_credentials : Bool = false\n\n        # A white-listed array of valid origins. Each origin may be a static String, or a Regex.\n        #\n        # Can be set to [\"*\"] to allow any origin.\n        property allow_origin : Array(String | Regex) = [] of String | Regex\n\n        # The header or headers that can be used when making the actual request.\n        #\n        # Can be set to `[\"*\"]` to allow any headers.\n        #\n        # maps to the `access-control-allow-headers` header.\n        property allow_headers : Array(String) = [] of String\n\n        # Array of headers that the browser is allowed to read from the response.\n        #\n        # Maps to the access-control-expose-headers header.\n        property expose_headers : Array(String) = [] of String\n\n        # The method(s) allowed when accessing the resource.\n        #\n        # Maps to the `access-control-allow-methods` header.\n        # Defaults to the [CORS-safelisted methods](https://fetch.spec.whatwg.org/#cors-safelisted-method).\n        property allow_methods : Array(String) = ATH::Listeners::CORS::SAFELISTED_METHODS\n\n        # Number of seconds that the results of a preflight request can be cached.\n        #\n        # Maps to the `access-control-max-age header`.\n        property max_age : Int32 = 0\n      end\n    end\n\n    module Router\n      include ADI::Extension::Schema\n\n      # The default URI used to generate URLs in non-HTTP contexts.\n      # See the [Getting Started](/getting_started/routing/#in-commands) docs for more information.\n      property default_uri : String? = nil\n\n      # The default HTTP port when generating URLs.\n      # See the [Getting Started](/getting_started/routing/#url-generation) docs for more information.\n      property http_port : Int32 = 80\n\n      # The default HTTPS port when generating URLs.\n      # See the [Getting Started](/getting_started/routing/#url-generation) docs for more information.\n      property https_port : Int32 = 443\n\n      # Determines how invalid parameters should be treated when [Generating URLs](/getting_started/routing/#url-generation):\n      #\n      # * `true` - Raise an exception for mismatched requirements.\n      # * `false` - Do not raise an exception, but return an empty string.\n      # * `nil` - Disables checks, returning a URL with possibly invalid parameters.\n      property strict_requirements : Bool? = true\n    end\n\n    module ViewHandler\n      include ADI::Extension::Schema\n\n      # If `nil` values should be serialized.\n      property serialize_nil : Bool = false\n\n      # The `HTTP::Status` used when there is no response content.\n      property empty_content_status : ::HTTP::Status = :no_content\n\n      # The `HTTP::Status` used when validations fail.\n      #\n      # Currently not used. Included for future work.\n      property failed_validation_status : ::HTTP::Status = :unprocessable_entity\n    end\n\n    # Configures settings related to file uploads.\n    #\n    # ```\n    # ATH.configure({\n    #   framework: {\n    #     file_uploads: {\n    #       enabled: true,\n    #     },\n    #   },\n    # })\n    # ```\n    # See the [Getting Started](/getting_started/routing/#file-uploads) docs for more information.\n    module FileUploads\n      include ADI::Extension::Schema\n\n      # If `false`, native file upload support will be disabled and related types will not be included in the resulting binary.\n      property enabled : Bool = false\n\n      # The directory where temp files will be stored while requests are being processed.\n      # If `nil`, then a directory called `athena` will be used within the system's [tempdir](https://crystal-lang.org/api/Dir.html#tempdir:String-class-method) by default.\n      #\n      # WARNING: If providing a custom directory, it _MUST_ already exist.\n      property temp_dir : String? = nil\n\n      # Controls how many files may be uploaded at once.\n      property max_uploads : Int32 = 25\n\n      # The maximum allowed file size, in bytes, that are allowed to be uploaded.\n      # Defaults to 10 MiB.\n      property max_file_size : Int64 = 1024 * 1024 * 10\n    end\n  end\n\n  # :nodoc:\n  module Extension\n    macro included\n      macro finished\n        {% verbatim do %}\n          # Built-in parameters\n          {%\n            cfg = CONFIG[\"framework\"]\n            parameters = CONFIG[\"parameters\"]\n\n            parameters[\"framework.default_locale\"] = cfg[\"default_locale\"]\n\n            debug = parameters[\"framework.debug\"]\n\n            # If no debug parameter was already configured, try and determine an appropriate value:\n            # * true if configured explicitly via ENV var\n            # * true if env ENV var is present and not production\n            # * true if not compiled with --release\n            #\n            # This should default to `false`, except explicitly set otherwise\n            if debug.nil?\n              release_flag = flag?(:release)\n              debug_env = env(\"ATHENA_DEBUG\") == \"true\"\n              non_prod_env = env(\"ATHENA_ENV\") != \"production\"\n\n              parameters[\"framework.debug\"] = debug_env || non_prod_env || !release_flag\n            end\n          %}\n\n          # CORS Listener\n          {%\n            cfg = CONFIG[\"framework\"][\"cors\"]\n\n            if cfg[\"enabled\"]\n              # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers\n              if cfg[\"defaults\"][\"allow_credentials\"] && cfg[\"defaults\"][\"expose_headers\"].includes? \"*\"\n                cfg[\"defaults\"][\"expose_headers\"].raise \"'expose_headers' cannot contain a wildcard ('*') when 'allow_credentials' is 'true'.\"\n              end\n\n              # Normalize headers to lowercase for HTTP/2 support.\n              allow_headers = cfg[\"defaults\"][\"allow_headers\"].map &.downcase\n              expose_headers = cfg[\"defaults\"][\"expose_headers\"].map &.downcase\n\n              # TODO: Support multiple paths\n              config = <<-CRYSTAL\n                ATH::Listeners::CORS::Config.new(\n                  allow_credentials: #{cfg[\"defaults\"][\"allow_credentials\"]},\n                  allow_origin: #{cfg[\"defaults\"][\"allow_origin\"]},\n                  allow_headers: #{allow_headers},\n                  allow_methods: #{cfg[\"defaults\"][\"allow_methods\"]},\n                  expose_headers: #{expose_headers},\n                  max_age: #{cfg[\"defaults\"][\"max_age\"]}\n                )\n              CRYSTAL\n\n              SERVICE_HASH[\"athena_framework_listeners_cors\"] = {\n                class:      ATH::Listeners::CORS,\n                parameters: {\n                  # TODO: Consider having some other service responsible for resolving the config obj\n                  config: {value: config.id},\n                },\n              }\n            end\n          %}\n\n          # Routing\n          {%\n            parameters = CONFIG[\"parameters\"]\n            cfg = CONFIG[\"framework\"][\"router\"]\n\n            parameters[\"framework.router.request_context.host\"] = \"localhost\"\n            parameters[\"framework.router.request_context.scheme\"] = \"http\"\n            parameters[\"framework.router.request_context.base_url\"] = \"\"\n            parameters[\"framework.request_listener.http_port\"] = cfg[\"http_port\"]\n            parameters[\"framework.request_listener.https_port\"] = cfg[\"https_port\"]\n\n            # TODO: Make this `default_router` with a public alias of `router` instead.\n            SERVICE_HASH[router_id = \"default_router\"] = {\n              class:      ATH::Routing::Router,\n              public:     true,\n              parameters: {\n                default_locale:      {value: \"%framework.default_locale%\"},\n                strict_requirements: {value: cfg[\"strict_requirements\"]},\n              },\n            }\n\n            SERVICE_HASH[request_context_id = \"athena_routing_request_context\"] = {\n              class:      ART::RequestContext,\n              factory:    {ART::RequestContext, \"from_uri\"},\n              parameters: {\n                uri:        {value: \"%framework.router.request_context.base_url%\"},\n                host:       {value: \"%framework.router.request_context.host%\"},\n                scheme:     {value: \"%framework.router.request_context.scheme%\"},\n                http_port:  {value: \"%framework.request_listener.http_port%\"},\n                https_port: {value: \"%framework.request_listener.https_port%\"},\n              },\n            }\n\n            SERVICE_HASH[\"athena_framework_controllers_redirect\"] = {\n              class:      Athena::Framework::Controller::Redirect,\n              public:     true,\n              parameters: {\n                http_port:  {value: \"%framework.request_listener.http_port%\"},\n                https_port: {value: \"%framework.request_listener.https_port%\"},\n              },\n            }\n\n            if default_uri = cfg[\"default_uri\"]\n              SERVICE_HASH[request_context_id][\"parameters\"][\"uri\"][\"value\"] = default_uri\n            end\n          %}\n\n          # Format Listener\n          {%\n            cfg = CONFIG[\"framework\"][\"format_listener\"]\n\n            if cfg[\"enabled\"] && !cfg[\"rules\"].empty?\n              matcher_arguments_to_service_id_map = {} of Nil => Nil\n\n              calls = [] of Nil\n\n              cfg[\"rules\"].each_with_index do |rule, idx|\n                matcher_id = {rule[\"path\"], rule[\"host\"], rule[\"methods\"], nil}.symbolize\n\n                # Optimization to allow reusing request matcher instances that are common between the rules.\n                if matcher_arguments_to_service_id_map[matcher_id] == nil\n                  matchers = [] of Nil\n\n                  if v = rule[\"path\"]\n                    matchers << \"AHTTP::RequestMatcher::Path.new(#{v})\".id\n                  end\n\n                  if v = rule[\"host\"]\n                    matchers << \"AHTTP::RequestMatcher::Hostname.new(#{v})\".id\n                  end\n\n                  if v = rule[\"methods\"]\n                    matchers << \"AHTTP::RequestMatcher::Method.new(#{v})\".id\n                  end\n\n                  SERVICE_HASH[matcher_service_id = \"framework_view_handler_request_match_#{idx}\"] = {\n                    class:      AHTTP::RequestMatcher,\n                    parameters: {\n                      matchers: {value: \"#{matchers} of AHTTP::RequestMatcher::Interface\".id},\n                    },\n                  }\n\n                  matcher_arguments_to_service_id_map[matcher_id] = matcher_service_id\n                else\n                  matcher_service_id = matcher_arguments_to_service_id_map[matcher_id]\n                end\n\n                calls << {\"add\", {matcher_service_id.id, \"ATH::View::FormatNegotiator::Rule.new(\n                  stop: #{rule[\"stop\"]},\n                  priorities: #{rule[\"priorities\"]},\n                  prefer_extension: #{rule[\"prefer_extension\"]},\n                  fallback_format: #{rule[\"fallback_format\"]}\n                )\".id}}\n              end\n\n              SERVICE_HASH[\"athena_framework_view_format_negotiator\"] = {\n                class: ATH::View::FormatNegotiator,\n                calls: calls,\n              }\n\n              SERVICE_HASH[\"athena_framework_listeners_format\"] = {\n                class: ATH::Listeners::Format,\n              }\n            end\n          %}\n\n          # View Handler\n          {%\n            cfg = CONFIG[\"framework\"][\"view_handler\"]\n\n            SERVICE_HASH[\"athena_framework_view_view_handler\"] = {\n              class:      ATH::View::ViewHandler,\n              parameters: {\n                emit_nil:                 {value: cfg[\"serialize_nil\"]},\n                failed_validation_status: {value: cfg[\"failed_validation_status\"]},\n                empty_content_status:     {value: cfg[\"empty_content_status\"]},\n              },\n            }\n          %}\n\n          # File Uploads\n          {%\n            cfg = CONFIG[\"framework\"][\"file_uploads\"]\n\n            if cfg[\"enabled\"]\n              SERVICE_HASH[\"athena_framework_listeners_file\"] = {\n                class: Athena::Framework::Listeners::File,\n              }\n\n              SERVICE_HASH[\"athena_framework_file_parser\"] = {\n                class:      ATH::FileParser,\n                parameters: {\n                  temp_dir:      {value: cfg[\"temp_dir\"]},\n                  max_uploads:   {value: cfg[\"max_uploads\"]},\n                  max_file_size: {value: cfg[\"max_file_size\"]},\n                },\n              }\n            end\n          %}\n        {% end %}\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/commands/debug_event_dispatcher.cr",
    "content": "@[ACONA::AsCommand(\"debug:event-dispatcher\", description: \"Display configured listeners for an application\")]\n@[ADI::Register]\n# Utility command to allow viewing information about an `AED::EventDispatcherInterface`.\n# Includes the type/method of each event listener, along with the order they run in based on their priority.\n# Accepts an optional argument to allow filtering the list to a specific event, or ones that contain the provided string.\n#\n# ```text\n# $ ./bin/console debug:event-dispatcher\n# Registered Listeners Grouped by Event\n# =====================================\n#\n# Athena::Framework::Events::Exception event\n# ------------------------------------------\n#\n#  ------- -------------------------------------------------- ----------\n#   Order   Callable                                           Priority\n#  ------- -------------------------------------------------- ----------\n#   #1      Athena::Framework::Listeners::Error#on_exception   -50\n#  ------- -------------------------------------------------- ----------\n#\n# Athena::Framework::Events::Request event\n# ----------------------------------------\n#\n#  ------- -------------------------------------------------- ----------\n#   Order   Callable                                           Priority\n#  ------- -------------------------------------------------- ----------\n#   #1      Athena::Framework::Listeners::CORS#on_request      250\n#   #2      Athena::Framework::Listeners::Format#on_request    34\n#   #3      Athena::Framework::Listeners::Routing#on_request   32\n#  ------- -------------------------------------------------- ----------\n#\n# ...\n# ```\n#\n# TODO: Support dedicated `AED::EventDispatcherInterface` services other than the default.\nclass Athena::Framework::Commands::DebugEventDispatcher < ACON::Command\n  def initialize(\n    @dispatcher : AED::EventDispatcherInterface,\n  )\n    super()\n  end\n\n  protected def configure : Nil\n    self\n      .argument(\"event\", description: \"An event name or a part of the event name\") { @dispatcher.listeners.keys.map &.to_s }\n      .option(\"format\", value_mode: :required, description: \"The output format (txt)\", default: \"txt\") { ACON::Helper::Descriptor.new.formats }\n      .option(\"raw\", nil, :none, \"To output raw command help\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    style = ACON::Style::Athena.new input, output\n\n    # TODO: Allow resolving a specific dispatcher service\n    dispatcher = @dispatcher\n\n    event_class_map = dispatcher.listeners.each_key.each_with_object({} of String => ACTR::EventDispatcher::Event.class) do |event, map|\n      map[event.to_s] = event\n    end\n\n    event_class = nil\n    event_classes = nil\n\n    if event = input.argument \"event\"\n      e = event_class_map[event]?\n\n      if e && dispatcher.has_listeners? e\n        event_class = e\n      else\n        events = self.search_for_event dispatcher, event\n\n        if events.empty?\n          style.error_style.warning \"The event '#{event}' does not have any registered listeners.\"\n\n          return Status::SUCCESS\n        elsif 1 == events.size\n          event_class = events.first\n        else\n          event_classes = events\n        end\n      end\n    end\n\n    helper = Athena::Framework::Console::Helper::Descriptor.new\n\n    helper\n      .describe(\n        style,\n        dispatcher,\n        ATH::Console::Descriptor::EventDispatcherContext.new(\n          output: style,\n          event_class: event_class,\n          event_classes: event_classes,\n          format: input.option(\"format\", String),\n          raw_text: input.option(\"raw\", Bool),\n        )\n      )\n\n    Status::SUCCESS\n  end\n\n  private def search_for_event(dispatcher : AED::EventDispatcherInterface, event : String) : Array(ACTR::EventDispatcher::Event.class)\n    event_class_string = event.downcase\n\n    matching_events = [] of ACTR::EventDispatcher::Event.class\n\n    dispatcher.listeners.each_key.each do |event_class|\n      matching_events << event_class if event_class.to_s.downcase.includes? event_class_string\n    end\n\n    matching_events\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/commands/debug_router.cr",
    "content": "@[ACONA::AsCommand(\"debug:router\", description: \"Display current routes for an application\")]\n@[ADI::Register]\n# Utility command to allow viewing all of the routes the framework is aware of within your application.\n#\n# ```text\n# $ ./bin/console debug:router\n# ----------------  -------  -------  -----  --------------------------------------------\n# Name              Method   Scheme   Host   Path\n# ----------------  -------  -------  -----  --------------------------------------------\n# homepage          ANY      ANY      ANY    /\n# contact           GET      ANY      ANY    /contact\n# contact_process   POST     ANY      ANY    /contact\n# article_show      ANY      ANY      ANY    /articles/{_locale}/{year}/{title}.{_format}\n# blog              ANY      ANY      ANY    /blog/{page}\n# blog_show         ANY      ANY      ANY    /blog/{slug}\n# ----------------  -------  -------  -----  --------------------------------------------\n# ```\n#\n# The command also supports viewing additional information about a specific route:\n# ```text\n# $ ./bin/console debug:router test\n# +--------------+-------------------------------------+\n# | Property     | Value                               |\n# +--------------+-------------------------------------+\n# | Route Name   | test                                |\n# | Path         | /{id}/{a}                           |\n# | Path Regex   | ^/(?P<id>\\d+)/(?P<a>10)$            |\n# | Host         | ANY                                 |\n# | Host Regex   |                                     |\n# | Scheme       | ANY                                 |\n# | Methods      | GET                                 |\n# | Requirements | a: 10                               |\n# |              | id: \\d+                             |\n# | Class        | Athena::Routing::Route              |\n# | Defaults     | _controller: ExampleController#root |\n# +--------------+-------------------------------------+\n# ```\n#\n# TIP: Checkout `ATH::Commands::DebugRouterMatch` to test which route a given path resolves to.\nclass Athena::Framework::Commands::DebugRouter < ACON::Command\n  def initialize(\n    @router : ART::RouterInterface,\n  )\n    super()\n  end\n\n  protected def configure : Nil\n    self\n      .argument(\"name\", description: \"A route name\") { @router.route_collection.routes.keys }\n      .option(\"show-controllers\", value_mode: :none, description: \"Show assigned controllers in overview\")\n      .option(\"format\", value_mode: :required, description: \"The output format (txt)\", default: \"txt\") { ACON::Helper::Descriptor.new.formats }\n      .option(\"raw\", value_mode: :none, description: \"To output raw command help\")\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    helper = Athena::Framework::Console::Helper::Descriptor.new\n    routes = @router.route_collection\n\n    style = ACON::Style::Athena.new input, output\n\n    if name = input.argument \"name\"\n      route = routes[name]?\n      matching_routes = self.find_route_name_containing name, routes\n\n      if !input.interactive? && !route && !matching_routes.empty?\n        helper\n          .describe(\n            style,\n            self.find_route_containing(name, routes),\n            ATH::Console::Descriptor::RoutingContext.new(\n              output: style,\n              show_controllers: input.option(\"show-controllers\", Bool),\n              format: input.option(\"format\", String),\n              raw_text: input.option(\"raw\", Bool),\n            )\n          )\n\n        return Status::SUCCESS\n      end\n\n      if !route && !matching_routes.empty?\n        default = (1 == matching_routes.size) ? matching_routes.first : nil\n        name = style.choice(\"Select one of the matching routes\", matching_routes, default).as String\n        route = routes[name]\n      end\n\n      if !route\n        raise ACON::Exception::InvalidArgument.new \"The route '#{name}' does not exist.\"\n      end\n\n      helper\n        .describe(\n          style,\n          route,\n          ATH::Console::Descriptor::RoutingContext.new(\n            name: name,\n            output: style,\n            format: input.option(\"format\", String),\n            raw_text: input.option(\"raw\", Bool),\n          )\n        )\n    else\n      helper\n        .describe(\n          style,\n          routes,\n          ATH::Console::Descriptor::RoutingContext.new(\n            output: style,\n            show_controllers: input.option(\"show-controllers\", Bool),\n            format: input.option(\"format\", String),\n            raw_text: input.option(\"raw\", Bool),\n          )\n        )\n    end\n\n    Status::SUCCESS\n  end\n\n  private def find_route_name_containing(name : String, routes : ART::RouteCollection) : Array(String)\n    routes.compact_map do |route_name, _|\n      next unless route_name.includes? name\n\n      route_name\n    end\n  end\n\n  private def find_route_containing(name : String, routes : ART::RouteCollection) : ART::RouteCollection\n    found_routes = ART::RouteCollection.new\n\n    routes.each do |route_name, route|\n      found_routes.add route_name, route if route_name.includes? name\n    end\n\n    found_routes\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/commands/debug_router_match.cr",
    "content": "@[ACONA::AsCommand(\"debug:router:match\", description: \"Simulate a path match to see which route, if any, would handle it\")]\n@[ADI::Register]\n# Similar to `ATH::Commands::DebugRouter`, but instead of providing the route name, you provide the request path\n# in order to determine which, if any, route that path maps to.\n#\n# ```text\n# $ ./bin/console debug:router:match /user/10\n#  [OK] Route 'example_controller_user' matches\n#\n# +--------------+-------------------------------------+\n# | Property     | Value                               |\n# +--------------+-------------------------------------+\n# | Route Name   | example_controller_user             |\n# | Path         | /user/{id}                          |\n# | Path Regex   | ^/user/(?P<id>\\d+)$                 |\n# | Host         | ANY                                 |\n# | Host Regex   |                                     |\n# | Scheme       | ANY                                 |\n# | Methods      | GET                                 |\n# | Requirements | id: \\d+                             |\n# | Class        | Athena::Routing::Route              |\n# | Defaults     | _controller: ExampleController#user |\n# +--------------+-------------------------------------+\n# ```\n#\n# Or if the route only partially matches:\n#\n# ```text\n# $ ./bin/console debug:router:match /user/foo\n#  Route 'example_controller_user' almost matches but requirement for 'id' does not match (\\d+)\n#\n#  [ERROR] None of the routes match the path '/user/foo'\n# ```\nclass Athena::Framework::Commands::DebugRouterMatch < ACON::Command\n  def initialize(\n    @router : ART::RouterInterface,\n  )\n    super()\n  end\n\n  protected def configure : Nil\n    self\n      .argument(\"path_info\", :required, \"A path to test\")\n      .option(\"method\", nil, :required, \"Set the HTTP method to use\")\n      .option(\"host\", nil, :required, \"Set the URI host\")\n      .option(\"scheme\", nil, :required, \"Set the URI scheme (usually http or https)\")\n      .help(\n        <<-HELP\n        The <info>%command.name%</info> shows which routes match a given request and which don't and for what reason:\n\n          <info>%command.full_name% /foo</info>\n\n        or\n\n          <info>%command.full_name% /foo --method=POST --scheme=https --host=https://crystal-lang.org/ --verbose</info>\n        HELP\n      )\n  end\n\n  protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status\n    style = ACON::Style::Athena.new input, output\n\n    context = @router.context\n\n    if method = input.option \"method\"\n      context.method = method\n    end\n\n    if scheme = input.option \"scheme\"\n      context.scheme = scheme\n    end\n\n    if host = input.option \"host\"\n      context.host = host\n    end\n\n    matcher = ART::Matcher::TraceableURLMatcher.new @router.route_collection, context\n\n    traces = matcher.traces input.argument \"path_info\", String\n\n    style.new_line\n\n    matches = false\n\n    traces.each do |trace|\n      if trace.level.partial?\n        style.text \"Route <info>'#{trace.name}'</> almost matches but #{trace.message.sub 0, trace.message[0].downcase}}\"\n      elsif trace.level.full?\n        style.success \"Route '#{trace.name}' matches\"\n\n        router_debug_command = self.application.find(\"debug:router\")\n        router_debug_command.run ACON::Input::Hash.new({\"name\" => trace.name}), output\n\n        matches = true\n      else\n        style.text \"Route '#{trace.name}' does not match: #{trace.message}\"\n      end\n    end\n\n    unless matches\n      style.error \"None of the routes match the path '#{input.argument \"path_info\"}'\"\n\n      return Status::FAILURE\n    end\n\n    Status::SUCCESS\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/compiler_passes/expose_controller_services.cr",
    "content": "module Athena::Framework::CompilerPasses::MakeControllerServicesPublicPass\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          SERVICE_HASH.each do |service_id, metadata|\n            if metadata[\"class\"] <= ATH::Controller\n              metadata[\"public\"] = true\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/controller/redirect.cr",
    "content": "# :nodoc:\nclass Athena::Framework::Controller::Redirect\n  def initialize(\n    @http_port : Int32? = nil,\n    @https_port : Int32? = nil,\n  ); end\n\n  # ameba:disable Metrics/CyclomaticComplexity:\n  def redirect_url(\n    request : AHTTP::Request,\n    path : String,\n    permanent : Bool = false,\n    scheme : String? = nil,\n    http_port : Int32? = nil,\n    https_port : Int32? = nil,\n    keep_request_method : Bool = false,\n  ) : AHTTP::RedirectResponse\n    if path.empty?\n      raise AHK::Exception::HTTPException.new (permanent ? ::HTTP::Status::GONE : ::HTTP::Status::NOT_FOUND), \"\"\n    end\n\n    status = if keep_request_method\n               permanent ? ::HTTP::Status::PERMANENT_REDIRECT : ::HTTP::Status::TEMPORARY_REDIRECT\n             else\n               permanent ? ::HTTP::Status::MOVED_PERMANENTLY : ::HTTP::Status::FOUND\n             end\n\n    scheme ||= request.scheme\n\n    if path.starts_with? \"//\"\n      path = \"#{scheme}:#{path}\"\n    end\n\n    uri = URI.parse path\n\n    # If the path has a scheme, assume it is a full URI\n    if uri.scheme.presence\n      return AHTTP::RedirectResponse.new path, status\n    end\n\n    # If the request has query params of its own, be sure to retain both sets of params.\n    if request.query.presence\n      # Don't use `merge!` here so the query string is correctly refreshed on the uri.\n      uri.query_params = uri.query_params.merge request.query_params, replace: false\n    end\n\n    if \"http\" == scheme\n      if http_port.nil?\n        http_port = if \"http\" == request.scheme\n                      request.port\n                    else\n                      @http_port\n                    end\n      end\n\n      uri.port = http_port if http_port && 80 != http_port\n    elsif \"https\" == scheme\n      if https_port.nil?\n        https_port = if \"https\" == request.scheme\n                       request.port\n                     else\n                       @https_port\n                     end\n      end\n\n      uri.port = https_port if https_port && 443 != https_port\n    end\n\n    uri.host = request.host\n    uri.scheme = scheme\n\n    AHTTP::RedirectResponse.new uri.normalize!.to_s, status\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/controller/value_resolvers/enum.cr",
    "content": "require \"./interface\"\n\n@[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 105}])]\n# Handles resolving an [Enum](https://crystal-lang.org/api/Enum.html) member from a string value that is stored in the request's [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes).\n# This resolver supports both numeric and string based parsing, returning a proper error response if the provided value does not map to any valid member.\n#\n# ```\n# require \"athena\"\n#\n# enum Color\n#   Red\n#   Blue\n#   Green\n# end\n#\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\"/numeric/{color}\")]\n#   def get_color_numeric(color : Color) : Color\n#     color\n#   end\n#\n#   @[ARTA::Get(\"/string/{color}\")]\n#   def get_color_string(color : Color) : Color\n#     color\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /numeric/1 # => \"blue\"\n# # GET /string/red # => \"red\"\n# ```\n#\n# TIP: Checkout `ART::Requirement::Enum` for an easy way to restrict routing to an enum's members, or a subset of them.\nstruct Athena::Framework::Controller::ValueResolvers::Enum\n  include Athena::Framework::Controller::ValueResolvers::Interface\n\n  # :inherit:\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata)\n    return unless parameter.instance_of? ::Enum\n    return unless enum_type = parameter.first_type_of ::Enum\n    return unless value = request.attributes.get? parameter.name, String\n\n    member = if (num = value.to_i128?(whitespace: false)) && (m = enum_type.from_value? num)\n               m\n             elsif m = enum_type.parse? value\n               m\n             end\n\n    unless member\n      raise AHK::Exception::BadRequest.new \"Parameter '#{parameter.name}' of enum type '#{enum_type}' has no valid member for '#{value}'.\"\n    end\n\n    member\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/controller/value_resolvers/interface.cr",
    "content": "# Value resolvers handle resolving the argument(s) to pass to a controller action based on values stored within the `AHTTP::Request`, or some other source.\n#\n# Custom resolvers can be defined by creating a service that implements this interface, and is tagged with `ATHR::Interface::TAG`.\n# The tag also accepts an optional *priority* field the determines the order in which the resolvers execute.\n# The list of built in resolvers and their priorities can be found on the `ATH::Controller::ValueResolvers` module.\n#\n# WARNING: Resolvers that mutate a value already within the [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes), such as one from a route or query parameter _MUST_ have a priority `>100`\n# to ensure the custom logic is applied before the raw value is resolved via the `ATHR::RequestAttribute` resolver.\n#\n# The first resolver to return a value wins and no other resolvers will be executed for that particular parameter.\n# The resolver should return `nil` to denote no value could be resolved,\n# such as if the parameter is of the wrong type, does not have a specific annotation applied, or anything else that can be deduced from either parameter.\n# If no resolver is able to resole a value for a specific parameter, an error is thrown and processing of the request ceases.\n#\n# For example:\n#\n# ```\n# @[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 10}])]\n# struct CustomResolver\n#   include ATHR::Interface\n#\n#   # :inherit:\n#   def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : MyCustomType?\n#     # Return early if a value is unresolvable from the current *request* and/or *parameter*.\n#     return if parameter.type != MyCustomType\n#\n#     # Return the resolved value. It could either come from the request itself, an injected service, or hard coded.\n#     MyCustomType.new \"foo\"\n#   end\n# end\n# ```\n#\n# Now, given the following controller:\n#\n# ```\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\"/\")]\n#   def root(my_obj : MyCustomType) : String\n#     my_obj.name\n#   end\n# end\n#\n# # GET / # => \"foo\"\n# ```\n#\n# Since none of the built-in resolvers are applicable for this parameter type,\n# nor is there a *my_obj* value in [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes), assuming no customer listeners manually add it, the `CustomResolver` would take over and provide the value for that parameter.\n#\n# ## Configuration\n#\n# In some cases, the request and parameter themselves may not be enough to know if a resolver should try to resolve a value or not.\n# A naive example would be say you want to have a resolver that multiplies certain `Int32` parameters by `10`.\n# It wouldn't be enough to just check if the parameter is an `Int32` as that leaves too much room for unexpected contexts to be resolved unexpectedly.\n# For such cases a `.configuration` annotation type may be defined to allow marking the specific parameters the related resolver should apply to.\n#\n# For example:\n#\n# ```\n# # The priority _MUST_ be `>100` to ensure the value isnt preemptively resolved by the `ATHR::RequestAttribute` resolver.\n# @[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 110}])]\n# struct Multiply\n#   include ATHR::Interface\n#\n#   # The value provided to the macro maps to the name of the annotation.\n#   configuration Multiply::This\n#\n#   def initialize(\n#     @annotation_resolver : ATH::AnnotationResolver,\n#   ); end\n#\n#   # :inherit:\n#   def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : Int32?\n#     # Return early if the controller action parameter doesn't have the annotation.\n#     return unless @annotation_resolver.action_parameter_annotations(request, parameter.name).has? This\n#\n#     # Return early if the parameter type is not `Int32`.\n#     return if parameter.type != Int32\n#\n#     request.attributes.get(parameter.name, Int32) * 10\n#   end\n# end\n#\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\"/{num}\")]\n#   def multiply(\n#     @[Multiply::This]\n#     num : Int32,\n#   ) : Int32\n#     num\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /10 # => 100\n# ```\n#\n# While this example is quite naive, this pattern is used as part of the `ATHR::RequestBody` to know if an object should be deserialized from the request body, or is intended be supplied some other way.\n#\n# ### Extra Data\n#\n# Another use case for this pattern is providing extra data on a per parameter basis.\n# For example, say we wanted to allow customizing the multiplier instead of having it hard coded to `10`.\n#\n# In order to do this we can pass properties to the `.configuration` macro to define what we want to be configurable via the annotation.\n# Next we can then use this value in our resolver, and when applying to a specific parameter:\n#\n# ```\n# # The priority _MUST_ be `>100` to ensure the value isnt preemptively resolved by the `ATHR::RequestAttribute` resolver.\n# @[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 110}])]\n# struct Multiply\n#   include ATHR::Interface\n#\n#   configuration Multiply::This, multiplier : Int32 = 10\n#\n#   def initialize(\n#     @annotation_resolver : ATH::AnnotationResolver,\n#   ); end\n#\n#   # :inherit:\n#   def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : Int32?\n#     # Return early if the controller action parameter doesn't have the annotation.\n#     return unless @annotation_resolver.action_parameter_annotations(request, parameter.name).has? This\n#\n#     # Return early if the parameter type is not `Int32`.\n#     return if parameter.type != Int32\n#\n#     request.attributes.get(parameter.name, Int32) * config.multiplier\n#   end\n# end\n#\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\"/{num}\")]\n#   def multiply(\n#     @[Multiply::This(multiplier: 50)]\n#     num : Int32,\n#   ) : Int32\n#     num\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /10 # => 500\n# ```\n#\n# A more real-world example of this pattern is how the `ATHR::Time` resolver allows customizing the format and/or location that should be used to parse the datetime string via `ATHA::MapTime` annotation.\n#\n# TIP: The configuration annotation may be defined in another namespace via prefixing the FQN of the path with `::`.\n# E.g. `configuration ::MyApp::Annotation::Multiply`.\n#\n# ## Handling Multiple Types\n#\n# When using an annotation to enable a particular resolver, it may be required to handle parameters of varying types.\n# E.g. it should do one thing when enabled on an `Int32` parameter, while a different thing when applied to a `String` parameter.\n# But both things are related enough to not warrant dedicated resolvers.\n# Because the type of the parameter is stored within a generic type, it can be used to overload the `#resolve` method based on its type\n# For example:\n#\n# ```\n# # The priority _MUST_ be `>100` to ensure the value isnt preemptively resolved by the `ATHR::RequestAttribute` resolver.\n# @[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 110}])]\n# struct MyResolver\n#   include ATHR::Interface\n#\n#   configuration MyResolver::Enable\n#\n#   def initialize(\n#     @annotation_resolver : ATH::AnnotationResolver,\n#   ); end\n#\n#   # :inherit:\n#   def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata(Int32)) : Int32?\n#     return unless @annotation_resolver.action_parameter_annotations(request, parameter.name).has? Enable\n#\n#     request.attributes.get(parameter.name, Int32) * 10\n#   end\n#\n#   # :inherit:\n#   def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata(String)) : String?\n#     return unless @annotation_resolver.action_parameter_annotations(request, parameter.name).has? Enable\n#\n#     request.attributes.get(parameter.name, String).upcase\n#   end\n#\n#   # :inherit:\n#   #\n#   # Fallback overload for types other than `Int32` and `String.\n#   def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : Nil\n#   end\n# end\n#\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\"/integer/{value}\")]\n#   def integer(\n#     @[MyResolver::Enable]\n#     value : Int32,\n#   ) : Int32\n#     value\n#   end\n#\n#   @[ARTA::Get(\"/string/{value}\")]\n#   def string(\n#     @[MyResolver::Enable]\n#     value : String,\n#   ) : String\n#     value\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /integer/10  # => 100\n# # GET /string/foo # => \"FOO\"\n# ```\n#\n# ### Free Vars\n#\n# If more precision is required, a [free variable](https://crystal-lang.org/reference/syntax_and_semantics/type_restrictions.html#free-variables)\n# can be used to extract the type of the related parameter such that it can be used to generate the proper code.\n#\n# An example of this is how `ATHR::RequestBody` handles both `ASR::Serializable` and `JSON::Serializable` types via:\n#\n# ```\n# {% begin %}\n#   {% if T.instance <= ASR::Serializable %}\n#     object = @serializer.deserialize T, body, :json\n#   {% elsif T.instance <= JSON::Serializable %}\n#     object = T.from_json body\n#   {% else %}\n#     return\n#   {% end %}\n# {% end %}\n# ```\n#\n# This works well to make the compiler happy when previous methods are not enough.\n#\n# ### Strict Typing\n#\n# In all of the examples so far, the resolvers could be applied to any parameter of any type and all of the logic to resolve a value would happen at runtime.\n# In some cases a specific resolver may only support a single, or small subset of types.\n# Such as how the `ATHR::RequestBody` resolver only allows `ASR::Serializable`, `JSON::Serializable`, or `URI::Params::Serializable` types.\n# In this case, the `ATHR::Interface::Typed` module may be used to define the allowed parameter types.\n#\n# WARNING: Strict typing is _ONLY_ supported when a configuration annotation is used to enable the resolver.\n#\n# ```\n# @[ADI::Register(tags: [{name: ATHR::Interface::TAG}])]\n# struct MyResolver\n#   # Multiple types may also be supplied by providing it a comma separated list.\n#   # If `nil` is a valid option, the `Nil` type should also be included.\n#   include ATHR::Interface::Typed(String)\n#\n#   configuration MyResolver::Enable\n#\n#   def initialize(\n#     @annotation_resolver : ATH::AnnotationResolver,\n#   ); end\n#\n#   # :inherit:\n#   def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : String?\n#     return unless @annotation_resolver.action_parameter_annotations(request, parameter.name).has? Enable\n#\n#     \"foo\"\n#   end\n# end\n#\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\"/integer\")]\n#   def integer(\n#     @[MyResolver::Enable]\n#     value : Int32,\n#   ) : Int32\n#     value\n#   end\n#\n#   @[ARTA::Get(\"/string\")]\n#   def string(\n#     @[MyResolver::Enable]\n#     value : String,\n#   ) : String\n#     value\n#   end\n# end\n#\n# ATH.run\n#\n# # Error: The annotation '@[MyResolver::Enable]' cannot be applied to 'ExampleController#integer:value : Int32'\n# # since the 'MyResolver' resolver only supports parameters of type 'String'.\n# ```\n#\n# Since `MyResolver` was defined to only support `String` types, a compile time error is raised when its annotation is applied to a non `String` parameter.\n# This feature pairs nicely with the [free var][Athena::Framework::Controller::ValueResolvers::Interface--free-vars] section as it essentially allows\n# scoping the possible types of `T` to the set of types defined as part of the module.\nmodule Athena::Framework::Controller::ValueResolvers::Interface\n  include Athena::HTTPKernel::Controller::ValueResolvers::Interface\n\n  # :nodoc:\n  ANNOTATION_RESOLVER_MAP = {} of Nil => Nil\n\n  # The tag name for `ATHR::Interface` services.\n  TAG = \"athena.controller.value_resolver\"\n\n  # Helper macro around `ADI.configuration_annotation` that allows defining resolver specific annotations.\n  # See the underlying macro and the [configuration][Athena::Framework::Controller::ValueResolvers::Interface--configuration] section for more information.\n  macro configuration(name, *args)\n    ADI.configuration_annotation {{name.id}}{% unless args.empty? %}, {{args.splat}}{% end %}\n    {% ANNOTATION_RESOLVER_MAP[name.id] = @type.resolve %}\n  end\n\n  # Represents an `ATHR::Interface` that only supports a subset of types.\n  #\n  # See the [strict typing][Athena::Framework::Controller::ValueResolvers::Interface--strict-typing] section for more information.\n  module Typed(*SupportedTypes)\n    include Athena::Framework::Controller::ValueResolvers::Interface\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/controller/value_resolvers/query_parameter.cr",
    "content": "# Attempts to resolve the value from the request's query parameters for any parameter with the `ATHA::MapQueryParameter` annotation.\n# Supports most primitive types, as well as arrays of most primitive types, and enums.\n#\n# The name of the query parameter is assumed to be the same as the controller action parameter's name.\n# This can be customized via the `name` field on the annotation.\n#\n# If the controller action parameter is not-nilable nor has a default value and is missing, an `AHK::Exception::NotFound` exception will be raised by default.\n# Similarly, an exception will be raised if the value fails to be converted to the expected type.\n# The specific type of exception can be customized via the `validation_failed_status` field on the annotation.\n#\n# ```\n# require \"athena\"\n#\n# enum Color\n#   Red\n#   Green\n#   Blue\n# end\n#\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\"/\")]\n#   def index(\n#     @[ATHA::MapQueryParameter] ids : Array(Int32),\n#     @[ATHA::MapQueryParameter(name: \"firstName\")] first_name : String,\n#     @[ATHA::MapQueryParameter] required : Bool,\n#     @[ATHA::MapQueryParameter] age : Int32,\n#     @[ATHA::MapQueryParameter] color : Color,\n#     @[ATHA::MapQueryParameter] category : String = \"\",\n#     @[ATHA::MapQueryParameter] theme : String? = nil,\n#   ) : Nil\n#     ids        # => [1, 2]\n#     first_name # => \"Jon\"\n#     required   # => false\n#     age        # => 123\n#     color      # => Color::Blue\n#     category   # => \"\"\n#     theme      # => nil\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /?ids=1&ids=2&firstName=Jon&required=false&age=123&color=blue\n# ```\n@[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 110}])]\nstruct Athena::Framework::Controller::ValueResolvers::QueryParameter\n  include Athena::Framework::Controller::ValueResolvers::Interface\n\n  # Enables the `ATHR::QueryParameter` resolver for the parameter this annotation is applied to.\n  # See the related resolver documentation for more information.\n  configuration ::Athena::Framework::Annotations::MapQueryParameter,\n    name : String? = nil,\n    validation_failed_status : ::HTTP::Status = :not_found\n\n  def initialize(\n    @annotation_resolver : ATH::AnnotationResolver,\n  ); end\n\n  # :inherit:\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata)\n    parameter_annotations = @annotation_resolver.action_parameter_annotations(request, parameter.name)\n\n    return unless ann = parameter_annotations[ATHA::MapQueryParameter]?\n\n    name = ann.name || parameter.name\n    validation_failed_status = ann.validation_failed_status\n\n    params = request.query_params\n\n    unless params.has_key? name\n      return if parameter.nilable? || parameter.has_default?\n\n      raise AHK::Exception::HTTPException.from_status validation_failed_status, \"Missing query parameter: '#{name}'.\"\n    end\n\n    value = if parameter.instance_of? Array\n              params.fetch_all name\n            else\n              params[name]\n            end\n\n    begin\n      parameter.type.from_parameter value\n    rescue ex : ArgumentError\n      # Catch type cast errors and bubble it up as a BadRequest\n      raise AHK::Exception::HTTPException.from_status validation_failed_status, \"Invalid query parameter: '#{name}'.\", cause: ex\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/controller/value_resolvers/request_body.cr",
    "content": "require \"uri/params/serializable\"\n\n@[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 105}])]\n# Attempts to resolve the value of any parameter with the `ATHA::MapRequestBody` annotation by\n# deserializing the request body into an object of the type of the related parameter.\n# The `ATHA::MapQueryString` annotation works similarly, but uses the request's query string instead of its body.\n# Lastly, the `ATHA::MapUploadedFile` annotation works by resolving one or more `AHTTP::UploadedFile` from [AHTTP::Request#files](/HTTP/Request/#Athena::HTTP::Request#files).\n#\n# If the object is also `AVD::Validatable`, any validations defined on it are executed before returning the object.\n# Requires the type of the related parameter to include one or more of:\n#\n# * `ASR::Serializable`\n# * `JSON::Serializable`\n# * `URI::Params::Serializable`\n#\n# ```\n# require \"athena\"\n#\n# # A type representing the structure of the request body.\n# struct UserCreate\n#   # Include some modules to tell Athena this type can be deserialized and validated\n#   include AVD::Validatable\n#   include JSON::Serializable\n#\n#   # Assert the user's name is not blank.\n#   @[Assert::NotBlank]\n#   getter first_name : String\n#\n#   # Assert the user's name is not blank.\n#   @[Assert::NotBlank]\n#   getter last_name : String\n#\n#   # Assert the user's email is not blank and is a valid HTMl5 email.\n#   @[Assert::NotBlank]\n#   @[Assert::Email(:html5)]\n#   getter email : String\n# end\n#\n# class UserController < ATH::Controller\n#   @[ARTA::Post(\"/user\")]\n#   @[ATHA::View(status: :created)]\n#   def new_user(\n#     @[ATHA::MapRequestBody]\n#     user_create : UserCreate,\n#   ) : UserCreate\n#     # Use the provided UserCreate instance to create an actual User DB record.\n#     # For purposes of this example, just return the instance.\n#\n#     user_create\n#   end\n# end\n#\n# ATH.run\n# ```\n#\n# Making a request to the `/user` endpoint with the following payload:\n#\n# ```json\n# {\n#   \"first_name\": \"George\",\n#   \"last_name\": \"\",\n#   \"email\": \"athenaframework.org\"\n# }\n# ```\n#\n# TIP: This resolver also supports `application/x-www-form-urlencoded` payloads.\n#\n# Would return the response:\n#\n# ```json\n# {\n#   \"code\": 422,\n#   \"message\": \"Validation failed\",\n#   \"errors\": [\n#     {\n#       \"property\": \"last_name\",\n#       \"message\": \"This value should not be blank.\",\n#       \"code\": \"0d0c3254-3642-4cb0-9882-46ee5918e6e3\"\n#     },\n#     {\n#       \"property\": \"email\",\n#       \"message\": \"This value is not a valid email address.\",\n#       \"code\": \"ad9d877d-9ad1-4dd7-b77b-e419934e5910\"\n#     }\n#   ]\n# }\n# ```\n#\n# While a valid request would return this response body, with a 201 status code:\n#\n# ```json\n# {\n#   \"first_name\": \"George\",\n#   \"last_name\": \"Dietrich\",\n#   \"email\": \"contact@athenaframework.org\"\n# }\n# ```\nstruct Athena::Framework::Controller::ValueResolvers::RequestBody\n  include Athena::Framework::Controller::ValueResolvers::Interface::Typed(Athena::Serializer::Serializable, JSON::Serializable, URI::Params::Serializable, Athena::HTTP::UploadedFile?, Array(Athena::HTTP::UploadedFile))\n\n  # Enables the `ATHR::RequestBody` resolver for the parameter this annotation is applied to based on the request's body.\n  # See the related resolver documentation for more information.\n  #\n  # ```\n  # class UserController < ATH::Controller\n  #   @[ARTA::Post(\"/user\")]\n  #   def new_user(\n  #     @[ATHA::MapRequestBody]\n  #     user_create : UserCreateDTO,\n  #   ) : UserCreateDTO\n  #     user_create\n  #   end\n  # end\n  # ```\n  #\n  # # Configuration\n  #\n  # ## Optional Arguments\n  #\n  # ### accept_formats\n  #\n  # **Type:** `Array(String)?` **Default:** `nil`\n  #\n  # Allows whitelisting the allowed [request format(s)](/HTTP/Request/#Athena::HTTP::Request::FORMATS).\n  # If the [AHTTP::Request#content_type_format](/HTTP/Request/#Athena::HTTP::Request#content_type_format) is not included in this list, a `AHK::Exception::UnsupportedMediaType` error will be raised.\n  #\n  # ### validation_groups\n  #\n  # **Type:** `Array(String) | AVD::Constraints::GroupSequence | Nil` **Default:** `nil`\n  #\n  # The [validation groups](/Validator/Constraint/#Athena::Validator::Constraint--validation-groups) that should be used when validating the resolved object.\n  configuration ::Athena::Framework::Annotations::MapRequestBody,\n    accept_formats : Array(String)? = nil,\n    validation_groups : Array(String) | AVD::Constraints::GroupSequence | Nil = nil\n\n  # Enables the `ATHR::RequestBody` resolver for the parameter this annotation is applied to based on the request's query string.\n  # See the related resolver documentation for more information.\n  #\n  # ```\n  # class ArticleController < ATH::Controller\n  #   @[ARTA::Get(\"/articles\")]\n  #   def articles(\n  #     @[ATHA::MapQueryString]\n  #     pagination_context : PaginationContext,\n  #   ) : Array(Article)\n  #     # ...\n  #   end\n  # end\n  # ```\n  #\n  # # Configuration\n  #\n  # ## Optional Arguments\n  #\n  # ### validation_groups\n  #\n  # **Type:** `Array(String) | AVD::Constraints::GroupSequence | Nil` **Default:** `nil`\n  #\n  # The [validation groups](/Validator/Constraint/#Athena::Validator::Constraint--validation-groups) that should be used when validating the resolved object.\n  configuration ::Athena::Framework::Annotations::MapQueryString,\n    validation_groups : Array(String) | AVD::Constraints::GroupSequence | Nil = nil\n\n  # Enables the `ATHR::RequestBody` resolver for the parameter this annotation is applied to based on [AHTTP::Request#files](/HTTP/Request/#Athena::HTTP::Request#files),\n  # if the related bundle configuration [is enabled](/Framework/Bundle/Schema/FileUploads/).\n  #\n  # If the type of the parameter this annotation is applied to is `AHTTP::UploadedFile`, then it will attempt to resolve the first file based on the name of the parameter.\n  # This can be customized via the *name* field on the annotation.\n  # If the type is a `Array(AHTTP::UploadedFile)` then all files with that name will be resolved, not just the first.\n  #\n  # When resolving a single file that is not found, and the parameter has a default value or is nilable, then that default value, or `nil`, will be used.\n  # If the parameter does not have a default and is not nilable, then an error response is returned.\n  # When resolving an array of files, then an empty array would be provided.\n  #\n  # ```\n  # class UserController < ATH::Controller\n  #   @[ARTA::Post(\"/avatar\")]\n  #   def avatar(\n  #     @[ATHA::MapUploadedFile(constraints: AVD::Constraints::Image.new)]\n  #     profile_picture : AHTTP::UploadedFile,\n  #   ) : Nil\n  #     # ...\n  #   end\n  # end\n  # ```\n  #\n  # # Configuration\n  #\n  # ## Optional Arguments\n  #\n  # ### name\n  #\n  # **Type:** `String?` **Default:** `nil`\n  #\n  # Use this value to resole the files instead of the name of the parameter the annotation is applied to.\n  #\n  # ### constraints\n  #\n  # **Type:** `AVD::Constraint | Array(AVD::Constraint) | Nil` **Default:** `nil`\n  #\n  # Validate the uploaded file(s) against these constraint(s).\n  # Mostly commonly will be a single `AVD::Constraints::File` or `AVD::Constraints::Image` constraint.\n  configuration ::Athena::Framework::Annotations::MapUploadedFile,\n    constraints : AVD::Constraint | Array(AVD::Constraint) | Nil = nil,\n    name : String? = nil\n\n  def initialize(\n    @serializer : ASR::SerializerInterface,\n    @validator : AVD::Validator::ValidatorInterface,\n    @annotation_resolver : ATH::AnnotationResolver,\n  ); end\n\n  # :inherit:\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata)\n    validation_groups = nil\n    constraints = nil\n    parameter_annotations = @annotation_resolver.action_parameter_annotations request, parameter.name\n\n    object = if configuration = parameter_annotations[ATHA::MapQueryString]?\n               validation_groups = configuration.validation_groups\n               self.map_query_string request, parameter, configuration\n             elsif configuration = parameter_annotations[ATHA::MapRequestBody]?\n               validation_groups = configuration.validation_groups\n               self.map_request_body request, parameter, configuration\n             elsif configuration = parameter_annotations[ATHA::MapUploadedFile]?\n               constraints = configuration.constraints\n               self.map_uploaded_file request, parameter, configuration\n             else\n               return\n             end\n\n    if object && (object.is_a?(AVD::Validatable) || !constraints.nil?)\n      if object.is_a?(Array) && constraints && !constraints.is_a?(AVD::Constraints::All)\n        constraints = AVD::Constraints::All.new constraints\n      end\n\n      errors = @validator.validate object, constraints: constraints, groups: validation_groups\n      raise AVD::Exception::ValidationFailed.new errors unless errors.empty?\n    end\n\n    object\n  end\n\n  private def map_query_string(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata, configuration : ATHA::MapQueryStringConfiguration)\n    return unless query = request.query\n    return if query.nil? && (parameter.nilable? || parameter.has_default?)\n\n    self.deserialize_form query, parameter.type\n  rescue ex : URI::SerializableError\n    raise AHK::Exception::UnprocessableEntity.new ex.message.not_nil!, cause: ex\n  rescue ex : URI::Error\n    raise AHK::Exception::BadRequest.new \"Malformed www form data payload.\", cause: ex\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity:\n  private def map_request_body(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata, configuration : ATHA::MapRequestBodyConfiguration)\n    if !(body = request.body) || body.peek.try &.empty?\n      raise AHK::Exception::BadRequest.new \"Request does not have a body.\"\n    end\n\n    format = request.content_type_format\n\n    if (accept_formats = configuration.accept_formats) && !accept_formats.includes? format\n      raise AHK::Exception::UnsupportedMediaType.new \"Unsupported format, expects one of: '#{accept_formats.join(\", \")}', but got '#{format}'.\"\n    end\n\n    # We have to use separate deserialization methods with the case such that a type that includes multiple modules is handled as expected.\n    case format\n    when \"form\"\n      self.deserialize_form body, parameter.type\n    when \"json\"\n      self.deserialize_json body, parameter.type\n    else\n      raise AHK::Exception::UnsupportedMediaType.new \"Unsupported format.\"\n    end\n  rescue ex : JSON::SerializableError\n    # JSON::Serializable seems to sometimes re-raise parse exceptions as `JSON::SerializableError`,\n    # so we handle those first based on the cause.\n    case cause = ex.cause\n    when JSON::ParseException\n      raise AHK::Exception::BadRequest.new \"Malformed JSON payload.\", cause: cause\n    else\n      raise AHK::Exception::UnprocessableEntity.new ex.message.not_nil!\n    end\n  rescue ex : JSON::ParseException | ASR::Exception::DeserializationException\n    # Otherwise if it really is a `ParseException` we can be assured it's just malformed\n    raise AHK::Exception::BadRequest.new \"Malformed JSON payload.\", cause: ex\n  rescue ex : URI::SerializableError\n    raise AHK::Exception::UnprocessableEntity.new ex.message.not_nil!, cause: ex\n  rescue ex : URI::Error\n    raise AHK::Exception::BadRequest.new \"Malformed www form data payload.\", cause: ex\n  end\n\n  private def deserialize_json(body : IO, klass : ASR::Serializable.class)\n    @serializer.deserialize klass, body, :json\n  end\n\n  private def deserialize_json(body : IO, klass : JSON::Serializable.class)\n    klass.from_json body\n  end\n\n  private def deserialize_json(body : IO, klass : _) : Nil\n  end\n\n  private def deserialize_form(body : IO, klass : URI::Params::Serializable.class)\n    klass.from_www_form body.gets_to_end\n  end\n\n  private def deserialize_form(body : String, klass : URI::Params::Serializable.class)\n    klass.from_www_form body\n  end\n\n  private def deserialize_form(body : IO | String, klass : _)\n  end\n\n  private def map_uploaded_file(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata, configuration : ATHA::MapUploadedFileConfiguration) : AHTTP::UploadedFile | Enumerable(AHTTP::UploadedFile) | Nil\n    files = request.files[configuration.name || parameter.name]? || [] of AHTTP::UploadedFile\n\n    if files.empty? && (parameter.nilable? || parameter.has_default?)\n      return\n    end\n\n    if parameter.instance_of?(Array(AHTTP::UploadedFile))\n      return files\n    end\n\n    files.first?\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/controller/value_resolvers/time.cr",
    "content": "@[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 105}])]\n# Attempts to parse a date(time) string into a `::Time` instance.\n#\n# Optionally allows specifying the *format* and *location* to use when parsing the string via the `ATHA::MapTime` annotation.\n# If no *format* is specified, defaults to [RFC 3339](https://crystal-lang.org/api/Time.html#parse_rfc3339%28time:String%29-class-method).\n# Defaults to `UTC` if no *location* is specified with the annotation.\n#\n# Raises an `AHK::Exception::BadRequest` if the date(time) string could not be parsed.\n#\n# TIP: The format can be anything supported via [Time::Format](https://crystal-lang.org/api/Time/Format.html).\n#\n# ```\n# require \"athena\"\n#\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(path: \"/event/{start_time}/{end_time}\")]\n#   def event(\n#     @[ATHA::MapTime(\"%F\", location: Time::Location.load(\"Europe/Berlin\"))]\n#     start_time : Time,\n#     end_time : Time,\n#   ) : Nil\n#     start_time # => 2020-04-07 00:00:00.0 +02:00 Europe/Berlin\n#     end_time   # => 2020-04-08 12:34:56.0 UTC\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /event/2020-04-07/2020-04-08T12:34:56Z\n# ```\nstruct Athena::Framework::Controller::ValueResolvers::Time\n  include Athena::Framework::Controller::ValueResolvers::Interface\n\n  # Allows customizing the time format and/or location used to parse the string datetime as part of the `ATHR::Time` resolver.\n  # See the related resolver documentation for more information.\n  configuration ::Athena::Framework::Annotations::MapTime, format : String? = nil, location : ::Time::Location = ::Time::Location::UTC\n\n  def initialize(\n    @annotation_resolver : ATH::AnnotationResolver,\n  ); end\n\n  # :inherit:\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : ::Time?\n    return unless parameter.instance_of? ::Time\n\n    if value = request.attributes.get? parameter.name, ::Time?\n      return value\n    end\n\n    return unless value = request.attributes.get? parameter.name, String?\n\n    parameter_annotations = @annotation_resolver.action_parameter_annotations(request, parameter.name)\n\n    if !(configuration = parameter_annotations[ATHA::MapTime]?) || !(format = configuration.format)\n      return ::Time.parse_rfc3339(value)\n    end\n\n    ::Time.parse value, format, configuration.location\n  rescue ex : ::Time::Format::Error\n    raise AHK::Exception::BadRequest.new \"Invalid date(time) for parameter '#{parameter.name}'.\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/controller/value_resolvers/uuid.cr",
    "content": "require \"uuid\"\n\n@[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 105}])]\n# Handles resolving a [UUID](https://crystal-lang.org/api/UUID.html) from a string value that is stored in the request's [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes).\n#\n# ```\n# require \"athena\"\n#\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\"/uuid/{uuid}\")]\n#   def get_uuid(uuid : UUID) : String\n#     \"Version: #{uuid.version} - Variant: #{uuid.variant}\"\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /uuid/b115c7a5-0a13-47b4-b4ac-55b3e2686946 # => \"Version: V4 - Variant: RFC4122\"\n# ```\n#\n# TIP: Checkout `ART::Requirement` for an easy way to restrict/validate the version of the UUID that is allowed.\nstruct Athena::Framework::Controller::ValueResolvers::UUID\n  include Athena::Framework::Controller::ValueResolvers::Interface\n\n  # :inherit:\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : ::UUID?\n    return unless parameter.instance_of? ::UUID # TODO: Test making this not nil\n    return unless value = request.attributes.get? parameter.name, String\n\n    ::UUID.parse?(value) || raise AHK::Exception::BadRequest.new \"Parameter '#{parameter.name}' with value '#{value}' is not a valid 'UUID'.\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/controller.cr",
    "content": "# The core of any framework is routing; how a route is tied to an action.\n# Athena takes an annotation based approach; an annotation, such as `ARTA::Get` is applied to an instance method of a controller class, which will be executed when that endpoint receives a request.\n#\n# Additional annotations also exist for defining [query parameters](/getting_started/routing/#query-parameters).\n#\n# Child controllers must inherit from `ATH::Controller` (or an abstract child of it). Each request gets its own instance of the controller to better allow for DI via `Athena::DependencyInjection`.\n#\n# A route action can either return an `AHTTP::Response`, or some other type. If an `AHTTP::Response` is returned, then it is used directly. Otherwise an `AHK::Events::View` is emitted to convert\n# the action result into an `AHTTP::Response`. By default, `ATH::Listeners::View` will JSON encode the value if it is not handled earlier by another listener.\n#\n# ### Example\n# The following controller shows examples of the various routing features of Athena. `ATH::Controller` also defines various macro DSLs, such as `ATH::Controller.get` to make defining routes\n# seem more Sinatra/Kemal like. See the documentation on the macros for more details.\n#\n# ```\n# require \"athena\"\n# require \"mime\"\n#\n# # The `ARTA::Route` annotation can also be applied to a controller class.\n# # This can be useful for applying a common path prefix, defaults, requirements,\n# # etc. to all actions in the controller.\n# @[ARTA::Route(path: \"/athena\")]\n# class TestController < ATH::Controller\n#   # A GET endpoint returning an `AHTTP::Response`.\n#   # Can be used to return raw data, such as HTML or CSS etc, in a one-off manor.\n#   @[ARTA::Get(path: \"/index\")]\n#   def index : AHTTP::Response\n#     AHTTP::Response.new \"<h1>Welcome to my website!</h1>\", headers: ::HTTP::Headers{\"content-type\" => MIME.from_extension(\".html\")}\n#   end\n#\n#   # A GET endpoint returning an `AHTTP::StreamedResponse`.\n#   # Can be used to stream the response content to the client;\n#   # useful if the content is too large to fit into memory.\n#   @[ARTA::Get(path: \"/users\")]\n#   def users : AHTTP::Response\n#     AHTTP::StreamedResponse.new headers: ::HTTP::Headers{\"content-type\" => \"application/json; charset=utf-8\"} do |io|\n#       User.all.to_json io\n#     end\n#   end\n#\n#   # A GET endpoint with no parameters returning a `String`.\n#   #\n#   # Action return type restrictions are required.\n#   @[ARTA::Get(\"/me\")]\n#   def get_me : String\n#     \"Jim\"\n#   end\n#\n#   # A GET endpoint with no parameters returning `Nil`.\n#   # `Nil` return types are returned with a status\n#   # of 204 no content\n#   @[ARTA::Get(\"/no_content\")]\n#   def get_no_content : Nil\n#     # Do stuff\n#   end\n#\n#   # A GET endpoint with two `Int32` parameters returning an `Int32`.\n#   #\n#   # The parameters of a route _MUST_ match the parameters of the action.\n#   # Type restrictions on action parameters are required.\n#   @[ARTA::Get(\"/add/{val1}/{val2}\")]\n#   def add(val1 : Int32, val2 : Int32) : Int32\n#     val1 + val2\n#   end\n#\n#   # A GET endpoint with a required trailing slash, a `String` route parameter,\n#   # and a required string query parameter; returning a `String`.\n#   #\n#   # Athena treats non `GET`/`HEAD` routes with a trailing slash as unique\n#   # E.g. `POST /foo/bar/` versus `POST /foo/bar`.\n#   # Be sure to keep you routes consistent!\n#   #\n#   # A non-nilable type denotes it as required. If the parameter is not supplied,\n#   # and no default value is assigned, an `AHK::Exception::BadRequest` exception is raised.\n#   @[ARTA::Get(\"/event/{event_name}/\")]\n#   def event_time(event_name : String, @[ATHA::MapQueryParameter] time : String) : String\n#     \"#{event_name} occurred at #{time}\"\n#   end\n#\n#   # A GET endpoint with an optional query parameter and optional path parameter\n#   # with a default value; returning a `NamedTuple(user_id : Int32?, page : Int32)`.\n#   #\n#   # A nilable type denotes it as optional.\n#   # If the parameter is not supplied (or could not be converted),\n#   # and no default value is assigned, it is `nil`.\n#   @[ARTA::Get(\"/events/{page}\")]\n#   def events(@[ATHA::MapQueryParameter] user_id : Int32?, page : Int32 = 1) : NamedTuple(user_id: Int32?, page: Int32)\n#     {user_id: user_id, page: page}\n#   end\n#\n#   # A GET endpoint with route parameter requirements.\n#   # The parameter must match the supplied Regex or this route will not be matched.\n#   #\n#   # This feature can allow multiple routes to exist with parameters in the same location,\n#   # but with different requirements.\n#   @[ARTA::Get(\"/time/{time}/\", requirements: {\"time\" => /\\d{2}:\\d{2}:\\d{2}/})]\n#   def get_constraint(time : String) : String\n#     time\n#   end\n#\n#   # A POST endpoint with a route parameter and accessing the request body; returning a `Bool`.\n#   #\n#   # It is recommended to use `ATHR::RequestBody` to allow passing an actual object representing the data\n#   # to the route's action; however the raw request body can be accessed by typing an action argument as `AHTTP::Request`.\n#   @[ARTA::Post(\"/test/{expected}\")]\n#   def post_body(expected : String, request : AHTTP::Request) : Bool\n#     expected == request.body.try &.gets_to_end\n#   end\n#\n#   # An endpoint may also have more than one route annotation applied to it.\n#   # This can be useful in allowing for a route to support multiple aliases.\n#   @[ARTA::Get(\"/users/{id}\")]\n#   @[ARTA::Get(\"/people/{id}\")]\n#   def get_user(id : Int64) : User\n#     # Fetch the user\n#     user = ...\n#\n#     user\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /athena/index                    # => <h1>Welcome to my website!</h1>\n# # GET /athena/users                    # => [{\"id\":1,...},...]\n# # GET /athena/wakeup/17                # => Morning, Allison it is currently 2020-02-01 18:38:12 UTC.\n# # GET /athena/me                       # => \"Jim\"\n# # GET /athena/add/50/25                # => 75\n# # GET /athena/event/foobar?time=1:1:1  # => \"foobar occurred at 1:1:1\"\n# # GET /athena/event/foobar/?time=1:1:1 # => \"foobar occurred at 1:1:1\"\n# # GET /athena/events                   # => {\"user_id\":null,\"page\":1}\n# # GET /athena/events/17?user_id=19     # => {\"user_id\":19,\"page\":17}\n# # GET /athena/time/12:45:30            # => \"12:45:30\"\n# # GET /athena/time/12:aa:30            # => 404 not found\n# # GET /athena/no_content               # => 204 no content\n# # GET /athena/users/19                 # => {\"user_id\":19}\n# # GET /athena/people/19                # => {\"user_id\":19}\n# # POST /athena/test/foo, body: \"foo\"   # => true\n# ```\nabstract class Athena::Framework::Controller\n  macro inherited\n    private CONTROLLER_ACTION_METHODS = [] of {String, String}\n\n    macro method_added(m)\n      \\{%\n         if (m.annotation(ARTA::Get) || m.annotation(ARTA::Post) || m.annotation(ARTA::Put) || m.annotation(ARTA::Delete) || m.annotation(ARTA::Patch) || m.annotation(ARTA::Link) || m.annotation(ARTA::Unlink) || m.annotation(ARTA::Head) || m.annotation(ARTA::Route))\n           if CONTROLLER_ACTION_METHODS.includes?({@type.name.id, m.name.id})\n             m.raise \"A controller action named '##{m.name}' already exists within '#{@type.name}'.\"\n           end\n\n           CONTROLLER_ACTION_METHODS << {@type.name.id, m.name.id}\n         end\n      %}\n    end\n  end\n\n  # Generates a URL to the provided *route* with the provided *params*.\n  #\n  # See `ART::Generator::Interface#generate`.\n  def generate_url(route : String, params : Hash(String, _) = Hash(String, String?).new, reference_type : ART::Generator::ReferenceType = :absolute_path) : String\n    # TODO: Make this type leverage a service locator for these common types.\n    ADI.container.router.generate route, params.transform_values(&.to_s.as(String?)), reference_type\n  end\n\n  # Generates a URL to the provided *route* with the provided *params*.\n  #\n  # See `ART::Generator::Interface#generate`.\n  def generate_url(route : String, reference_type : ART::Generator::ReferenceType = :absolute_path, **params)\n    self.generate_url route, params.to_h.transform_keys(&.to_s), reference_type\n  end\n\n  # Returns an `AHTTP::RedirectResponse` to the provided *route* with the provided *params*.\n  #\n  # ```\n  # require \"athena\"\n  #\n  # class ExampleController < ATH::Controller\n  #   # Define a route to redirect to, explicitly naming this route `add`.\n  #   # The default route name is controller + method down snake-cased; e.x. `example_controller_add`.\n  #   @[ARTA::Get(\"/add/{value1}/{value2}\", name: \"add\")]\n  #   def add(value1 : Int32, value2 : Int32, negative : Bool = false) : Int32\n  #     sum = value1 + value2\n  #     negative ? -sum : sum\n  #   end\n  #\n  #   # Define a route that redirects to the `add` route with fixed parameters.\n  #   @[ARTA::Get(\"/\")]\n  #   def redirect : AHTTP::RedirectResponse\n  #     self.redirect_to_route \"add\", {\"value1\" => 8, \"value2\" => 2}\n  #   end\n  # end\n  #\n  # ATH.run\n  #\n  # # GET / # => 10\n  # ```\n  def redirect_to_route(route : String, params : Hash(String, _) = Hash(String, String?).new, status : ::HTTP::Status = :found) : AHTTP::RedirectResponse\n    self.redirect self.generate_url(route, params), status\n  end\n\n  # Returns an `AHTTP::RedirectResponse` to the provided *route* with the provided *params*.\n  #\n  # ```\n  # require \"athena\"\n  #\n  # class ExampleController < ATH::Controller\n  #   # Define a route to redirect to, explicitly naming this route `add`.\n  #   # The default route name is controller + method down snake-cased; e.x. `example_controller_add`.\n  #   @[ARTA::Get(\"/add/{value1}/{value2}\", name: \"add\")]\n  #   def add(value1 : Int32, value2 : Int32, negative : Bool = false) : Int32\n  #     sum = value1 + value2\n  #     negative ? -sum : sum\n  #   end\n  #\n  #   # Define a route that redirects to the `add` route with fixed parameters.\n  #   @[ARTA::Get(\"/\")]\n  #   def redirect : AHTTP::RedirectResponse\n  #     self.redirect_to_route \"add\", value1: 8, value2: 2\n  #   end\n  # end\n  #\n  # ATH.run\n  #\n  # # GET / # => 10\n  # ```\n  def redirect_to_route(route : String, status : ::HTTP::Status = :found, **params) : AHTTP::RedirectResponse\n    self.redirect_to_route route, params.to_h.transform_keys(&.to_s.as(String)), status\n  end\n\n  # Returns an `AHTTP::RedirectResponse` to the provided *url*, optionally with the provided *status*.\n  #\n  # ```\n  # class ExampleController < ATH::Controller\n  #   @[ARTA::Get(\"redirect/google\")]\n  #   def redirect_to_google : AHTTP::RedirectResponse\n  #     self.redirect \"https://google.com\"\n  #   end\n  # end\n  # ```\n  def redirect(url : String | Path, status : ::HTTP::Status = ::HTTP::Status::FOUND) : AHTTP::RedirectResponse\n    AHTTP::RedirectResponse.new url, status\n  end\n\n  # Returns an `ATH::View` that'll redirect to the provided *url*, optionally with the provided *status* and *headers*.\n  #\n  # Is essentially the same as `#redirect`, but invokes the [view](/getting_started/middleware#4-view-event) layer.\n  def redirect_view(url : String, status : ::HTTP::Status = ::HTTP::Status::FOUND, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ATH::View\n    ATH::View.create_redirect url, status, headers\n  end\n\n  # Returns an `ATH::View` that'll redirect to the provided *route*, optionally with the provided *params*, *status*, and *headers*.\n  #\n  # Is essentially the same as `#redirect_to_route`, but invokes the [view](/getting_started/middleware#4-view-event) layer.\n  def route_redirect_view(route : String, params : Hash(String, _) = Hash(String, String?).new, status : ::HTTP::Status = ::HTTP::Status::CREATED, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ATH::View\n    ATH::View.create_route_redirect route, params\n  end\n\n  # Returns an `ATH::View` with the provided *data*, and optionally *status* and *headers*.\n  #\n  # ```\n  # @[ARTA::Get(\"/{name}\")]\n  # def say_hello(name : String) : ATH::View(NamedTuple(greeting: String))\n  #   self.view({greeting: \"Hello #{name}\"}, :im_a_teapot)\n  # end\n  # ```\n  def view(data = nil, status : ::HTTP::Status? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ATH::View\n    ATH::View.new data, status, headers\n  end\n\n  {% begin %}\n    {% for method in [\"DELETE\", \"GET\", \"HEAD\", \"PATCH\", \"POST\", \"PUT\", \"LINK\", \"UNLINK\"] %}\n      # Helper DSL macro for creating `{{method.id}}` actions.\n      #\n      # The first argument is the path that the action should handle; which maps to path on the HTTP method annotation.\n      # The second argument is a variable amount of arguments with a syntax similar to Crystal's `record`.\n      # There are also a few optional named arguments that map to the corresponding field on the HTTP method annotation.\n      #\n      # The macro simply defines a method based on the options passed to it. Additional annotations, such as for query params\n      # or a param converter can simply be added on top of the macro.\n      #\n      # ### Optional Named Arguments\n      # - `return_type` - The return type to set for the action. Defaults to `String` if not provided.\n      # - `constraints` - Any constraints that should be applied to the route.\n      #\n      # ### Example\n      #\n      # ```\n      # class ExampleController < ATH::Controller\n      #   {{method.downcase.id}} \"values/{value1<\\\\d+>}/{value2<\\\\d+\\\\.\\\\d+>}\", value1 : Int32, value2 : Float64 do\n      #     \"Value1: #{value1} - Value2: #{value2}\"\n      #   end\n      # end\n      # ```\n      macro {{method.downcase.id}}(path, *args, **named_args, &)\n        @[ARTA::{{method.capitalize.id}}(path: \\{{path}})]\n        def {{method.downcase.id}}_\\{{path.gsub(/\\W/, \"_\").id}}(\\{{args.splat}}) : \\{{named_args[:return_type] || String}}\n          \\{{yield}}\n        end\n      end\n    {% end %}\n  {% end %}\n\n  # Renders a template.\n  #\n  # Uses `ECR` to render the *template*, creating an `AHTTP::Response` with its rendered content and adding a `text/html` `content-type` header.\n  #\n  # The response can be modified further before returning it if needed.\n  #\n  # Variables used within the template must be defined within the action's body manually if they are not provided within the action's arguments.\n  #\n  # ```\n  # # greeting.ecr\n  # Greetings, <%= name %>!\n  #\n  # # example_controller.cr\n  # class ExampleController < ATH::Controller\n  #   @[ARTA::Get(\"/{name}\")]\n  #   def greet(name : String) : AHTTP::Response\n  #     render \"greeting.ecr\"\n  #   end\n  # end\n  #\n  # ATH.run\n  #\n  # # GET /Fred # => Greetings, Fred!\n  # ```\n  macro render(template)\n    Athena::HTTP::Response.new ECR.render({{template}}), headers: ::HTTP::Headers{\"content-type\" => \"text/html\"}\n  end\n\n  # Renders a template within a layout.\n  # ```\n  # # layout.ecr\n  # <h1>Content:</h1> <%= content -%>\n  #\n  # # greeting.ecr\n  # Greetings, <%= name %>!\n  #\n  # # example_controller.cr\n  # class ExampleController < ATH::Controller\n  #   @[ARTA::Get(\"/{name}\")]\n  #   def greet(name : String) : AHTTP::Response\n  #     render \"greeting.ecr\", \"layout.ecr\"\n  #   end\n  # end\n  #\n  # ATH.run\n  #\n  # # GET /Fred # => <h1>Content:</h1> Greetings, Fred!\n  # ```\n  macro render(template, layout)\n    content = ECR.render {{template}}\n    {{@type}}.render {{layout}}\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/clock.cr",
    "content": "@[ADI::Register(name: \"clock\", factory: \"create\")]\n@[ADI::AsAlias(ACLK::Interface)]\n# :nodoc:\nclass Athena::Clock\n  # :nodoc:\n  #\n  # There a better way to handle this?\n  # By default the `ACLK::Interface` causes some infinite recursion due to this service being aliased to the interface\n  def self.create : self\n    new\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/console/application.cr",
    "content": "@[ADI::Register(public: true, name: \"athena_console_application\")]\n# Entrypoint for the `Athena::Console` integration.\n#\n# ```\n# # Require your code\n# require \"./main\"\n#\n# # Run the application\n# ATH.run_console\n# ```\n#\n# Checkout the [Getting Started](/getting_started/commands) docs for more information.\nclass Athena::Framework::Console::Application < ACON::Application\n  protected def initialize(\n    command_loader : ACON::Loader::Interface? = nil,\n    event_dispatcher : ACTR::EventDispatcher::Interface? = nil,\n    eager_commands : Enumerable(ACON::Command)? = nil,\n  )\n    super \"Athena\", ATH::VERSION\n\n    self.command_loader = command_loader\n    # TODO: set event dispatcher when that's implemented in the console component.\n\n    eager_commands.try &.each do |cmd|\n      self.add cmd\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/console/compiler_passes/register_commands.cr",
    "content": "# Contains types related to the `Athena::Console` integration.\nmodule Athena::Framework::Console::CompilerPasses::RegisterCommands\n  TAG = \"athena.console.command\"\n\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          command_map = {} of Nil => Nil\n          command_refs = {} of Nil => Nil\n\n          # Services that are not configured via the annotation so must be registered eagerly.\n          eager_service_ids = [] of Nil\n\n          (TAG_HASH[ATH::Console::Command::TAG] || [] of Nil).each do |(service_id, _attributes)|\n            metadata = SERVICE_HASH[service_id]\n\n            # TODO: Any benefit in allowing commands to be configured via tags instead of the annotation?\n\n            ann = metadata[\"class\"].annotation ACONA::AsCommand\n\n            if ann == nil\n              SERVICE_HASH[public_service_id = \"_#{service_id.id}_public\"] = metadata\n              service_id = public_service_id\n              eager_service_ids << service_id.id\n            else\n              name = ann[0] || ann[:name]\n\n              unless name\n                ann.raise \"Console command '#{metadata[\"class\"]}' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.\"\n              end\n\n              aliases = name.split '|'\n              aliases = aliases + (ann[\"aliases\"] || [] of Nil)\n\n              if ann[\"hidden\"] && \"\" != aliases[0]\n                aliases.unshift \"\"\n              end\n\n              command_name = aliases[0]\n              aliases = aliases[1..]\n\n              if is_hidden = \"\" == command_name\n                command_name = aliases[0]\n                aliases = aliases[1..]\n              end\n\n              command_map[command_name] = metadata[\"class\"]\n              command_refs[metadata[\"class\"]] = service_id\n\n              aliases.each do |a|\n                command_map[a] = metadata[\"class\"]\n              end\n\n              SERVICE_HASH[lazy_service_id = \"_#{service_id.id}_lazy\"] = {\n                class:               ACON::Commands::Lazy,\n                tags:                [] of Nil,\n                generics:            [] of Nil,\n                calls:               [] of Nil,\n                public:              false,\n                referenced_services: [service_id],\n                parameters:          {\n                  name:        {value: command_name},\n                  aliases:     {value: \"#{aliases} of String\".id},\n                  description: {value: ann[\"description\"] || \"\"},\n                  hidden:      {value: is_hidden},\n                  command:     {value: \"->{ #{service_id.id}.as(ACON::Command) }\".id},\n                },\n              }\n\n              command_refs[metadata[\"class\"]] = lazy_service_id\n            end\n          end\n\n          SERVICE_HASH[loader_id = \"athena_console_command_loader_container\"] = {\n            class:               \"Athena::Framework::Console::ContainerCommandLoaderLocator\",\n            tags:                [] of Nil,\n            generics:            [] of Nil,\n            calls:               [] of Nil,\n            public:              false,\n            referenced_services: command_refs.values,\n            parameters:          {\n              container: {value: \"self\".id},\n            },\n          }\n\n          SERVICE_HASH[command_loader_service_id = \"athena_console_command_loader\"] = {\n            class:      Athena::Framework::Console::ContainerCommandLoader,\n            tags:       [] of Nil,\n            generics:   [] of Nil,\n            calls:      [] of Nil,\n            public:     false,\n            parameters: {\n              command_map: {value: \"#{command_map} of String => ACON::Command.class\".id},\n              loader:      {value: loader_id.id},\n            },\n          }\n\n          SERVICE_HASH[\"athena_console_application\"][\"parameters\"][\"command_loader\"][\"value\"] = command_loader_service_id.id\n          SERVICE_HASH[\"athena_console_application\"][\"parameters\"][\"eager_commands\"][\"value\"] = \"#{eager_service_ids} of ACON::Command\".id\n          # Track eager commands as referenced services to ensure their getters are generated\n          eager_service_ids.each do |sid|\n            SERVICE_HASH[\"athena_console_application\"][\"referenced_services\"] << sid\n          end\n        %}\n\n        # :nodoc:\n        #\n        # TODO: Define some more generic way to create these\n        struct ::Athena::Framework::Console::ContainerCommandLoaderLocator\n          def initialize(@container : ::ADI::ServiceContainer); end\n\n          {% for service_type, service_id in command_refs %}\n            def get(service : {{service_type}}.class) : ACON::Command\n              @container.{{service_id.id}}\n            end\n          {% end %}\n\n          def get(service) : ACON::Command\n            {% begin %}\n              case service\n              {% for service_type, service_id in command_refs %}\n                when {{service_type}} then @container.{{service_id.id}}\n              {% end %}\n              else\n                raise \"BUG: Couldn't find correct service.\"\n              end\n            {% end %}\n          end\n        end\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/console/container_command_loader.cr",
    "content": "# :nodoc:\nclass Athena::Framework::Console::ContainerCommandLoader\n  include Athena::Console::Loader::Interface\n\n  @command_map : Hash(String, ACON::Command.class)\n\n  def initialize(\n    @command_map : Hash(String, ACON::Command.class),\n    @loader : ATH::Console::ContainerCommandLoaderLocator,\n  ); end\n\n  # :inherit:\n  def get(name : String) : ACON::Command\n    if !self.has? name\n      raise ACON::Exception::CommandNotFound.new \"Command '#{name}' does not exist.\"\n    end\n\n    @loader.get @command_map[name]\n  end\n\n  # :inherit:\n  def has?(name : String) : Bool\n    @command_map.has_key? name\n  end\n\n  # :inherit:\n  def names : Array(String)\n    @command_map.keys\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/console/descriptor/descriptor.cr",
    "content": "# :nodoc:\nabstract class Athena::Framework::Console::Descriptor\n  include Athena::Console::Descriptor::Interface\n\n  getter! output : ACON::Output::Interface\n\n  abstract class FrameworkContext < ACON::Descriptor::Context\n    getter output : ACON::Output::Interface\n\n    def initialize(\n      @output : ACON::Output::Interface,\n      format : String = \"txt\",\n      raw_text : Bool = false,\n      raw_output : Bool? = nil,\n      namespace : String? = nil,\n      total_width : Int32? = nil,\n      short : Bool = false,\n    )\n      super format, raw_text, raw_output, namespace, total_width, short\n    end\n  end\n\n  class RoutingContext < FrameworkContext\n    getter name : String?\n    getter? show_controllers : Bool\n\n    def initialize(\n      output : ACON::Output::Interface,\n      @name : String? = nil,\n      @show_controllers : Bool = false,\n      format : String = \"txt\",\n      raw_text : Bool = false,\n      raw_output : Bool? = nil,\n      namespace : String? = nil,\n      total_width : Int32? = nil,\n      short : Bool = false,\n    )\n      super output, format, raw_text, raw_output, namespace, total_width, short\n    end\n  end\n\n  class EventDispatcherContext < FrameworkContext\n    getter event_class : ACTR::EventDispatcher::Event.class | Nil\n    getter event_classes : Array(ACTR::EventDispatcher::Event.class)?\n\n    def initialize(\n      output : ACON::Output::Interface,\n      @event_class : ACTR::EventDispatcher::Event.class | Nil = nil,\n      @event_classes : Array(ACTR::EventDispatcher::Event.class)? = nil,\n      format : String = \"txt\",\n      raw_text : Bool = false,\n      raw_output : Bool? = nil,\n      namespace : String? = nil,\n      total_width : Int32? = nil,\n      short : Bool = false,\n    )\n      super output, format, raw_text, raw_output, namespace, total_width, short\n    end\n  end\n\n  def describe(output : ACON::Output::Interface, object : _, context : ACON::Descriptor::Context) : Nil\n    @output = output\n\n    self.describe object, context\n  end\n\n  protected abstract def describe(route : ART::Route, context : RoutingContext) : Nil\n  protected abstract def describe(routes : ART::RouteCollection, context : RoutingContext) : Nil\n  protected abstract def describe(event_dispatcher : AED::EventDispatcherInterface, context : EventDispatcherContext) : Nil\n\n  protected def describe(obj : _, context : ACON::Descriptor::Context) : Nil\n    raise \"BUG: Failed to describe #{obj}\"\n  end\n\n  protected def write(content : String, decorated : Bool = false) : Nil\n    self.output.print content, output_type: decorated ? Athena::Console::Output::Type::NORMAL : Athena::Console::Output::Type::RAW\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/console/descriptor/text.cr",
    "content": "# :nodoc:\nclass Athena::Framework::Console::Descriptor::Text < Athena::Framework::Console::Descriptor\n  protected def describe(route : ART::Route, context : ATH::Console::Descriptor::RoutingContext) : Nil\n    defaults = route.defaults\n\n    headers = %w(Property Value)\n    rows = [\n      [\"Route Name\", context.name],\n      [\"Path\", route.path],\n      [\"Path Regex\", route.compile.regex.source],\n      [\"Host\", (host = route.host) ? host : \"ANY\"],\n      [\"Host Regex\", !route.host.nil? ? route.compile.host_regex.try(&.source) : \"\"],\n      [\"Scheme\", (schemes = route.schemes) ? schemes.join('|') : \"ANY\"],\n      [\"Methods\", (methods = route.methods) ? methods.join('|') : \"ANY\"],\n      [\"Requirements\", !(requirements = route.requirements).empty? ? self.format_router_config(requirements) : \"NO CUSTOM\"],\n      [\"Class\", route.class.to_s],\n      [\"Defaults\", self.format_router_config(defaults.to_h)],\n    ]\n\n    ACON::Helper::Table.new(self.output)\n      .headers(headers)\n      .rows(rows)\n      .render\n  end\n\n  protected def describe(routes : ART::RouteCollection, context : ATH::Console::Descriptor::RoutingContext) : Nil\n    show_controllers = context.show_controllers?\n\n    headers = %w(Name Method Scheme Host Path)\n    headers << \"Controller\" if show_controllers\n\n    rows = routes.map do |name, route|\n      controller = route.default \"_controller\", String\n\n      row = [\n        name,\n        (methods = route.methods) ? methods.join('|') : \"ANY\",\n        (schemes = route.schemes) ? schemes.join('|') : \"ANY\",\n        (host = route.host) ? host : \"ANY\",\n        route.path,\n      ]\n\n      if show_controllers && controller\n        row << controller\n      end\n\n      row\n    end\n\n    if output = context.output\n      output.as(ACON::Style::Athena).table(headers, rows)\n    else\n      ACON::Helper::Table.new(self.output)\n        .headers(headers)\n        .rows(rows)\n        .render\n    end\n  end\n\n  private def format_router_config(config : Hash) : String\n    return \"\" if config.empty?\n\n    # Sort hash via key.\n    config = config\n      .to_a\n      .sort! { |(n1, _), (n2, _)| n1 <=> n2 }\n      .to_h\n\n    String.build do |io|\n      config.each do |key, value|\n        io << '\\n'\n        io << key\n        io << ':' << ' '\n        io << case value\n        when Regex then value.source\n        else\n          value\n        end\n      end\n    end.strip\n  end\n\n  protected def describe(event_dispatcher : AED::EventDispatcherInterface, context : EventDispatcherContext) : Nil\n    # TODO: Support specific dispatcher services\n\n    title = \"Registered Listeners\"\n\n    if event = context.event_class\n      title = \"#{title} for the #{event} Event\"\n      listeners = event_dispatcher.listeners event\n    else\n      title = \"#{title} Grouped by Event\"\n      listeners = if events = context.event_classes\n                    events.each_with_object({} of ACTR::EventDispatcher::Event.class => Array(AED::Callable)) do |ec, map|\n                      map[ec] = event_dispatcher.listeners ec\n                    end\n                  else\n                    event_dispatcher.listeners\n                  end\n    end\n\n    output = context.output.as ACON::Style::Athena\n\n    output.title title\n\n    self.render_event_listener_table output, event_dispatcher, listeners\n  end\n\n  private def render_event_listener_table(\n    output : ACON::Style::Athena,\n    event_dispatcher : AED::EventDispatcherInterface,\n    event_listeners : Hash(ACTR::EventDispatcher::Event.class, Array(AED::Callable)),\n  ) : Nil\n    sorted_listeners = event_listeners\n      .to_a\n      .sort! { |(n1, _), (n2, _)| n1.to_s <=> n2.to_s }\n      .to_h\n\n    sorted_listeners.each do |event, el|\n      output.section \"#{event} event\"\n      self.render_event_listener_table output, event_dispatcher, el\n    end\n  end\n\n  private def render_event_listener_table(\n    output : ACON::Style::Athena,\n    event_dispatcher : AED::EventDispatcherInterface,\n    event_listeners : Array(AED::Callable),\n  ) : Nil\n    table_headers = %w(Order Callable Priority)\n    table_rows = [] of Array(String | Int32)\n\n    event_listeners.each_with_index do |callable, idx|\n      table_rows << [\"##{idx + 1}\", callable.name, callable.priority]\n    end\n\n    output.table table_headers, table_rows\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/console/helper/descriptor_helper.cr",
    "content": "# :nodoc:\nclass Athena::Framework::Console::Helper::Descriptor < Athena::Console::Helper::Descriptor\n  def initialize\n    self.register \"txt\", ATH::Console::Descriptor::Text.new\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/console.cr",
    "content": "# :nodoc:\nmodule Athena::Framework::Console::Command\n  TAG = \"athena.console.command\"\nend\n\nrequire \"./console/**\"\n\n@[ADI::Autoconfigure(tags: [ATH::Console::Command::TAG])]\nabstract class ACON::Command; end\n"
  },
  {
    "path": "src/components/framework/src/ext/event_dispatcher.cr",
    "content": "@[ADI::Register(name: \"event_dispatcher\", public: true)]\n@[ADI::AsAlias(AED::EventDispatcherInterface)]\n@[ADI::AsAlias(ACTR::EventDispatcher::Interface)]\nclass AED::EventDispatcher; end\n\n# :nodoc:\nmodule Athena::Framework::EventDispatcher::CompilerPasses::RegisterEventListenersPass\n  macro included\n    macro finished\n      {% verbatim do %}\n        {%\n          event_dispatcher_service = SERVICE_HASH[\"event_dispatcher\"]\n\n          SERVICE_HASH.each do |service_id, definition|\n            # Include types with the annotation applied to class methods for proper error handling.\n            if (klass = definition[\"class\"]).is_a?(TypeNode) &&\n               (\n                 klass.class.methods.any?(&.annotation AEDA::AsEventListener) ||\n                 klass.methods.any?(&.annotation AEDA::AsEventListener)\n               )\n              event_dispatcher_service[\"calls\"] << {\"listener\", {service_id.id}}\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/http.cr",
    "content": "@[ADI::Register(name: \"request_store\", public: true)]\nclass Athena::HTTP::RequestStore; end\n"
  },
  {
    "path": "src/components/framework/src/ext/http_kernel.cr",
    "content": "@[ADI::Register(name: \"athena_http_kernel\", public: true)]\nstruct Athena::HTTPKernel::HTTPKernel; end\n\n@[ADI::Register]\nstruct Athena::HTTPKernel::Listeners::Routing; end\n\n@[ADI::Register]\nstruct Athena::HTTPKernel::Listeners::Error; end\n\n@[ADI::Register]\n@[ADI::AsAlias]\nclass Athena::HTTPKernel::ActionResolver; end\n\nADI.bind value_resolvers : Array(Athena::HTTPKernel::Controller::ValueResolvers::Interface), \"!athena.controller.value_resolver\"\n\n@[ADI::Register(name: \"parameter_resolver_request_attribute\", tags: [{name: ATHR::Interface::TAG, priority: 100}])]\nstruct Athena::HTTPKernel::Controller::ValueResolvers::RequestAttribute; end\n\n@[ADI::Register(name: \"parameter_resolver_request\", tags: [{name: ATHR::Interface::TAG, priority: 50}])]\nstruct Athena::HTTPKernel::Controller::ValueResolvers::Request; end\n\n@[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: -100}])]\nstruct Athena::HTTPKernel::Controller::ValueResolvers::DefaultValue; end\n\n@[ADI::Register]\n@[ADI::AsAlias]\nstruct Athena::HTTPKernel::Controller::ArgumentResolver; end\n\n@[ADI::Register(_debug: \"%framework.debug%\")]\n@[ADI::AsAlias(AHK::ErrorRendererInterface)]\nstruct Athena::HTTPKernel::ErrorRenderer; end\n"
  },
  {
    "path": "src/components/framework/src/ext/routing/annotation_route_loader.cr",
    "content": "# :nodoc:\n#\n# Loads and caches a `ART::RouteCollection` from `ART::Controllers` as well as a mapping of route names to `ATH::Action`s.\nmodule Athena::Framework::Routing::AnnotationRouteLoader\n  class_getter route_collection : ART::RouteCollection do\n    populate_collection\n  end\n\n  # :nodoc:\n  #\n  # Abstracts the logic to create the `ART::RouteCollection` such that it can be tested in a more performant way.\n  macro populate_collection(base = nil)\n    collection = ART::RouteCollection.new\n\n    {% begin %}\n      {% for klass, c_idx in (base ? [base.resolve] : ATH::Controller.all_subclasses.reject &.abstract?) %}\n        # Define global vars derived from the controller data.\n        {%\n          methods = klass.methods.select { |m| m.annotation(ARTA::Get) || m.annotation(ARTA::Post) || m.annotation(ARTA::Put) || m.annotation(ARTA::Delete) || m.annotation(ARTA::Patch) || m.annotation(ARTA::Link) || m.annotation(ARTA::Unlink) || m.annotation(ARTA::Head) || m.annotation(ARTA::Route) }\n          class_actions = klass.class.methods.select { |m| m.annotation(ARTA::Get) || m.annotation(ARTA::Post) || m.annotation(ARTA::Put) || m.annotation(ARTA::Delete) || m.annotation(ARTA::Patch) || m.annotation(ARTA::Link) || m.annotation(ARTA::Unlink) || m.annotation(ARTA::Head) || m.annotation(ARTA::Route) }\n\n          # Raise compile time error if a route is defined as a class method.\n          unless class_actions.empty?\n            class_actions.first.raise \"Routes can only be defined as instance methods. Did you mean '#{klass.name}##{class_actions.first.name}'?\"\n          end\n\n          globals = {\n            path:            \"\",\n            localized_paths: nil,\n            requirements:    {} of Nil => Nil,\n            defaults:        {} of Nil => Nil,\n            schemes:         [] of Nil,\n            methods:         [] of Nil,\n            host:            nil,\n            condition:       nil,\n            name:            nil,\n            priority:        0,\n          }\n\n          if controller_ann = klass.annotation ARTA::Route\n            if (ann_path = (controller_ann[:path] || controller_ann[0])) && ann_path.is_a? HashLiteral\n              globals[:localized_paths] = ann_path\n            elsif ann_path != nil\n              ann_path = ann_path.resolve if ann_path.is_a?(Path)\n\n              if !ann_path.is_a?(StringLiteral) && !ann_path.is_a?(HashLiteral)\n                ann_path.raise \"Route action '#{klass.name}' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its 'ARTA::Route#path' field, but got a '#{ann_path.class_name.id}'.\"\n              end\n              globals[:path] = ann_path\n            end\n\n            if (value = controller_ann[:defaults]) != nil\n              unless value.is_a? HashLiteral\n                value.raise \"Route action '#{klass.name}' expects a 'HashLiteral(StringLiteral, _)' for its 'ARTA::Route#defaults' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:defaults] = value\n            end\n\n            if (value = controller_ann[:locale]) != nil\n              unless value.is_a? StringLiteral\n                value.raise \"Route action '#{klass.name}' expects a 'StringLiteral' for its 'ARTA::Route#locale' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:defaults][\"_locale\"] = value\n            end\n\n            if (value = controller_ann[:format]) != nil\n              unless value.is_a? StringLiteral\n                value.raise \"Route action '#{klass.name}' expects a 'StringLiteral' for its 'ARTA::Route#format' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:defaults][\"_format\"] = value\n            end\n\n            if controller_ann[:stateless] != nil\n              value = controller_ann[:stateless]\n\n              unless value.is_a? BoolLiteral\n                value.raise \"Route action '#{klass.name}' expects a 'BoolLiteral' for its 'ARTA::Route#stateless' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:defaults][\"_stateless\"] = value\n            end\n\n            if (value = controller_ann[:name]) != nil\n              unless value.is_a? StringLiteral\n                value.raise \"Route action '#{klass.name}' expects a 'StringLiteral' for its 'ARTA::Route#name' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:name] = value\n            end\n\n            if (value = controller_ann[:requirements]) != nil\n              unless value.is_a? HashLiteral\n                value.raise \"Route action '#{klass.name}' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its 'ARTA::Route#requirements' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:requirements] = value\n            end\n\n            if (value = controller_ann[:schemes]) != nil\n              if !value.is_a?(StringLiteral) && !value.is_a?(ArrayLiteral) && !value.is_a?(TupleLiteral)\n                value.raise \"Route action '#{klass.name}' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#schemes' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:schemes] = value\n            end\n\n            if (value = controller_ann[:methods]) != nil\n              if !value.is_a?(StringLiteral) && !value.is_a?(ArrayLiteral) && !value.is_a?(TupleLiteral)\n                value.raise \"Route action '#{klass.name}' expects a 'StringLiteral | Enumerable(StringLiteral)' for its 'ARTA::Route#methods' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:methods] = value\n            end\n\n            if (value = controller_ann[:host]) != nil\n              if !value.is_a?(StringLiteral) && !value.is_a?(RegexLiteral)\n                value.raise \"Route action '#{klass.name}' expects a 'StringLiteral | RegexLiteral' for its 'ARTA::Route#host' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:host] = value\n            end\n\n            if (value = controller_ann[:condition]) != nil\n              if !value.is_a?(Call) || value.receiver.resolve != ART::Route::Condition\n                value.raise \"Route action '#{klass.name}' expects an 'ART::Route::Condition' for its 'ARTA::Route#condition' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:condition] = value\n            end\n\n            if (value = controller_ann[:priority]) != nil\n              if !value.is_a?(NumberLiteral)\n                value.raise \"Route action '#{klass.name}' expects a 'NumberLiteral' for its 'ARTA::Route#priority' field, but got a '#{value.class_name.id}'.\"\n              end\n\n              globals[:priority] = value\n            end\n          end\n        %}\n\n        %collection{c_idx} = ART::RouteCollection.new\n\n        # Build out the routes\n        {% for m in methods %}\n          # Raise compile time error if the action doesn't have a return type.\n          {% if m.return_type.is_a? Nop %}\n            {% m.raise \"Route action return type must be set for '#{klass.name}##{m.name}'.\" %}\n          {% end %}\n\n          # Determine routes that this method should handle\n          {%\n            routes_for_method = [] of Nil # Tuple(Annotation, Array(String))\n\n            # Numerical index used to make the route name more unique if no more explicit name was provided\n            default_route_index = 0\n\n            m.annotations(ARTA::Route).each do |a|\n              methods = a[:methods] || [] of Nil\n\n              if !methods.is_a?(StringLiteral) && !methods.is_a?(ArrayLiteral) && !methods.is_a?(TupleLiteral)\n                a.raise \"Route action '#{klass.name}##{m.name}' expects a 'StringLiteral | ArrayLiteral | TupleLiteral' for its 'ARTA::Route#methods' field, but got a '#{methods.class_name.id}'.\"\n              end\n\n              methods = methods.is_a?(StringLiteral) ? [methods] : methods\n\n              routes_for_method << {a, methods}\n            end\n\n            # Set the route_def and method(s) based on annotation.\n            {\n              {ARTA::Get, [\"GET\"]},\n              {ARTA::Post, [\"POST\"]},\n              {ARTA::Put, [\"PUT\"]},\n              {ARTA::Patch, [\"PATCH\"]},\n              {ARTA::Delete, [\"DELETE\"]},\n              {ARTA::Link, [\"LINK\"]},\n              {ARTA::Unlink, [\"UNLINK\"]},\n              {ARTA::Head, [\"HEAD\"]},\n            }.each do |(t, methods)|\n              m.annotations(t).each do |a|\n                routes_for_method << {a, methods}\n              end\n            end\n          %}\n\n          {% for route_info in routes_for_method %}\n            {%\n              route_def = route_info[0]\n              methods = route_info[1]\n            %}\n\n            {%\n              parameters = [] of Nil\n              annotation_configurations = {} of Nil => Nil\n              parameter_annotation_configuration_map = {} of Nil => Nil\n\n              # Logic for creating the `ATH::Action` instances:\n\n              arg_types = m.args.map &.restriction\n              arg_names = m.args.map &.name.stringify\n\n              # Disallow `methods` field when _NOT_ using `ARTA::Route`.\n              if !m.annotation(ARTA::Route) && route_def[:methods] != nil\n                route_def.raise \"Route action '#{klass.name}##{m.name}' cannot change the required methods when _NOT_ using the 'ARTA::Route' annotation.\"\n              end\n\n              # Process controller action parameters.\n              m.args.each do |arg|\n                parameter_annotation_configurations = {} of Nil => Nil\n\n                # Process custom annotation types\n                ADI::CUSTOM_ANNOTATIONS.each do |ann_class|\n                  annotations = [] of Nil\n\n                  (arg.annotations ann_class.resolve).each do |ann|\n                    # See if this annotation relates to a typed resolver interface,\n                    # checking the namespace the configuration annotation was defined in for the interface.\n                    resolver = ATH::Controller::ValueResolvers::Interface::ANNOTATION_RESOLVER_MAP[ann_class.id]\n\n                    if resolver && (interface = resolver.resolve.ancestors.find &.<=(ATHR::Interface::Typed))\n                      supported_types = interface.type_vars.first.type_vars\n\n                      unless supported_types.any? { |t| arg.restriction.resolve <= t.resolve }\n                        arg.raise %(The annotation '#{ann}' cannot be applied to '#{klass.name}##{m.name}:#{arg.name} : #{arg.restriction}' since the '#{resolver}' resolver only supports parameters of type '#{supported_types.join(\" | \").id}'.)\n                      end\n                    end\n\n                    annotations << \"#{ann_class}Configuration.new(#{ann.args.empty? ? \"\".id : \"#{ann.args.splat},\".id}#{ann.named_args.double_splat})\".id\n                  end\n\n                  parameter_annotation_configurations[ann_class.resolve] = \"(#{annotations} of ADI::AnnotationConfigurations::ConfigurationBase)\".id unless annotations.empty?\n                end\n\n                if arg.restriction.is_a? Nop\n                  arg.raise \"Route action parameter '#{klass.name}##{m.name}:#{arg.name}' must have a type restriction.\"\n                end\n\n                parameters << %(AHK::Controller::ParameterMetadata(#{arg.restriction}).new(\n                  #{arg.name.stringify},\n                  #{!arg.default_value.is_a? Nop},\n                  #{arg.default_value.is_a?(Nop) ? nil : arg.default_value},\n                )).id\n\n                parameter_annotation_configuration_map[arg.name.stringify] = %(ADI::AnnotationConfigurations.new(#{parameter_annotation_configurations} of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase))).id\n              end\n\n              # Process custom annotation types\n              ADI::CUSTOM_ANNOTATIONS.each do |ann_class|\n                ann_class = ann_class.resolve\n                annotations = [] of Nil\n\n                (klass.annotations(ann_class) + m.annotations(ann_class)).each do |ann|\n                  annotations << \"#{ann_class}Configuration.new(#{ann.args.empty? ? \"\".id : \"#{ann.args.splat},\".id}#{ann.named_args.double_splat})\".id\n                end\n\n                annotation_configurations[ann_class] = \"(#{annotations} of ADI::AnnotationConfigurations::ConfigurationBase)\".id unless annotations.empty?\n              end\n\n              # Setup the `ATH::Action` and set the `_controller` default so future logic knows which method it should handle it by default.\n              route_name = if (value = route_def[:name]) != nil\n                             if !value.is_a?(StringLiteral)\n                               value.raise \"Route action '#{klass.name}##{m.name}' expects a 'StringLiteral' for its '#{route_def.name}#name' field, but got a '#{value.class_name.id}'.\"\n                             end\n\n                             value\n                           else\n                             # MyApp::UserController#new_user # => my_app_user_controller_new_user\n                             name = \"#{klass.name.stringify.split(\"::\").join('_').underscore.downcase.id}_#{m.name.id}\"\n\n                             # If more than one route is making use of this action, make the name unique\n                             if default_route_index > 0\n                               name += \"_#{default_route_index}\"\n                             end\n\n                             default_route_index += 1\n\n                             name\n                           end\n\n              if globals_name = globals[:name]\n                route_name = \"#{globals_name.id}_#{route_name.id}\"\n              end\n\n              globals[:defaults][\"_controller\"] = action_name = \"#{klass.name}##{m.name}\"\n            %}\n\n            {% unless annotation_configurations.empty? %}\n              ::ATH::AnnotationResolver::ACTION_ANNOTATIONS[{{action_name}}] = ADI::AnnotationConfigurations.new({{annotation_configurations}} of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase))\n            {% end %}\n\n            {% unless parameter_annotation_configuration_map.empty? %}\n              ::ATH::AnnotationResolver::ACTION_PARAMETER_ANNOTATIONS[{{action_name}}] = {{parameter_annotation_configuration_map}} of String => ADI::AnnotationConfigurations\n            {% end %}\n\n            %action{action_name} = AHK::Action.new(\n              action: Proc({{arg_types.empty? ? \"typeof(Tuple.new)\".id : \"Tuple(#{arg_types.splat})\".id}}, {{m.return_type}}).new do |arguments|\n                # If the controller is not registered as a service, simply new one up, otherwise fetch it directly from the SC.\n                {% if klass.annotation(ADI::Register) %}\n                  %instance = ADI.container.get(::{{klass.id}})\n                {% else %}\n                  %instance = ::{{klass.id}}.new\n                {% end %}\n\n                %instance.{{m.name.id}} *arguments\n              end,\n              parameters: {{parameters.empty? ? \"Tuple.new\".id : \"{#{parameters.splat}}\".id}},\n              _return_type: {{m.return_type}},\n            )\n\n            {%\n              paths = {} of Nil => Nil\n              defaults = {} of Nil => Nil\n              requirements = {} of Nil => Nil\n\n              # Resolve `ART::Route` data from the route annotation and globals.\n\n              globals[:defaults].each { |k, v| defaults[k] = v }\n              globals[:requirements].each { |k, v| requirements[k] = v }\n\n              if (value = route_def[:locale]) != nil\n                unless value.is_a? StringLiteral\n                  value.raise \"Route action '#{klass.name}##{m.name}' expects a 'StringLiteral' for its '#{route_def.name}#locale' field, but got a '#{value.class_name.id}'.\"\n                end\n\n                defaults[\"_locale\"] = value\n              end\n\n              if (value = route_def[:format]) != nil\n                unless value.is_a? StringLiteral\n                  value.raise \"Route action '#{klass.name}##{m.name}' expects a 'StringLiteral' for its '#{route_def.name}#format' field, but got a '#{value.class_name.id}'.\"\n                end\n\n                defaults[\"_format\"] = value\n              end\n\n              if route_def[:stateless] != nil\n                value = route_def[:stateless]\n\n                unless value.is_a? BoolLiteral\n                  value.raise \"Route action '#{klass.name}##{m.name}' expects a 'BoolLiteral' for its '#{route_def.name}#stateless' field, but got a '#{value.class_name.id}'.\"\n                end\n\n                defaults[\"_stateless\"] = value\n              end\n              if ann_defaults = route_def[:defaults]\n                unless ann_defaults.is_a? HashLiteral\n                  ann_defaults.raise \"Route action '#{klass.name}##{m.name}' expects a 'HashLiteral(StringLiteral, _)' for its '#{route_def.name}#defaults' field, but got a '#{ann_defaults.class_name.id}'.\"\n                end\n\n                ann_defaults.each { |k, v| defaults[k] = v }\n              end\n\n              if ann_requirements = route_def[:requirements]\n                unless ann_requirements.is_a? HashLiteral\n                  ann_requirements.raise \"Route action '#{klass.name}##{m.name}' expects a 'HashLiteral(StringLiteral, StringLiteral | RegexLiteral)' for its '#{route_def.name}#requirements' field, but got a '#{ann_requirements.class_name.id}'.\"\n                end\n\n                ann_requirements.each do |k, v|\n                  requirements[k] = if v.is_a?(StringLiteral) || v.is_a?(RegexLiteral)\n                                      v\n                                    else\n                                      \"#{v}.to_s\".id\n                                    end\n                end\n              end\n\n              if (value = route_def[:host]) != nil\n                if !value.is_a?(StringLiteral) && !value.is_a?(RegexLiteral)\n                  value.raise \"Route action '#{klass.name}##{m.name}' expects a 'StringLiteral | RegexLiteral' for its '#{route_def.name}#host' field, but got a '#{value.class_name.id}'.\"\n                end\n              end\n\n              if (value = route_def[:priority]) != nil\n                if !value.is_a?(NumberLiteral)\n                  value.raise \"Route action '#{klass.name}##{m.name}' expects a 'NumberLiteral' for its '#{route_def.name}#priority' field, but got a '#{value.class_name.id}'.\"\n                end\n              end\n\n              if (value = route_def[:condition]) != nil\n                if !value.is_a?(Call) || value.receiver.resolve != ART::Route::Condition\n                  value.raise \"Route action '#{klass.name}##{m.name}' expects an 'ART::Route::Condition' for its '#{route_def.name}#condition' field, but got a '#{value.class_name.id}'.\"\n                end\n              end\n\n              schemes = (globals[:schemes] + (route_def[:schemes] || [] of Nil)).uniq\n              methods = (globals[:methods] + methods).uniq\n              priority = route_def[:priority] || globals[:priority]\n              host = route_def[:host] || globals[:host]\n\n              condition = route_def[:condition] || globals[:condition]\n              priority = route_def[:priority] || globals[:priority]\n\n              unless path = route_def[:localized_paths] || route_def[0] || route_def[:path]\n                m.raise \"Route action '#{klass.name}##{m.name}' is missing its path.\"\n              end\n\n              path = path.resolve if path.is_a?(Path)\n\n              if !path.is_a?(StringLiteral) && !path.is_a?(HashLiteral)\n                path.raise \"Route action '#{klass.name}##{m.name}' expects a 'StringLiteral | HashLiteral(StringLiteral, StringLiteral)' for its '#{route_def.name}#path' field, but got a '#{path.class_name.id}'.\"\n              end\n\n              prefix = globals[:localized_paths] || globals[:path]\n\n              # Process path/prefix values to a hash of paths that should be created.\n              if path.is_a? HashLiteral\n                if !prefix.is_a? HashLiteral\n                  path.each do |locale, locale_path|\n                    paths[locale] = if !locale_path.empty? && !locale_path.starts_with?('/')\n                                      \"#{prefix.id}/#{locale_path.id}\"\n                                    else\n                                      \"#{prefix.id}#{locale_path.id}\"\n                                    end\n                  end\n                elsif !(missing = prefix.keys.reject { |k| path[k] }).empty?\n                  m.raise \"Route action '#{klass.name}##{m.name}' is missing paths for locale(s) '#{missing.join(\",\").id}'.\"\n                else\n                  path.each do |locale, locale_path|\n                    if prefix[locale] == nil\n                      m.raise \"Route action '#{klass.name}##{m.name}' is missing a corresponding route prefix for the '#{locale.id}' locale.\"\n                    end\n\n                    paths[locale] = if !locale_path.empty? && !locale_path.starts_with?('/')\n                                      \"#{prefix[locale].id}/#{locale_path.id}\"\n                                    else\n                                      \"#{prefix[locale].id}#{locale_path.id}\"\n                                    end\n                  end\n                end\n              elsif prefix.is_a? HashLiteral\n                prefix.each do |locale, locale_prefix|\n                  paths[locale] = if !path.empty? && !path.starts_with?('/')\n                                    \"#{locale_prefix.id}/#{path.id}\"\n                                  else\n                                    \"#{locale_prefix.id}#{path.id}\"\n                                  end\n                end\n              else\n                # Normalize non empty route specific paths so they always start with `/`.\n                paths[\"_default\"] = if !path.empty? && !path.starts_with?('/')\n                                      \"#{prefix.id}/#{path.id}\"\n                                    else\n                                      \"#{prefix.id}#{path.id}\"\n                                    end\n              end\n\n              m.args.each do |arg|\n                paths.each do |_, pth|\n                  pth.split('/').each do |p|\n                    if p.starts_with?(\"{#{arg.name}\") && defaults[arg.name.stringify] == nil && !arg.default_value.is_a?(Nop) && p =~ /\\{\\w+(?:<.*?>)?\\}/\n                      defaults[arg.name.stringify] = arg.default_value\n                    end\n                  end\n                end\n              end\n            %}\n\n            {% for locale, path in paths %}\n              {%\n                r_name = route_name\n                r_defaults = defaults\n                r_requirements = requirements\n\n                if locale != \"_default\"\n                  r_defaults[\"_locale\"] = locale\n                  r_requirements[\"_locale\"] = \"Regex.escape(#{locale})\".id\n                  r_defaults[\"_canonical_route\"] = r_name\n                  r_name = \"#{route_name.id}.#{locale.id}\"\n                end\n              %}\n\n              %route{r_name} = ART::Route.new(\n                path: {{path}},\n                defaults: {{r_defaults.empty? ? \"ART::Parameters.new\".id : r_defaults}},\n                requirements: {{r_requirements}} of String => Regex | String,\n                host: {{host}},\n                schemes: {{schemes.empty? ? nil : schemes}},\n                methods: {{methods.empty? ? nil : methods}},\n                condition: {{condition}}\n              )\n              %route{r_name}.set_default \"_action\", %action{action_name}\n\n              %collection{c_idx}.add({{r_name}}, %route{r_name}, {{priority}})\n            {% end %}\n          {% end %}\n        {% end %}\n\n        collection.add %collection{c_idx}\n      {% end %}\n    {% end %}\n\n    ART.compile collection\n\n    collection\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/routing/redirectable_url_matcher.cr",
    "content": "# :nodoc:\nclass Athena::Framework::Routing::RedirectableURLMatcher < Athena::Routing::Matcher::URLMatcher\n  include ART::Matcher::RedirectableURLMatcherInterface\n\n  def redirect(path : String, route : String, scheme : String? = nil) : ART::Parameters?\n    ART::Parameters.new({\n      \"_controller\" => \"Athena::Framework::Controller::Redirect#redirect_url\",\n      \"_action\"     => AHK::Action.new(\n        action: Proc(Tuple(AHTTP::Request, String, Bool, String?, Int32?, Int32?, Bool), AHTTP::RedirectResponse).new do |arguments|\n          ADI.container.get(Athena::Framework::Controller::Redirect).redirect_url *arguments\n        end,\n        parameters: {\n          AHK::Controller::ParameterMetadata(AHTTP::Request).new(\"request\"),\n          AHK::Controller::ParameterMetadata(String).new(\"path\"),\n          AHK::Controller::ParameterMetadata(Bool).new(\"permanent\", true, false),\n          AHK::Controller::ParameterMetadata(String?).new(\"scheme\", true, nil),\n          AHK::Controller::ParameterMetadata(Int32?).new(\"http_port\", true, nil),\n          AHK::Controller::ParameterMetadata(Int32?).new(\"https_port\", true, nil),\n          AHK::Controller::ParameterMetadata(Bool).new(\"keep_request_method\", true, false),\n        },\n        _return_type: AHTTP::RedirectResponse,\n      ),\n      \"_route\"    => route,\n      \"path\"      => path,\n      \"permanent\" => \"true\",\n      \"scheme\"    => scheme,\n    })\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/routing/router.cr",
    "content": "# :nodoc:\n@[ADI::AsAlias(ART::Generator::Interface)]\n@[ADI::AsAlias(ART::Matcher::URLMatcherInterface)]\n@[ADI::AsAlias(ART::RouterInterface)]\n@[ADI::AsAlias(ART::RequestContextAwareInterface)]\n@[ADI::AsAlias(\"router\", public: true)]\nclass Athena::Framework::Routing::Router < Athena::Routing::Router\n  getter matcher : ART::Matcher::URLMatcherInterface do\n    ATH::Routing::RedirectableURLMatcher.new(@context)\n  end\n\n  def initialize(\n    default_locale : String? = nil,\n    strict_requirements : Bool? = true,\n    request_context : ART::RequestContext? = nil,\n  )\n    super(\n      ATH::Routing::AnnotationRouteLoader.route_collection,\n      default_locale,\n      strict_requirements,\n      request_context,\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/routing.cr",
    "content": "require \"athena-routing\"\n\n# :nodoc:\nmodule Athena::Framework::Routing; end\n\nrequire \"./routing/*\"\n"
  },
  {
    "path": "src/components/framework/src/ext/serializer.cr",
    "content": "require \"athena-serializer\"\n\n@[ADI::Register]\n@[ADI::AsAlias]\nstruct Athena::Serializer::Serializer; end\n\n@[ADI::Register]\nstruct Athena::Serializer::Navigators::NavigatorFactory; end\n\n@[ADI::Register]\nstruct Athena::Serializer::InstantiateObjectConstructor; end\n"
  },
  {
    "path": "src/components/framework/src/ext/validator/validation_failed_exception.cr",
    "content": "# Wraps an `AVD::Violation::ConstraintViolationListInterface` as an `AHK::Exception::UnprocessableEntity`; exposing the violations within the response body.\nclass Athena::Validator::Exception::ValidationFailed < AHK::Exception::UnprocessableEntity\n  getter violations : Athena::Validator::Violation::ConstraintViolationListInterface\n\n  def initialize(violations : AVD::Violation::ConstraintViolationInterface | AVD::Violation::ConstraintViolationListInterface, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    if violations.is_a? AVD::Violation::ConstraintViolationInterface\n      violations = AVD::Violation::ConstraintViolationList.new [violations]\n    end\n\n    @violations = violations\n\n    super \"Validation failed\", cause, headers\n  end\n\n  def to_json(builder : JSON::Builder) : Nil\n    builder.object do\n      builder.field \"code\", self.status_code\n      builder.field \"message\", @message\n\n      builder.field \"errors\" do\n        @violations.to_json builder\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/ext/validator.cr",
    "content": "require \"athena-validator\"\n\nrequire \"./validator/validation_failed_exception\"\n\n@[ADI::Register]\n@[ADI::AsAlias]\nclass Athena::Validator::Validator::RecursiveValidator; end\n\n@[ADI::Autoconfigure(tags: [\"athena.validator.constraint_validator\"])]\nabstract class AVD::ServiceConstraintValidator; end\n\n@[ADI::Register]\nstruct Athena::Validator::ConstraintValidatorFactory; end\n\nADI.bind constraint_validators : Array(AVD::ServiceConstraintValidator), \"!athena.validator.constraint_validator\"\n"
  },
  {
    "path": "src/components/framework/src/file_parser.cr",
    "content": "# :nodoc:\nclass Athena::Framework::FileParser\n  # Store the tmp uploaded paths to use to validate `AHTTP::UploadedFile`s.\n  @uploaded_files : Set(String) = Set(String).new\n\n  protected class_getter default_temp_dir : String do\n    temp_dir = Path.new Dir.tempdir, \"athena\"\n    Dir.mkdir_p temp_dir\n    temp_dir.to_s\n  end\n\n  def initialize(\n    temp_dir : String?,\n    @max_uploads : Int32,\n    @max_file_size : Int64,\n  )\n    @temp_dir = temp_dir || self.class.default_temp_dir\n  end\n\n  def parse(request : AHTTP::Request) : Nil\n    uploaded_file_count = 0\n\n    ::HTTP::FormData.parse(request.request) do |part|\n      case filename = part.filename\n      when \"\" then next\n      when .nil?\n        request.attributes.set part.name, part.body.gets_to_end, String\n        next\n      end\n\n      next if uploaded_file_count >= @max_uploads\n\n      status : AHTTP::UploadedFile::Status = :ok\n\n      size : Int64? = 0\n\n      temp_file = ::File.tempfile \"file_upload.\", nil, dir: @temp_dir do |file|\n        size = self.copy_with_max part.body, file\n      end\n\n      file_path = temp_file.path\n\n      if size.nil?\n        status = :size_limit_exceeded\n        temp_file.delete\n        file_path = \"\"\n      end\n\n      if status.ok?\n        @uploaded_files << file_path\n      end\n\n      request.files[part.name] << AHTTP::UploadedFile.new file_path, filename, part.headers[\"content-type\"]?, status\n      uploaded_file_count += 1\n    end\n  end\n\n  def clear : Nil\n    @uploaded_files.each do |tmp_uploaded_file_path|\n      ::File.delete? tmp_uploaded_file_path\n    rescue ex\n      Log.warn(exception: ex) { \"Failed to cleanup temp file upload: '#{tmp_uploaded_file_path}'.\" }\n    end\n  end\n\n  protected def uploaded_file?(path : String) : Bool\n    @uploaded_files.includes? path\n  end\n\n  # Based off of https://github.com/crystal-lang/crystal/blob/54022594f84040c976634863ce5fac1b31a68048/src/io.cr#L1173\n  # but returns `nil` if more bytes than allowed were written.\n  private def copy_with_max(src : IO, dest : IO) : Int64?\n    buffer = uninitialized UInt8[IO::DEFAULT_BUFFER_SIZE]\n    count = 0_i64\n    while (len = src.read(buffer.to_slice).to_i32) > 0\n      dest.write buffer.to_slice[0, len]\n      count &+= len\n      return if count > @max_file_size\n    end\n    count\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/listeners/cors.cr",
    "content": "# Supports [Cross-Origin Resource Sharing](https://enable-cors.org) (CORS) requests.\n#\n# Handles CORS preflight `OPTIONS` requests as well as adding CORS headers to each response.\n# See `ATH::Bundle::Schema::Cors` for information on configuring the listener.\n#\n# TIP: Set your [Log::Severity](https://crystal-lang.org/api/Log/Severity.html) to `TRACE` to help debug the listener.\nstruct Athena::Framework::Listeners::CORS\n  # :nodoc:\n  struct Config\n    getter? allow_credentials : Bool\n    getter allow_origin : Array(String | Regex)\n    getter allow_headers : Array(String)\n    getter allow_methods : Array(String)\n    getter expose_headers : Array(String)\n    getter max_age : Int32\n\n    def initialize(\n      @allow_credentials : Bool = false,\n      allow_origin : Array(String | Regex) = Array(String | Regex).new,\n      @allow_headers : Array(String) = [] of String,\n      @allow_methods : Array(String) = Athena::Framework::Listeners::CORS::SAFELISTED_METHODS,\n      @expose_headers : Array(String) = [] of String,\n      @max_age : Int32 = 0,\n    )\n      @allow_origin = allow_origin.map &.as String | Regex\n    end\n  end\n\n  # The [CORS-safelisted request-headers](https://fetch.spec.whatwg.org/#cors-safelisted-request-header).\n  SAFELISTED_HEADERS = [\n    \"accept\",\n    \"accept-language\",\n    \"content-language\",\n    \"content-type\",\n    \"origin\",\n  ]\n\n  # The [CORS-safelisted methods](https://fetch.spec.whatwg.org/#cors-safelisted-method).\n  SAFELISTED_METHODS = [\n    \"GET\",\n    \"POST\",\n    \"HEAD\",\n  ]\n\n  # :nodoc:\n  ALLOW_SET_ORIGIN = \"athena.routing.cors.allow_set_origin\"\n\n  private WILDCARD = \"*\"\n\n  private REQUEST_METHOD_HEADER    = \"access-control-request-method\"\n  private REQUEST_HEADERS_HEADER   = \"access-control-request-headers\"\n  private ALLOW_CREDENTIALS_HEADER = \"access-control-allow-credentials\"\n  private ALLOW_HEADERS_HEADER     = \"access-control-allow-headers\"\n  private ALLOW_METHODS_HEADER     = \"access-control-allow-methods\"\n  private ALLOW_ORIGIN_HEADER      = \"access-control-allow-origin\"\n  private EXPOSE_HEADERS_HEADER    = \"access-control-expose-headers\"\n  private MAX_AGE_HEADER           = \"access-control-max-age\"\n\n  protected def initialize(@config : ATH::Listeners::CORS::Config = ATH::Listeners::CORS::Config.new); end\n\n  @[AEDA::AsEventListener(priority: 250)]\n  def on_request(event : AHK::Events::Request) : Nil\n    request = event.request\n\n    # Return early if there is no configuration.\n    unless @config\n      Log.trace { \"#{self.class.name} is unconfigured, skipping CORS.\" }\n\n      return\n    end\n\n    # Return early if not a CORS request.\n    # TODO: optimize this by also checking if origin matches the request's host.\n    unless request.headers.has_key? \"origin\"\n      Log.trace { \"Request does not have an 'origin' header, skipping CORS.\" }\n\n      return\n    end\n\n    # If the request is a preflight, return the proper response.\n    if request.method == \"OPTIONS\" && request.headers.has_key? REQUEST_METHOD_HEADER\n      Log.trace { \"Request is a pre-flight request, creating response.\" }\n\n      return event.response = set_preflight_response event.request\n    end\n\n    unless check_origin event.request\n      Log.trace { \"Origin check failed.\" }\n\n      return\n    end\n\n    Log.trace { \"Origin is allowed, proceed with adding CORS response headers.\" }\n\n    event.request.attributes.set ALLOW_SET_ORIGIN, true, Bool\n  end\n\n  @[AEDA::AsEventListener]\n  def on_response(event : AHK::Events::Response) : Nil\n    # Return early if the request shouldn't have CORS set.\n    unless event.request.attributes.get? ALLOW_SET_ORIGIN\n      Log.trace { \"The origin is not allowed, skipping CORS response headers.\" }\n\n      return\n    end\n\n    # Return early if there is no configuration.\n    unless @config\n      Log.trace { \"#{self.class.name} is unconfigured, skipping CORS response headers.\" }\n\n      return\n    end\n\n    origin = event.request.headers[\"origin\"]\n\n    Log.trace { \"Setting '#{ALLOW_ORIGIN_HEADER}' to '#{origin}'.\" }\n\n    # TODO: Add a configuration option to allow setting this explicitly\n    event.response.headers[ALLOW_ORIGIN_HEADER] = origin\n\n    if @config.allow_credentials?\n      Log.trace { \"Setting '#{ALLOW_CREDENTIALS_HEADER}' to 'true'.\" }\n\n      event.response.headers[ALLOW_CREDENTIALS_HEADER] = \"true\"\n    end\n\n    unless @config.expose_headers.empty?\n      headers = @config.expose_headers.join(\", \")\n\n      Log.trace { \"Settings '#{EXPOSE_HEADERS_HEADER}' to '#{headers}'.\" }\n\n      event.response.headers[EXPOSE_HEADERS_HEADER] = headers\n    end\n  end\n\n  # Configures the given *response* for CORS preflight\n  private def set_preflight_response(request : AHTTP::Request) : AHTTP::Response\n    response = AHTTP::Response.new\n    response.headers[\"vary\"] = \"origin\"\n\n    if @config.allow_credentials?\n      Log.trace { \"Setting '#{ALLOW_CREDENTIALS_HEADER}' response header to 'true'.\" }\n\n      response.headers[ALLOW_CREDENTIALS_HEADER] = \"true\"\n    end\n\n    if @config.max_age > 0\n      max_age = @config.max_age.to_s\n\n      Log.trace { \"Setting '#{MAX_AGE_HEADER}' response header to '#{max_age}'.\" }\n\n      response.headers[MAX_AGE_HEADER] = max_age\n    end\n\n    unless @config.allow_methods.empty?\n      allow_methods = @config.allow_methods.join(\", \")\n\n      Log.trace { \"Setting '#{ALLOW_METHODS_HEADER}' response header to '#{allow_methods}'.\" }\n\n      response.headers[ALLOW_METHODS_HEADER] = allow_methods\n    end\n\n    unless @config.allow_headers.empty?\n      headers : Array(String) = @config.allow_headers.includes?(WILDCARD) ? (request.headers[REQUEST_HEADERS_HEADER]?.try &.split(/,\\ ?/) || [] of String) : @config.allow_headers\n\n      unless headers.empty?\n        allow_headers = headers.join(\", \")\n\n        Log.trace { \"Setting '#{ALLOW_HEADERS_HEADER}' response header to '#{allow_headers}'.\" }\n\n        response.headers[ALLOW_HEADERS_HEADER] = allow_headers\n      end\n    end\n\n    unless check_origin request\n      Log.trace { \"Removing '#{ALLOW_ORIGIN_HEADER}' response header.\" }\n\n      request.headers.delete ALLOW_ORIGIN_HEADER\n\n      return response\n    end\n\n    origin = request.headers[\"origin\"]\n\n    Log.trace { \"Setting '#{ALLOW_ORIGIN_HEADER}' response header to '#{origin}'.\" }\n\n    response.headers[ALLOW_ORIGIN_HEADER] = origin\n\n    unless @config.allow_methods.includes?(method = request.headers[REQUEST_METHOD_HEADER].upcase)\n      Log.trace { \"Method '#{method}' is not allowed.\" }\n\n      response.status = :method_not_allowed\n\n      return response\n    end\n\n    unless @config.allow_headers.includes? WILDCARD\n      ((rh = request.headers[REQUEST_HEADERS_HEADER]?) ? rh.split(/,\\ ?/) : [] of String).each do |header|\n        next if SAFELISTED_HEADERS.includes? header\n        next if @config.allow_headers.includes? header\n\n        raise AHK::Exception::Forbidden.new \"Unauthorized header: '#{header}'.\"\n      end\n    end\n\n    response\n  end\n\n  private def check_origin(request : AHTTP::Request) : Bool\n    origin = request.headers[\"origin\"]\n\n    if @config.allow_origin.includes?(WILDCARD)\n      Log.trace { \"Origin is a wildcard.\" }\n\n      return true\n    end\n\n    # Use case equality in case an origin is a Regex\n    @config.allow_origin.each do |ao|\n      Log.trace { \"Checking allowed origin '#{ao}' to origin '#{origin}'.\" }\n\n      if ao === origin\n        Log.trace { \"Allowed origin '#{ao}' matches origin '#{origin}'.\" }\n\n        return true\n      end\n    end\n\n    Log.trace { \"Origin '#{origin}' is not allowed.\" }\n\n    false\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/listeners/file.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Listeners::File\n  protected def initialize(@file_parser : ATH::FileParser); end\n\n  @[AEDA::AsEventListener]\n  def on_request(event : AHK::Events::Request) : Nil\n    return unless event.request.headers[\"content-type\"]?.try &.starts_with? \"multipart/form-data\"\n\n    @file_parser.parse event.request\n  end\n\n  @[AEDA::AsEventListener]\n  def on_terminate(event : AHK::Events::Terminate) : Nil\n    @file_parser.clear\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/listeners/format.cr",
    "content": "require \"mime\"\n\n# Attempts to determine the best format for the current request based on its [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) `HTTP` header\n# and the format priority configuration.\n#\n# [AHTTP::Request::FORMATS](/HTTP/Request/#Athena::HTTP::Request::FORMATS) is used to determine the related format from the request's `MIME` type.\n#\n# See the [Getting Started](/getting_started/routing#content-negotiation) docs for more information.\nstruct Athena::Framework::Listeners::Format\n  def initialize(\n    @format_negotiator : ATH::View::FormatNegotiator,\n  ); end\n\n  @[AEDA::AsEventListener(priority: 34)]\n  def on_request(event : AHK::Events::Request) : Nil\n    request = event.request\n    format = request.request_format nil\n\n    if format.nil?\n      accept = @format_negotiator.best \"\"\n\n      if !accept.nil? && 0.0 < accept.quality\n        format = request.format accept.header\n\n        unless format.nil?\n          request.attributes.set \"media_type\", accept.header, String\n        end\n      end\n    end\n\n    raise AHK::Exception::NotAcceptable.new \"No matching accepted Response format could be determined.\" if format.nil?\n\n    request.request_format = format\n  rescue ex : AHK::Exception::StopFormatListener\n    # ignore\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/listeners/view.cr",
    "content": "class Athena::HTTPKernel::Action(ReturnType, ParameterTypeTuple, ParametersType) < Athena::HTTPKernel::ActionBase\n  # This has to live here so the view is properly typed and not a union of all possible views.\n  # Would be more ideal if we didn't have to monkey patch this in but :shrug:.\n  protected def create_view(data : ReturnType) : ATH::View\n    ATH::View(ReturnType).new data\n  end\n\n  protected def create_view(data : _) : NoReturn\n    raise \"BUG:  Invoked wrong `create_view` overload.\"\n  end\nend\n\n@[ADI::Register]\n# Listens on the `AHK::Events::View` event to convert a non `AHTTP::Response` into an `AHTTP::Response`.\n# Allows creating format agnostic controllers by allowing them to return format agnostic data that\n# is later used to render the content in the expected format.\n#\n# See the [Getting Started](/getting_started/routing#content-negotiation) docs for more information.\nstruct Athena::Framework::Listeners::View\n  def initialize(\n    @view_handler : ATH::View::ViewHandlerInterface,\n    @annotation_resolver : ATH::AnnotationResolver,\n  ); end\n\n  @[AEDA::AsEventListener(priority: 100)]\n  def on_view(event : AHK::Events::View) : Nil\n    request = event.request\n    action = request.attributes.get \"_action\", AHK::ActionBase\n\n    view = event.action_result\n\n    unless view.is_a? ATH::View\n      view = action.create_view view\n    end\n\n    if configuration = @annotation_resolver.action_annotations(request)[ATHA::View]?\n      if (status = configuration.status) && (view.status.nil? || view.status.not_nil!.ok?)\n        view.status = status\n      end\n\n      context = view.context\n\n      if groups = configuration.serialization_groups\n        if context_groups = context.groups\n          context_groups.concat groups\n        else\n          context.groups = groups\n        end\n      end\n\n      configuration.emit_nil.try do |emit_nil|\n        context.emit_nil = emit_nil\n      end\n    end\n\n    if view.format.nil?\n      view.format = request.request_format\n    end\n\n    event.response = @view_handler.handle view, request\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/logging.cr",
    "content": "require \"log\"\n\nmodule Athena::Framework\n  Log = ::Log.for \"athena.framework\"\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/abstract_browser.cr",
    "content": "# Simulates a browser to make requests to some destination.\n#\n# NOTE: Currently just acts as a client to make `HTTP` requests. This type exists to allow for introduction of other functionality in the future.\nabstract class Athena::Framework::Spec::AbstractBrowser\n  @request : AHTTP::Request?\n  @response : ::HTTP::Server::Response?\n\n  # :nodoc:\n  #\n  # Makes a *request* and returns the response.\n  abstract def do_request(request : AHTTP::Request) : ::HTTP::Server::Response\n\n  def request : AHTTP::Request\n    if request = @request\n      return request\n    end\n\n    raise RuntimeError.new \"The '#request' method must be called before a request is available.\"\n  end\n\n  def response : ::HTTP::Server::Response\n    if response = @response\n      return response\n    end\n\n    raise RuntimeError.new \"The '#request' method must be called before a response is available.\"\n  end\n\n  # Makes an HTTP request with the provided *method*, at the provided *path*, with the provided *body* and/or *headers* and returns the resulting response.\n  def request(\n    method : String,\n    path : String,\n    headers : ::HTTP::Headers,\n    body : String | Bytes | IO | Nil,\n  ) : ::HTTP::Server::Response\n    # At the moment this just calls into `do_request`.\n    # Kept this as way allow for future expansion.\n\n    self.request AHTTP::Request.new method, path, headers, body\n  end\n\n  # Makes an HTTP request with the provided *request*, returning the resulting response.\n  def request(request : AHTTP::Request | ::HTTP::Request) : ::HTTP::Server::Response\n    @request = AHTTP::Request.new request\n\n    @response = self.do_request self.request\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/api_test_case.cr",
    "content": "require \"./web_test_case\"\n\n# A `WebTestCase` implementation with the intent of testing API controllers.\n# Can be extended to add additional application specific configuration, such as setting up an authenticated user to make the request as.\n#\n# ## Usage\n#\n# Say we want to test the following controller:\n#\n# ```\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\"/add/{value1}/{value2}\")]\n#   def add(value1 : Int32, value2 : Int32, @[ATHA::MapQueryParameter] negative : Bool = false) : Int32\n#     sum = value1 + value2\n#     negative ? -sum : sum\n#   end\n# end\n# ```\n#\n# We can define a struct inheriting from `self` to implement our test logic:\n#\n# ```\n# struct ExampleControllerTest < ATH::Spec::APITestCase\n#   def test_add_positive : Nil\n#     self.get(\"/add/5/3\").body.should eq \"8\"\n#   end\n#\n#   def test_add_negative : Nil\n#     self.get(\"/add/5/3?negative=true\").body.should eq \"-8\"\n#   end\n# end\n# ```\n#\n# The `#request` method is used to make our requests to the API, then we run are assertions against the resulting `::HTTP::Server::Response`.\n# A key thing to point out is that there is no `::HTTP::Server` involved, thus resulting in more performant specs.\n#\n# TIP: Checkout the built in [expectations][Athena::Framework::Spec::Expectations::HTTP] to make testing easier.\n#\n# ATTENTION: Be sure to call `Athena::Spec.run_all` to your `spec_helper.cr` to ensure all test case instances are executed.\n#\n# ### Mocking External Dependencies\n#\n# The previous example was quite simple. However, most likely a controller is going to have dependencies on various other services; such as an API client to make requests to a third party API.\n# By default each test will be executed with the same services as it would normally, i.e. those requests to the third party API would actually be made.\n# To solve this we can create a mock implementation of the API client and make it so that implementation is injected when the test runs.\n#\n# ```\n# # Create an example API client.\n# @[ADI::Register]\n# class APIClient\n#   def fetch_latest_data : String\n#     # Assume this method actually makes an `HTTP` request to get the latest data.\n#     \"DATA\"\n#   end\n# end\n#\n# # Define a mock implementation of our APIClient that does not make a request and just returns mock data.\n# class MockAPIClient < APIClient\n#   def fetch_latest_data : String\n#     # This could also be an instance variable that gets set when this mock is created.\n#     \"MOCK_DATA\"\n#   end\n# end\n#\n# # Enable our API client to be replaced in the service container.\n# class ADI::Spec::MockableServiceContainer\n#   # Use the block version of the `property` macro to use our mocked client by default, while still allowing it to be replaced at runtime.\n#   #\n#   # The block version of `getter` could also be used if you don't need to set it at runtime.\n#   # The `setter` macro could be also if you only want to allow replacing it at runtime.\n#   property(api_client) { MockAPIClient.new }\n# end\n#\n# @[ADI::Register]\n# class ExampleServiceController < ATH::Controller\n#   def initialize(@api_client : APIClient); end\n#\n#   @[ARTA::Post(\"/sync\")]\n#   def sync_data : String\n#     # Use the injected api client to get the latest data to sync.\n#     data = @api_client.fetch_latest_data\n#\n#     # ...\n#\n#     data\n#   end\n# end\n#\n# struct ExampleServiceControllerTest < ATH::Spec::APITestCase\n#   def initialize\n#     super\n#\n#     # Our API client could also have been replaced at runtime;\n#     # such as if you wanted provide it what data it should return on a test by test basis.\n#     # self.client.container.api_client = MockAPIClient.new\n#   end\n#\n#   def test_sync_data : Nil\n#     self.post(\"/sync\").body.should eq %(\"MOCK_DATA\")\n#   end\n# end\n# ```\n#\n# TIP: See `ADI::Spec::MockableServiceContainer` for more details on mocking services.\n#\n# Each `test_*` method has its own service container instance.\n# Any services that are mutated/replaced within the `initialize` method will affect all `test_*` methods.\n# However, services can also be mutated/replaced within specific `test_*` methods to scope it that particular test;\n# just be sure that you do it _before_ calling `#request`.\nabstract struct Athena::Framework::Spec::APITestCase < ATH::Spec::WebTestCase\n  def initialize\n    # Ensure each test method has a unique container.\n    self.init_container\n\n    super\n  end\n\n  # Returns a reference to the `AbstractBrowser` being used for the test.\n  def client : ATH::Spec::HTTPBrowser\n    @client.as(ATH::Spec::HTTPBrowser).not_nil!\n  end\n\n  # Makes a `DELETE` request to the provided *path*, optionally with the provided *headers*.\n  def delete(path : String, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ::HTTP::Server::Response\n    self.request \"DELETE\", path, headers: headers\n  end\n\n  # Makes a `GET` request to the provided *path*, optionally with the provided *headers*.\n  def get(path : String, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ::HTTP::Server::Response\n    self.request \"GET\", path, headers: headers\n  end\n\n  # Makes a `HEAD` request to the provided *path*, optionally with the provided *headers*.\n  def head(path : String, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ::HTTP::Server::Response\n    self.request \"HEAD\", path, headers: headers\n  end\n\n  # Makes a `LINK` request to the provided *path*, optionally with the provided *headers*.\n  def link(path : String, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ::HTTP::Server::Response\n    self.request \"LINK\", path, headers: headers\n  end\n\n  # Makes a `PATCH` request to the provided *path*, optionally with the provided *body* and *headers*.\n  def patch(path : String, body : String | Bytes | IO | Nil = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ::HTTP::Server::Response\n    self.request \"PATCH\", path, headers: headers\n  end\n\n  # Makes a `POST` request to the provided *path*, optionally with the provided *body* and *headers*.\n  def post(path : String, body : String | Bytes | IO | Nil = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ::HTTP::Server::Response\n    self.request \"POST\", path, body, headers\n  end\n\n  # Makes a `PUT` request to the provided *path*, optionally with the provided *body* and *headers*.\n  def put(path : String, body : String | Bytes | IO | Nil = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ::HTTP::Server::Response\n    self.request \"PUT\", path, body, headers\n  end\n\n  # Makes a `UNLINK` request to the provided *path*, optionally with the provided *headers*.\n  def unlink(path : String, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ::HTTP::Server::Response\n    self.request \"UNLINK\", path, headers: headers\n  end\n\n  # See `AbstractBrowser#request`.\n  def request(method : String, path : String, body : String | Bytes | IO | Nil = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new) : ::HTTP::Server::Response\n    self.request AHTTP::Request.new method, path, headers, body\n  end\n\n  # :ditto:\n  def request(request : ::HTTP::Request | AHTTP::Request) : ::HTTP::Server::Response\n    self.client.request AHTTP::Request.new request\n  end\n\n  # Helper method to init the container.\n  # Creates a new container instance and assigns it to the current fiber.\n  protected def init_container : Nil\n    Fiber.current.container = ADI::Spec::MockableServiceContainer.new\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/http.cr",
    "content": "require \"./response/*\"\nrequire \"./request/*\"\n\n# Provides expectation helper method for making assertions about the `AHTTP::Request` and/or `::HTTP::Server::Response` of a controller action.\n# For example asserting the response is successful, has a specific header/cookie (value), and/or if the request has an attribute with a specific value.\n#\n# ```\n# struct ExampleControllerTest < ATH::Spec::APITestCase\n#   def test_root : Nil\n#     self.get \"/\"\n#\n#     self.assert_response_is_successful\n#   end\n# end\n# ```\n#\n# Some expectations will also print more information upon failure to make it easier to understand _why_ it failed.\n# `#assert_response_is_successful` for example will include the response status, headers, and body as well as the exception that caused the failure if applicable.\nmodule Athena::Framework::Spec::Expectations::HTTP\n  # Asserts the response returns with a [successful?](https://crystal-lang.org/api/HTTP/Status.html#success%3F%3ABool-instance-method) status code.\n  def assert_response_is_successful(description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should Response::IsSuccessful.new(description), file: file, line: line\n  end\n\n  # Asserts the response returns with status of `422 Unprocessable Entity`.\n  def assert_response_is_unprocessable(description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should Response::IsUnprocessable.new(description), file: file, line: line\n  end\n\n  # Asserts the response returns with a [redirection?](https://crystal-lang.org/api/HTTP/Status.html#redirection%3F%3ABool-instance-method) status code.\n  # Optionally allows also asserting the `location` header is that of the provided *location*,\n  # and/or the status is equal to the provided *status*.\n  def assert_response_redirects(location : String? = nil, status : ::HTTP::Status | Int32 | Nil = nil, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should Response::IsRedirected.new(description), file: file, line: line\n    self.response.should Response::HeaderEquals.new(\"location\", location), file: file, line: line if location\n    self.response.should Response::HasStatus.new(status), file: file, line: line if status\n  end\n\n  # Asserts the response has the same status as the one provided.\n  def assert_response_has_status(status : ::HTTP::Status | Int32, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should Response::HasStatus.new(status.is_a?(Int32) ? ::HTTP::Status.from_value(status) : status, description), file: file, line: line\n  end\n\n  # Asserts the response has a header with the provided *name*.\n  def assert_response_has_header(name : String, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should Response::HasHeader.new(name, description), file: file, line: line\n  end\n\n  # Asserts the response does not have a header with the provided *name*.\n  def assert_response_not_has_header(name : String, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should_not Response::HasHeader.new(name, description), file: file, line: line\n  end\n\n  # Asserts the response has a cookie with the provided *name*, and optionally *path* and *domain*.\n  def assert_response_has_cookie(name : String, path : String? = nil, domain : String? = nil, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should Response::HasCookie.new(name, path, domain, description), file: file, line: line\n  end\n\n  # Asserts the response does not have a cookie with the provided *name*, and optionally *path* and *domain*.\n  def assert_response_not_has_cookie(name : String, path : String? = nil, domain : String? = nil, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should_not Response::HasCookie.new(name, path, domain, description), file: file, line: line\n  end\n\n  # Asserts the format of the response equals the provided *format*.\n  def assert_response_format_equals(format : String, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should Response::FormatEquals.new(self.request, format, description), file: file, line: line\n  end\n\n  # Asserts the value of the header with the provided *name*, equals that of the provided *value*.\n  def assert_response_header_equals(name : String, value : String, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should Response::HeaderEquals.new(name, value, description), file: file, line: line\n  end\n\n  # Asserts the value of the header with the provided *name*, does not equal that of the provided *value*.\n  def assert_response_header_not_equals(name : String, value : String, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should_not Response::HeaderEquals.new(name, value, description), file: file, line: line\n  end\n\n  # Asserts the value of the cookie with the provided *name*, and optionally *path* and *domain*, equals that of the provided *value*\n  def assert_cookie_has_value(name : String, value : String, path : String? = nil, domain : String? = nil, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.response.should Response::HasCookie.new(name, path, domain, description), file: file, line: line\n    self.response.should Response::CookieValueEquals.new(name, value, path, domain, description), file: file, line: line\n  end\n\n  # Asserts the request attribute with the provided *name* equals the provided *value*.\n  def assert_request_attribute_equals(name : String, value : _, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.request.should Request::AttributeEquals.new(name, value, description), file: file, line: line\n  end\n\n  # Asserts the request was matched against the route with the provided *name*.\n  def assert_route_equals(name : String, parameters : Hash? = nil, description : String? = nil, *, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    self.request.should Request::AttributeEquals.new(\"_route\", name, description), file: file, line: line\n\n    parameters.try &.each do |k, v|\n      self.request.should Request::AttributeEquals.new(k, v, description), file: file, line: line\n    end\n  end\n\n  private abstract def client : AbstractBrowser?\n\n  private def response : ::HTTP::Server::Response\n    self.client.response\n  end\n\n  private def request : AHTTP::Request\n    self.client.request\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/request/attribute_equals.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Request::AttributeEquals(T)\n  @name : String\n  @value : T\n  @description : String?\n\n  def initialize(\n    @name : String,\n    @value : T,\n    @description : String? = nil,\n  ); end\n\n  def match(actual_value : AHTTP::Request) : Bool\n    @value == actual_value.attributes.get?(@name, T)\n  end\n\n  def match(actual_value : _) : Bool\n    false\n  end\n\n  def failure_message(actual_value : AHTTP::Request) : String\n    String.build do |io|\n      if desc = @description\n        io << desc << '\\n' << '\\n'\n      end\n\n      io << \"Failed asserting that the request has attribute '#{@name}' with value '#{@value}'.\"\n    end\n  end\n\n  def negative_failure_message(actual_value : AHTTP::Request) : String\n    String.build do |io|\n      if desc = @description\n        io << desc << '\\n' << '\\n'\n      end\n\n      io << \"Failed asserting that the request does not have attribute '#{@name}' with value '#{@value}'.\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/base.cr",
    "content": "# :nodoc:\nabstract struct Athena::Framework::Spec::Expectations::Response::Base\n  @description : String?\n\n  def initialize(@description : String? = nil); end\n\n  abstract def match(actual_value : ::HTTP::Server::Response) : Bool\n  abstract def failure_message : String\n  abstract def negated_failure_message : String\n\n  def match(actual_value : _) : Bool\n    false\n  end\n\n  def failure_message(actual_value : ::HTTP::Server::Response) : String\n    self.build_message actual_value, self.failure_message\n  end\n\n  def negative_failure_message(actual_value : ::HTTP::Server::Response) : String\n    self.build_message actual_value, self.negated_failure_message\n  end\n\n  private def include_response? : Bool\n    true\n  end\n\n  private def build_message(response : ::HTTP::Server::Response, message : String) : String\n    String.build do |io|\n      if desc = @description\n        io << desc << '\\n' << '\\n'\n      end\n\n      io << \"Failed asserting that the response #{message}#{self.include_response? ? \":\\n#{response}\" : \".\"}\"\n\n      if (\"500\" == response.headers[\"x-debug-exception-code\"]?.presence) &&\n         (exception_message = response.headers[\"x-debug-exception-message\"]?.presence) &&\n         (exception_file = response.headers[\"x-debug-exception-file\"]?.presence) &&\n         (exception_class = response.headers[\"x-debug-exception-class\"]?.presence)\n        io << '\\n' << '\\n'\n\n        io << \"Caused By:\\n\"\n        io << ' ' << ' '\n        URI.decode exception_message, io\n        io << ' ' << '(' << exception_class << ')' << '\\n'\n        io << ' ' << ' ' << ' ' << ' ' << \"from\" << ' ' << exception_file\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/cookie_value_equals.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Response::CookieValueEquals < Athena::Framework::Spec::Expectations::Response::Base\n  @name : String\n  @value : String\n  @path : String?\n  @domain : String?\n\n  def initialize(\n    @name : String,\n    @value : String,\n    @path : String? = nil,\n    @domain : String? = nil,\n    description : String? = nil,\n  )\n    super description\n  end\n\n  def match(actual_value : ::HTTP::Server::Response) : Bool\n    return false unless cookie = actual_value.cookies[@name]?\n\n    @path == cookie.path && @domain == cookie.domain && @value == cookie.value\n  end\n\n  private def failure_message : String\n    String.build do |io|\n      io << \"has cookie '#{@name}'\"\n\n      io << \" with path '#{@path}'\" unless @path.nil?\n      io << \" for domain '#{@domain}'\" unless @domain.nil?\n      io << \" with value '#{@value}'\"\n    end\n  end\n\n  private def negated_failure_message : String\n    String.build do |io|\n      io << \"does not have cookie '#{@name}'\"\n\n      io << \" with path '#{@path}'\" unless @path.nil?\n      io << \" for domain '#{@domain}'\" unless @domain.nil?\n      io << \" with value '#{@value}'\"\n    end\n  end\n\n  private def include_response? : Bool\n    false\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/format_equals.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Response::FormatEquals < Athena::Framework::Spec::Expectations::Response::Base\n  @request : AHTTP::Request\n  @format : String?\n\n  def initialize(\n    @request : AHTTP::Request,\n    @format : String? = nil,\n    description : String? = nil,\n  )\n    super description\n  end\n\n  def match(actual_value : ::HTTP::Server::Response) : Bool\n    return false unless content_type = actual_value.headers[\"content-type\"]?\n\n    @format == @request.format(content_type)\n  end\n\n  private def failure_message : String\n    \"format is '#{@format || \"null\"}'\"\n  end\n\n  private def negated_failure_message : String\n    \"format is not '#{@format || \"null\"}'\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/has_cookie.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Response::HasCookie < Athena::Framework::Spec::Expectations::Response::Base\n  @name : String\n  @path : String?\n  @domain : String?\n\n  def initialize(\n    @name : String,\n    @path : String? = nil,\n    @domain : String? = nil,\n    description : String? = nil,\n  )\n    super description\n  end\n\n  def match(actual_value : ::HTTP::Server::Response) : Bool\n    return false unless cookie = actual_value.cookies[@name]?\n\n    @path == cookie.path && @domain == cookie.domain\n  end\n\n  private def failure_message : String\n    String.build do |io|\n      io << \"has cookie '#{@name}'\"\n\n      io << \" with path '#{@path}'\" unless @path.nil?\n      io << \" for domain '#{@domain}'\" unless @domain.nil?\n    end\n  end\n\n  private def negated_failure_message : String\n    String.build do |io|\n      io << \"does not have cookie '#{@name}'\"\n\n      io << \" with path '#{@path}'\" unless @path.nil?\n      io << \" for domain '#{@domain}'\" unless @domain.nil?\n    end\n  end\n\n  private def include_response? : Bool\n    false\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/has_header.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Response::HasHeader < Athena::Framework::Spec::Expectations::Response::Base\n  @name : String\n\n  def initialize(\n    @name : String,\n\n    description : String? = nil,\n  )\n    super description\n  end\n\n  def match(actual_value : ::HTTP::Server::Response) : Bool\n    actual_value.headers.has_key? @name\n  end\n\n  private def failure_message : String\n    \"has header '#{@name}'\"\n  end\n\n  private def negated_failure_message : String\n    \"does not have header '#{@name}'\"\n  end\n\n  private def include_response? : Bool\n    false\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/has_status.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Response::HasStatus < Athena::Framework::Spec::Expectations::Response::Base\n  @status : ::HTTP::Status\n\n  def self.new(\n    status_code : Int32,\n    description : String? = nil,\n  )\n    new ::HTTP::Status.from_value(status_code), description\n  end\n\n  def initialize(\n    @status : ::HTTP::Status,\n    description : String? = nil,\n  )\n    super description\n  end\n\n  def match(actual_value : ::HTTP::Server::Response) : Bool\n    actual_value.status == @status\n  end\n\n  private def failure_message : String\n    \"status is '#{@status}'\"\n  end\n\n  private def negated_failure_message : String\n    \"status is not '#{@status}'\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/header_equals.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Response::HeaderEquals < Athena::Framework::Spec::Expectations::Response::Base\n  @name : String\n  @value : String\n\n  def initialize(\n    @name : String,\n    @value : String,\n    description : String? = nil,\n  )\n    super description\n  end\n\n  def match(actual_value : ::HTTP::Server::Response) : Bool\n    @value == actual_value.headers[@name]?\n  end\n\n  private def failure_message : String\n    \"has header '#{@name}' with value '#{@value}'\"\n  end\n\n  private def negated_failure_message : String\n    \"does not have header '#{@name}' with value '#{@value}'\"\n  end\n\n  private def include_response? : Bool\n    false\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/is_redirected.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Response::IsRedirected < Athena::Framework::Spec::Expectations::Response::Base\n  def match(actual_value : ::HTTP::Server::Response) : Bool\n    actual_value.status.redirection?\n  end\n\n  private def failure_message : String\n    \"is redirected\"\n  end\n\n  private def negated_failure_message : String\n    \"is not redirected\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/is_successful.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Response::IsSuccessful < Athena::Framework::Spec::Expectations::Response::Base\n  def match(actual_value : ::HTTP::Server::Response) : Bool\n    actual_value.status.success?\n  end\n\n  private def failure_message : String\n    \"is successful\"\n  end\n\n  private def negated_failure_message : String\n    \"is not successful\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/expectations/response/is_unprocessable.cr",
    "content": "# :nodoc:\nstruct Athena::Framework::Spec::Expectations::Response::IsUnprocessable < Athena::Framework::Spec::Expectations::Response::Base\n  def match(actual_value : ::HTTP::Server::Response) : Bool\n    actual_value.status.unprocessable_entity?\n  end\n\n  private def failure_message : String\n    \"is unprocessable\"\n  end\n\n  private def negated_failure_message : String\n    \"is not unprocessable\"\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/http_browser.cr",
    "content": "# Simulates a browser and makes a requests to `ATH::RouteHandler`.\nclass Athena::Framework::Spec::HTTPBrowser < ATH::Spec::AbstractBrowser\n  # Returns a reference to an `ADI::Spec::MockableServiceContainer` to allow configuring the container before a test.\n  def container : ADI::Spec::MockableServiceContainer\n    ADI.container.as(ADI::Spec::MockableServiceContainer)\n  end\n\n  protected def do_request(request : AHTTP::Request) : ::HTTP::Server::Response\n    response = ::HTTP::Server::Response.new IO::Memory.new\n\n    handler = ADI.container.athena_http_kernel\n    athena_response = handler.handle request\n\n    athena_response.send request, response\n\n    handler.terminate request, athena_response\n\n    response\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec/web_test_case.cr",
    "content": "require \"./expectations/*\"\n\n# Base `ASPEC::TestCase` for web based integration tests.\n#\n# NOTE: Currently only `API` based tests are supported. This type exists to allow for introduction of other types in the future.\nabstract struct Athena::Framework::Spec::WebTestCase < ASPEC::TestCase\n  include ATH::Spec::Expectations::HTTP\n\n  protected getter client : AbstractBrowser\n\n  def initialize\n    @client = create_client\n  end\n\n  # Returns the `AbstractBrowser` instance to which requests should be made against.\n  def create_client : AbstractBrowser\n    HTTPBrowser.new\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/spec.cr",
    "content": "require \"athena-spec\"\n\nrequire \"athena-clock/spec\"\nrequire \"athena-console/spec\"\nrequire \"athena-event_dispatcher/spec\"\nrequire \"athena-dependency_injection/spec\"\nrequire \"athena-validator/spec\"\n\nrequire \"./spec/*\"\n\n# :nodoc:\n#\n# Monkey patch ::HTTP::Server::Response to allow accessing the response body directly and string representation of it.\nclass ::HTTP::Server::Response\n  @body_io : IO = IO::Memory.new\n  @body : String? = nil\n\n  def write(slice : Bytes) : Nil\n    @body_io.write slice\n\n    previous_def\n  end\n\n  def body : String\n    @body ||= @body_io.to_s\n  end\n\n  def to_s(io : IO) : Nil\n    io << @version << ' ' << self.status_code << ' ' << @status.description << '\\n' << '\\n'\n    ::HTTP.serialize_headers_and_string_body io, @headers, self.body\n  end\nend\n\n# A set of testing utilities/types to aid in testing `Athena::Framework` related types.\n#\n# ### Getting Started\n#\n# Require this module in your `spec_helper.cr` file.\n#\n# ```\n# # This also requires \"spec\" and \"athena-spec\".\n# require \"athena/spec\"\n# ```\n#\n# Add `Athena::Spec` as a development dependency, then run a `shards install`.\n# See the individual types for more information.\nmodule Athena::Framework::Spec\n  # `ATH::Spec` includes a set of custom spec expectations for making it easier to test certain aspects of the application.\n  # These expectations are exposed via helper methods within the modules defined within this namespace.\n  # See each module for more information.\n  module Expectations\n    # :nodoc:\n    module Request; end\n\n    # :nodoc:\n    module Response; end\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/view/configurable_view_handler_interface.cr",
    "content": "# These live here so `Athena::Framework::View` is correctly created as a class versus a module.\n\n# Parent type of a view just used for typing.\n#\n# See `ATH::View`.\nmodule Athena::Framework::ViewBase; end\n\nclass Athena::Framework::View(T)\n  include Athena::Framework::ViewBase\nend\n\nrequire \"./view_handler_interface\"\n\n# Specialized `ATH::View::ViewHandlerInterface` that allows controlling various serialization `ATH::View::Context` aspects dynamically.\nmodule Athena::Framework::View::ConfigurableViewHandlerInterface\n  include Athena::Framework::View::ViewHandlerInterface\n\n  # Sets the *groups* that should be used as part of `ASR::ExclusionStrategies::Groups`.\n  abstract def serialization_groups=(groups : Enumerable(String)) : Nil\n\n  # Sets the *version* that should be used as part of `ASR::ExclusionStrategies::Version`.\n  abstract def serialization_version=(version : SemanticVersion) : Nil\n\n  # Determines if properties with `nil` values should be emitted.\n  abstract def emit_nil=(emit_nil : Bool) : Nil\nend\n"
  },
  {
    "path": "src/components/framework/src/view/context.cr",
    "content": "# Represents (de)serialization options in a serializer agnostic way.\nclass Athena::Framework::View::Context\n  # Returns the groups that can be used to create different \"views\" of an object.\n  #\n  # `ASR::ExclusionStrategies::Groups` is an example of this.\n  getter groups : Set(String)? = nil\n\n  # Determines if properties with `nil` values should be emitted.\n  property? emit_nil : Bool? = nil\n\n  # Represents the version of an object. Can be used to control what properties are serialized based on the version.\n  #\n  # `ASR::ExclusionStrategies::Version` is an example of this.\n  property version : SemanticVersion? = nil\n\n  # Returns any `ASR::ExclusionStrategies::ExclusionStrategyInterface` that should be used by the serializer.\n  getter exclusion_strategies = Array(ASR::ExclusionStrategies::ExclusionStrategyInterface).new\n\n  # Adds the provided *strategy* to the `#exclusion_strategies` array.\n  def add_exclusion_strategy(strategy : ASR::ExclusionStrategies::ExclusionStrategyInterface) : self\n    @exclusion_strategies << strategy\n\n    self\n  end\n\n  # Adds the provided *group* to the `#groups` array.\n  def add_group(group : String) : self\n    (@groups ||= Set(String).new) << group\n\n    self\n  end\n\n  # Adds the provided *groups* to the `#groups` array.\n  def add_groups(*groups : String) : self\n    self.add_groups groups\n  end\n\n  # :ditto:\n  def add_groups(groups : Enumerable(String)) : self\n    groups.each do |group|\n      self.add_group group\n    end\n\n    self\n  end\n\n  # Sets the `#groups` array to the provided *groups*.\n  def groups=(groups : Enumerable(String)) : self\n    @groups = groups.to_set\n\n    self\n  end\n\n  # Sets the `#version` to the provided *version*.\n  def version=(version : String) : self\n    self.version = SemanticVersion.parse version\n\n    self\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/view/format_handler_interface.cr",
    "content": "# Represents custom logic that should be applied for a specific format in order to render an `ATH::View` into an `AHTTP::Response`\n# that is not handled by default by Athena. E.g. `HTML`.\n#\n# ```\n# # Register our handler as a service.\n# @[ADI::Register]\n# class HTMLFormatHandler\n#   # Implement the interface.\n#   include Athena::Framework::View::FormatHandlerInterface\n#\n#   # :inherit:\n#   #\n#   # Turn the provided data into a response that can be returned to the client.\n#   def call(view_handler : ATH::View::ViewHandlerInterface, view : ATH::ViewBase, request : AHTTP::Request, format : String) : AHTTP::Response\n#     AHTTP::Response.new \"<h1>#{view.data}</h1>\", headers: ::HTTP::Headers{\"content-type\" => \"text/html\"}\n#   end\n#\n#   # :inherit:\n#   #\n#   # Specify that `self` handles the `HTML` format.\n#   def format : String\n#     \"html\"\n#   end\n# end\n# ```\n#\n# The implementation for `HTML` for example could use `.to_s` as depicted here, or utilize a templating engine, possibly taking advantage\n# of [custom annotations](/getting_started/configuration#custom-annotations) to allow specifying the related template name.\n@[ADI::Autoconfigure(tags: [Athena::Framework::View::FormatHandlerInterface::TAG])]\nmodule Athena::Framework::View::FormatHandlerInterface\n  TAG = \"athena.format_handler\"\n\n  # Responsible for returning an `AHTTP::Response` for the provided *view* and *request* in the provided *format*.\n  #\n  # The `ATH::View::ViewHandlerInterface` is also provided to ease response creation.\n  abstract def call(view_handler : ATH::View::ViewHandlerInterface, view : ATH::View, request : AHTTP::Request, format : String) : AHTTP::Response\n\n  # Returns the format that `self` handles.\n  #\n  # The *format* must be registered with the [AHTTP::Request::FORMATS](/HTTP/Request/#Athena::HTTP::Request::FORMATS) hash;\n  # either as a built in format, or a custom one that has registered via [AHTTP::Request.register_format](/HTTP/Request/#Athena::HTTP::Request.register_format(format,mime_types)).\n  abstract def format : String\nend\n"
  },
  {
    "path": "src/components/framework/src/view/format_negotiator.cr",
    "content": "# An extension of `ANG::Negotiator` that supports resolving the format based on an applications `ATH::Bundle::Schema::FormatListener` rules.\n#\n# See the [Getting Started](/getting_started/routing#content-negotiation) docs for more information.\nclass Athena::Framework::View::FormatNegotiator < ANG::Negotiator\n  # :nodoc:\n  record Rule,\n    priorities : Array(String)? = nil,\n    fallback_format : String | Bool | Nil = false,\n    stop : Bool = false,\n    prefer_extension : Bool = true\n\n  @map : Array({AHTTP::RequestMatcher::Interface, ATH::View::FormatNegotiator::Rule}) = [] of {AHTTP::RequestMatcher::Interface, ATH::View::FormatNegotiator::Rule}\n\n  def initialize(\n    @request_store : AHTTP::RequestStore,\n    @mime_types : Hash(String, Array(String)) = Hash(String, Array(String)).new,\n  )\n  end\n\n  protected def add(request_matcher : AHTTP::RequestMatcher::Interface, rule : Rule) : Nil\n    @map << {request_matcher, rule}\n  end\n\n  # :inherit:\n  # ameba:disable Metrics/CyclomaticComplexity\n  def best(header : String, priorities : Indexable(String)? = nil, strict : Bool = false) : HeaderType?\n    request = @request_store.request\n\n    header = header.presence || request.headers[\"accept\"]?\n    extension_header = nil\n\n    @map.each do |(matcher, rule)|\n      next unless matcher.matches? request\n\n      if rule.stop\n        raise AHK::Exception::StopFormatListener.new \"Stopping format listener.\"\n      end\n\n      if priorities.nil? && rule.priorities.nil?\n        if fallback_format = rule.fallback_format\n          request.mime_type(fallback_format.as(String)).try do |mime_type|\n            return ANG::Accept.new mime_type\n          end\n        end\n\n        next\n      end\n\n      if rule.prefer_extension && extension_header.nil?\n        if (extension = Path.new(request.path).extension.lchop '.').presence\n          extension_header = request.mime_type extension\n\n          header = %(#{extension_header}; q=2.0#{(h = header.presence) ? \",#{h}\" : \"\"})\n        end\n      end\n\n      if h = header.presence\n        # Priorities defined on the rule wont be nil at this point it would have been skipped\n        mime_types = self.normalize_mime_types priorities || rule.priorities.not_nil!\n\n        if mime_type = super h, mime_types\n          return mime_type\n        end\n      end\n\n      rule.fallback_format.try do |ff|\n        return if false == ff\n\n        request.mime_type(ff.as(String)).try do |mt|\n          return ANG::Accept.new mt\n        end\n      end\n    end\n\n    nil\n  end\n\n  private def normalize_mime_types(priorities : Indexable(String)) : Array(String)\n    priorities = priorities.map &.gsub(/\\s+/, \"\").downcase\n\n    mime_types = [] of String\n\n    priorities.each do |priority|\n      if priority.includes? '/'\n        mime_types << priority\n\n        next\n      end\n\n      mime_types = mime_types.concat AHTTP::Request.mime_types priority\n\n      if @mime_types.has_key? priority\n        mime_types.concat @mime_types[priority]\n      end\n    end\n\n    mime_types\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/view/view.cr",
    "content": "# An `ATH::View` represents an `AHTTP::Response`, but in a format agnostic way.\n#\n# Returning a `ATH::View` is essentially the same as returning the data directly; but allows customizing\n# the response status and headers without needing to render the response body within the controller as an `AHTTP::Response`.\n#\n# ```\n# require \"athena\"\n#\n# class HelloController < ATH::Controller\n#   @[ARTA::Get(\"/{name}\")]\n#   def say_hello(name : String) : NamedTuple(greeting: String)\n#     {greeting: \"Hello #{name}\"}\n#   end\n#\n#   @[ARTA::Get(\"/view/{name}\")]\n#   def say_hello_view(name : String) : ATH::View(NamedTuple(greeting: String))\n#     self.view({greeting: \"Hello #{name}\"}, :im_a_teapot)\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /Fred      # => 200 {\"greeting\":\"Hello Fred\"}\n# # GET /view/Fred # => 418 {\"greeting\":\"Hello Fred\"}\n# ```\n#\n# See the [Getting Started](/getting_started/routing#content-negotiation) docs for more information.\nclass Athena::Framework::View(T)\n  # The response data.\n  property data : T\n\n  # The `HTTP::Status` of the underlying `#response`.\n  property status : ::HTTP::Status?\n\n  # The format the view should be rendered in.\n  #\n  # The *format* must be registered with the [AHTTP::Request::FORMATS](/HTTP/Request/#Athena::HTTP::Request::FORMATS) hash;\n  # either as a built in format, or a custom one that has registered via [AHTTP::Request.register_format](/HTTP/Request/#Athena::HTTP::Request.register_format(format,mime_types)).\n  property format : String? = nil\n\n  # The parameters that should be used when constructing the redirect `#route` URL.\n  property route_params : Hash(String, String?) = Hash(String, String?).new\n\n  property context : ATH::View::Context { ATH::View::Context.new }\n\n  # Returns the `URL` that the current request should be redirected to.\n  #\n  # See the [Location](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) header documentation.\n  getter location : String? = nil\n\n  # Returns the name of the route the current request should be redirected to.\n  #\n  # See the [Getting Started](/getting_started/routing#url-generation) docs for more information.\n  getter route : String? = nil\n\n  # The wrapped `AHTTP::Response` instance.\n  property response : AHTTP::Response do\n    response = AHTTP::Response.new\n\n    if status = @status\n      response.status = status\n    end\n\n    response\n  end\n\n  # Creates a view instance that'll redirect to the provided *url*. See `#location`.\n  #\n  # Optionally allows setting the underlying *status* and/or *headers*.\n  def self.create_redirect(\n    url : String,\n    status : ::HTTP::Status = ::HTTP::Status::FOUND,\n    headers : ::HTTP::Headers = ::HTTP::Headers.new,\n  ) : self\n    view = ATH::View(Nil).new status: status, headers: headers\n    view.location = url\n\n    view\n  end\n\n  # Creates a view instance that'll redirect to the provided *route*. See `#route`.\n  #\n  # Optionally allows setting the underlying route *params*, *status*, and/or *headers*.\n  def self.create_route_redirect(\n    route : String,\n    params : Hash(String, _) = Hash(String, String?).new,\n    status : ::HTTP::Status = ::HTTP::Status::FOUND,\n    headers : ::HTTP::Headers = ::HTTP::Headers.new,\n  ) : self\n    view = ATH::View(Nil).new status: status, headers: headers\n    view.route = route\n    view.route_params = params.transform_values &.to_s.as(String?)\n\n    view\n  end\n\n  def initialize(@data : T? = nil, @status : ::HTTP::Status? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    self.headers = headers unless headers.empty?\n  end\n\n  # Returns the headers of the underlying `#response`.\n  def headers : AHTTP::Response::Headers\n    self.response.headers\n  end\n\n  # Sets the redirect `#location`.\n  def location=(@location : String) : Nil\n    @route = nil\n  end\n\n  # Returns the type of the data represented by `self`.\n  def return_type : T.class\n    T\n  end\n\n  # Sets the redirect `#route`.\n  def route=(@route : String) : Nil\n    @location = nil\n  end\n\n  # Adds the provided header *name* and *value* to the underlying `#response`.\n  def set_header(name : String, value : String) : Nil\n    self.response.headers[name] = value\n  end\n\n  # :ditto:\n  def set_header(name : String, value : _) : Nil\n    self.set_header name, value.to_s\n  end\n\n  # Sets the *headers* that should be returned as part of the underlying `#response`.\n  def headers=(headers : ::HTTP::Headers) : Nil\n    self.response.headers.clear\n    self.response.headers.merge! headers\n  end\n\n  # Recurses over all the types within `T` to determine what serializer the data should use.\n  protected def serializable_data : T?\n    {% begin %}\n      {%\n        types_to_recurse = [T]\n\n        types_to_recurse.each do |t|\n          t = t.resolve\n\n          if t.union?\n            t.union_types.each do |ut|\n              types_to_recurse << ut\n            end\n          elsif t <= NamedTuple\n            t.keys.each do |k|\n              types_to_recurse << t[k].resolve\n            end\n          elsif t <= Enumerable\n            t.type_vars.each do |ut|\n              types_to_recurse << ut\n            end\n\n            # Use `to_json` if a type includes `JSON::Serializable` but not `ASR::Serializable`\n          elsif (t <= JSON::Serializable) && !(t <= ASR::Serializable)\n            use_serializer_component = false\n\n            # Use the serializer component if the type is `ASR::Serializable`\n          elsif t <= ASR::Serializable\n            use_serializer_component = true\n          else\n            # Fallback on the serializer component\n            use_serializer_component = true\n          end\n        end\n      %}\n\n      {{ use_serializer_component == true ? nil : \"self.data\".id }}\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/view/view_handler.cr",
    "content": "require \"./configurable_view_handler_interface\"\n\nADI.bind format_handlers : Array(Athena::Framework::View::FormatHandlerInterface), \"!athena.format_handler\"\n\n@[ADI::AsAlias(ATH::View::ConfigurableViewHandlerInterface)]\n@[ADI::AsAlias(ATH::View::ViewHandlerInterface)]\n# Default implementation of `ATH::View::ConfigurableViewHandlerInterface`.\nclass Athena::Framework::View::ViewHandler\n  include Athena::Framework::View::ConfigurableViewHandlerInterface\n\n  @custom_handlers = Hash(String, ATH::View::ViewHandlerInterface::HandlerType).new\n\n  @serialization_groups : Set(String)? = nil\n  @serialization_version : SemanticVersion? = nil\n\n  @empty_content_status : ::HTTP::Status\n  @failed_validation_status : ::HTTP::Status\n  @emit_nil : Bool\n\n  def initialize(\n    @url_generator : ART::Generator::Interface,\n    @serializer : ASR::SerializerInterface,\n    @request_store : AHTTP::RequestStore,\n    format_handlers : Array(Athena::Framework::View::FormatHandlerInterface),\n    @failed_validation_status : ::HTTP::Status = ::HTTP::Status::UNPROCESSABLE_ENTITY,\n    @empty_content_status : ::HTTP::Status = ::HTTP::Status::NO_CONTENT,\n    @emit_nil : Bool = false,\n  )\n    format_handlers.each do |format_handler|\n      self.register_handler format_handler.format, format_handler\n    end\n  end\n\n  # :inherit:\n  def serialization_groups=(groups : Enumerable(String)) : Nil\n    @serialization_groups = groups.to_set\n  end\n\n  # :inherit:\n  def serialization_version=(version : String) : Nil\n    self.serialization_version = SemanticVersion.parse version\n  end\n\n  # :inherit:\n  def serialization_version=(version : SemanticVersion) : Nil\n    @serialization_version = version\n  end\n\n  # :inherit:\n  def emit_nil=(@emit_nil : Bool) : Nil\n  end\n\n  # :nodoc:\n  #\n  # This method is mainly for testing.\n  def register_handler(format : String, &block : ATH::View::ViewHandlerInterface, ATH::ViewBase, AHTTP::Request, String -> AHTTP::Response) : Nil\n    self.register_handler format, block\n  end\n\n  # :inherit:\n  def register_handler(format : String, handler : ATH::View::ViewHandlerInterface::HandlerType) : Nil\n    @custom_handlers[format] = handler\n  end\n\n  # :inherit:\n  def supports?(format : String) : Bool\n    # JSON is the only format supported via the serializer ATM.\n    @custom_handlers.has_key?(format) || \"json\" == format\n  end\n\n  # :inherit:\n  def handle(view : ATH::ViewBase, request : AHTTP::Request? = nil) : AHTTP::Response\n    request = @request_store.request if request.nil?\n\n    format = view.format || request.request_format\n\n    unless self.supports? format\n      raise AHK::Exception::NotAcceptable.new \"The server is unable to return a response in the requested format: '#{format}'.\"\n    end\n\n    if custom_handler = @custom_handlers[format]?\n      return custom_handler.call self, view, request, format\n    end\n\n    self.create_response view, request, format\n  end\n\n  # :inherit:\n  def create_response(view : ATH::ViewBase, request : AHTTP::Request, format : String) : AHTTP::Response\n    route = view.route\n\n    if location = (route ? @url_generator.generate(route, view.route_params, :absolute_url) : view.location)\n      return self.create_redirect_response view, location, format\n    end\n\n    response = self.init_response view, format\n\n    unless response.headers.has_key? \"content-type\"\n      mime_type = request.attributes.get? \"media_type\", String\n\n      if mime_type.nil?\n        mime_type = request.mime_type format\n      end\n\n      response.headers[\"content-type\"] = mime_type if mime_type\n    end\n\n    response\n  end\n\n  # :inherit:\n  def create_redirect_response(view : ATH::ViewBase, location : String, format : String) : AHTTP::Response\n    content = nil\n\n    if (vs = view.status) && (vs.created? || vs.accepted?) && !view.data.nil?\n      response = self.init_response view, format\n    else\n      response = view.response\n    end\n\n    response.status = self.status view, content\n    response.headers[\"location\"] = location\n\n    response\n  end\n\n  private def init_response(view : ATH::ViewBase, format : String) : AHTTP::Response\n    content = nil\n\n    # Skip serialization if the action's return type is explicitly `Nil`.\n    if @emit_nil || view.return_type != Nil\n      # TODO: Support Form typed views.\n\n      # Fallback on `to_json` for non ASR::Serializable types.\n      content = if data = view.serializable_data\n                  data.to_json\n                else\n                  data = view.data\n\n                  context = self.serialization_context view\n\n                  # TODO: Implement some sort of Adapter system to convert ATH::View::Context\n                  # into the serializer's required format. Just do that here for now.\n                  athena_serializer_context = ASR::SerializationContext.new\n\n                  context.emit_nil?.try do |en|\n                    athena_serializer_context.emit_nil = en\n                  end\n\n                  context.version.try do |v|\n                    athena_serializer_context.version = v\n                  end\n\n                  context.groups.try do |g|\n                    athena_serializer_context.groups = g\n                  end\n\n                  context.exclusion_strategies.each do |s|\n                    athena_serializer_context.add_exclusion_strategy s\n                  end\n\n                  serialized_data = @serializer.serialize data, format, athena_serializer_context\n\n                  # If the serialized data is \"null\", but the data is not `nil`, assume this means the serializer component failed to serialize it,\n                  # raise an error as it is likely the user forgot to include either `JSON::Serializable` or `ASR::Serializable`.\n                  if \"null\" == serialized_data && !data.nil?\n                    raise AHK::Exception::Logic.new \"Failed to serialize response body. Did you forget to include either `JSON::Serializable` or `ASR::Serializable`?\"\n                  end\n\n                  serialized_data\n                end\n    end\n\n    response = view.response\n    response.status = self.status view, content\n\n    response.content = content unless content.nil?\n\n    response\n  end\n\n  private def serialization_context(view : ATH::ViewBase) : ATH::View::Context\n    context = view.context\n\n    groups = context.groups\n\n    if groups.nil? && (view_handler_groups = @serialization_groups) && !view_handler_groups.empty?\n      context.groups = view_handler_groups\n    end\n\n    if context.version.nil? && (view_handler_version = @serialization_version)\n      context.version = view_handler_version\n    end\n\n    if context.emit_nil?.nil? && (view_handler_emit_nil = @emit_nil)\n      context.emit_nil = view_handler_emit_nil\n    end\n\n    # TODO: Set status code in context attributes if that's ever implemented.\n\n    context\n  end\n\n  private def status(view : ATH::ViewBase, content : _) : ::HTTP::Status\n    # TODO: Handle validating Form data.\n\n    if status = view.status\n      return status\n    end\n\n    content.nil? ? @empty_content_status : ::HTTP::Status::OK\n  end\nend\n"
  },
  {
    "path": "src/components/framework/src/view/view_handler_interface.cr",
    "content": "# Processes an `ATH::View` into an `AHTTP::Response` of the proper format.\n#\n# See the [Getting Started](/getting_started/routing/#content-negotiation) docs for more information.\nmodule Athena::Framework::View::ViewHandlerInterface\n  # The possible types for a view format handler.\n  alias HandlerType = ATH::View::FormatHandlerInterface | Proc(ATH::View::ViewHandlerInterface, ATH::ViewBase, AHTTP::Request, String, AHTTP::Response)\n\n  # Registers the provided *handler* to handle the provided *format*.\n  abstract def register_handler(format : String, handler : ATH::View::ViewHandlerInterface::HandlerType) : Nil\n\n  # Determines if `self` can handle the provided *format*.\n  #\n  # First checks if a custom format handler supports the provided *format*,\n  # otherwise falls back on the `ASR::SerializerInterface`.\n  abstract def supports?(format : String) : Bool\n\n  # Handles the conversion of the provided *view* into an `AHTTP::Response`.\n  #\n  # If no *request* is provided, it is fetched from `AHTTP::RequestStore`.\n  abstract def handle(view : ATH::ViewBase, request : AHTTP::Request? = nil) : AHTTP::Response\n\n  # Creates an `AHTTP::Response` based on the provided *view* that'll redirect to the provided *location*.\n  #\n  # *location* may either be a `URL` or the name of a route.\n  abstract def create_redirect_response(view : ATH::ViewBase, location : String, format : String) : AHTTP::Response\n\n  # Creates an `AHTTP::Response` based on the provided *view* and *request*.\n  abstract def create_response(view : ATH::ViewBase, request : AHTTP::Request, format : String) : AHTTP::Response\nend\n"
  },
  {
    "path": "src/components/http/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/http/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/http/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.1.0] - 2026-04-19\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/http/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/http/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/http/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2025 George Dietrich\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": "src/components/http/README.md",
    "content": "# HTTP\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/workflows/CI/badge.svg)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/http.svg)](https://github.com/athena-framework/http/releases)\n\nShared common HTTP abstractions/utilities.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/HTTP).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/http/docs/README.md",
    "content": "The `Athena::HTTP` component provides various HTTP related Athena types and utilities.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-http:\n    github: athena-framework/http\n    version: ~> 0.1.0\n```\n"
  },
  {
    "path": "src/components/http/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: HTTP\nsite_url: https://athenaframework.org/HTTP/\nrepo_url: https://github.com/athena-framework/http\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-mime/src/athena-mime.cr\n            - ./lib/athena-http/src/athena-http.cr\n          source_locations:\n            lib/athena-http: https://github.com/athena-framework/http/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/http/shard.yml",
    "content": "name: athena-http\n\nversion: 0.1.0\n\ncrystal: ~> 1.4\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/http\n\ndocumentation: https://athenaframework.org/HTTP\n\nShared common HTTP abstractions/utilities: |\n  Shared common HTTP abstractions/utilities.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n"
  },
  {
    "path": "src/components/http/spec/assets/.unknownextension",
    "content": ""
  },
  {
    "path": "src/components/http/spec/assets/directory/.empty",
    "content": ""
  },
  {
    "path": "src/components/http/spec/assets/file-big.txt",
    "content": "I'm not big, but I'm big enough to carry more than 50 bytes inside me."
  },
  {
    "path": "src/components/http/spec/assets/file-small.txt",
    "content": "I'm a file with less than 50 bytes."
  },
  {
    "path": "src/components/http/spec/assets/foo.txt",
    "content": "foo\n"
  },
  {
    "path": "src/components/http/spec/assets/fööö.html",
    "content": "<h1>Hello!</h1>\n"
  },
  {
    "path": "src/components/http/spec/assets/webkitdirectory/nested/test.txt",
    "content": "nested webkitdirectory text\n"
  },
  {
    "path": "src/components/http/spec/assets/webkitdirectory/test.txt",
    "content": "webkitdirectory text\n"
  },
  {
    "path": "src/components/http/spec/binary_file_response_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct AHTTP::BinaryFileResponseTest < ASPEC::TestCase\n  def test_new_without_disposition : Nil\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/foo.txt\", 418, ::HTTP::Headers{\"FOO\" => \"BAR\"}, true, nil, true, true\n    response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n    response.headers[\"FOO\"]?.should eq \"BAR\"\n    response.headers.has_key?(\"etag\").should be_true\n    response.headers.has_key?(\"last-modified\").should be_true\n    response.headers.has_key?(\"content-disposition\").should be_false\n  end\n\n  def test_new_with_disposition : Nil\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/foo.txt\", 418, public: true, content_disposition: :inline\n    response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n    response.headers.has_key?(\"etag\").should be_false\n    response.headers.has_key?(\"content-disposition\").should be_true\n    response.headers[\"content-disposition\"]?.should eq %(inline; filename=foo.txt)\n  end\n\n  def test_new_with_non_ascii_filename : Nil\n    AHTTP::BinaryFileResponse.new(\"#{__DIR__}/assets/fööö.html\").file.basename.should eq \"fööö.html\"\n  end\n\n  def test_set_file_unreadable : Nil\n    pending! \"Windows does not have unreadable files\" if {{ flag? :windows }}\n\n    path = Path[Dir.tempdir, \"unreadable\"].to_s\n\n    begin\n      ::File.write path, \"\", 0\n\n      ex = expect_raises AHTTP::Exception::File, \"The file must be readable.\" do\n        AHTTP::BinaryFileResponse.new path\n      end\n\n      ex.file.should eq path\n    ensure\n      ::File.delete? path\n    end\n  end\n\n  def test_set_content : Nil\n    expect_raises(::Exception, \"The content cannot be set on a BinaryFileResponse instance.\") do\n      AHTTP::BinaryFileResponse.new(__FILE__).content = \"FOO\"\n    end\n  end\n\n  def test_content : Nil\n    AHTTP::BinaryFileResponse.new(__FILE__).content.should be_empty\n  end\n\n  def test_set_content_disposition_custom_fallback_filename : Nil\n    response = AHTTP::BinaryFileResponse.new __FILE__\n    response.set_content_disposition :attachment, \"föö.html\", \"FILE\"\n\n    response.headers[\"content-disposition\"]?.should eq %(attachment; filename=FILE; filename*=utf-8''f%C3%B6%C3%B6.html)\n  end\n\n  def test_set_content_disposition_custom_filename : Nil\n    response = AHTTP::BinaryFileResponse.new __FILE__\n    response.set_content_disposition :attachment, \"foo.html\"\n\n    response.headers[\"content-disposition\"]?.should eq %(attachment; filename=foo.html)\n  end\n\n  def test_range_requests_without_last_modified_header : Nil\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\", content_disposition: nil, auto_last_modified: false\n\n    # Request to get ETag\n    request = AHTTP::Request.new \"GET\", \"/\", ::HTTP::Headers{\n      \"if-range\" => Time::Format::HTTP_DATE.format(Time.utc),\n      \"range\"    => \"bytes=1-4\",\n    }\n\n    response.prepare request\n    output = String.build do |io|\n      response.write io\n    end\n\n    ::File.read(\"#{__DIR__}/assets/test.gif\").should eq output\n    response.headers.has_key?(\"content-range\").should be_false\n  end\n\n  def test_range_on_post_method : Nil\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\"\n\n    request = AHTTP::Request.new \"POST\", \"/\", ::HTTP::Headers{\"range\" => \"bytes=10-20\"}\n\n    expected_output = ::File.open \"#{__DIR__}/assets/test.gif\", &.read_string(35)\n\n    response.prepare request\n\n    output = String.build do |io|\n      response.write io\n    end\n\n    output.should eq expected_output\n    response.status.should eq ::HTTP::Status::OK\n    response.headers[\"content-length\"].should eq \"35\"\n    response.headers.has_key?(\"content-range\").should be_false\n  end\n\n  def test_unprepared_response_sends_full_file : Nil\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\"\n\n    expected_output = ::File.read \"#{__DIR__}/assets/test.gif\"\n\n    output = String.build do |io|\n      response.write io\n    end\n\n    output.should eq expected_output\n    response.status.should eq ::HTTP::Status::OK\n  end\n\n  def test_delete_file_after_send : Nil\n    path = Path[Dir.tempdir, \"unreadable\"]\n\n    begin\n      ::File.touch path\n\n      ::File.file?(path).should be_true\n\n      request = AHTTP::Request.new \"GET\", \"/\"\n\n      response = AHTTP::BinaryFileResponse.new path\n      response.delete_file_after_send = true\n\n      response.prepare request\n      response.write IO::Memory.new\n\n      ::File.file?(path).should be_false\n    ensure\n      ::File.delete? path\n    end\n  end\n\n  def test_accept_range_unsafe_methods : Nil\n    request = AHTTP::Request.new \"POST\", \"/\"\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\"\n\n    response.prepare request\n\n    response.headers[\"accept-ranges\"]?.should eq \"none\"\n  end\n\n  def test_accept_range_not_overridden : Nil\n    request = AHTTP::Request.new \"POST\", \"/\"\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\", headers: ::HTTP::Headers{\"accept-ranges\" => \"foo\"}\n\n    response.prepare request\n\n    response.headers[\"accept-ranges\"]?.should eq \"foo\"\n  end\n\n  def test_prepare_cache_request_etag : Nil\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\"if-none-match\" => \"\\\"ETAG\\\"\"}\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\", headers: ::HTTP::Headers{\"accept-ranges\" => \"foo\", \"etag\" => \"\\\"ETAG\\\"\"}\n\n    response.prepare request\n\n    response.status.should eq ::HTTP::Status::NOT_MODIFIED\n    response.headers.has_key?(\"date\").should be_true\n    response.headers.has_key?(\"content-length\").should be_false\n    response.headers.has_key?(\"content-type\").should be_false\n  end\n\n  def test_prepare_cache_request_etag_star : Nil\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\"if-none-match\" => \"*\"}\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\", headers: ::HTTP::Headers{\"accept-ranges\" => \"foo\", \"etag\" => \"\\\"ETAG\\\"\"}\n\n    response.prepare request\n\n    response.status.should eq ::HTTP::Status::NOT_MODIFIED\n    response.headers.has_key?(\"date\").should be_true\n    response.headers.has_key?(\"content-length\").should be_false\n    response.headers.has_key?(\"content-type\").should be_false\n  end\n\n  def test_prepare_cache_request_last_modified : Nil\n    now = Time.utc\n\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\"if-modified-since\" => ::HTTP.format_time(now)}\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\", headers: ::HTTP::Headers{\"accept-ranges\" => \"foo\", \"last-modified\" => ::HTTP.format_time(now)}\n\n    response.prepare request\n\n    response.status.should eq ::HTTP::Status::NOT_MODIFIED\n    response.headers.has_key?(\"date\").should be_true\n    response.headers.has_key?(\"content-length\").should be_false\n    response.headers.has_key?(\"content-type\").should be_false\n  end\n\n  @[DataProvider(\"ranges\")]\n  def test_requests(request_range : String, offset : Int32, length : Int32, response_range : String) : Nil\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\", auto_etag: true\n\n    # Request to get ETag\n    request = AHTTP::Request.new \"GET\", \"/\"\n    response.prepare request\n    etag = response.headers[\"etag\"]\n\n    # Request for a range of test file\n    request = AHTTP::Request.new \"GET\", \"/\", ::HTTP::Headers{\"if-range\" => etag, \"range\" => request_range}\n\n    expected_output = ::File.open(\"#{__DIR__}/assets/test.gif\", &.read_at(offset, length, &.gets_to_end))\n\n    response.prepare request\n    output = String.build do |io|\n      response.write io\n    end\n\n    output.should eq expected_output\n    response.status.should eq ::HTTP::Status::PARTIAL_CONTENT\n    response.headers[\"content-range\"]?.should eq response_range\n    response.headers[\"content-length\"]?.should eq length.to_s\n  end\n\n  @[DataProvider(\"ranges\")]\n  def test_requests_without_etag(request_range : String, offset : Int32, length : Int32, response_range : String) : Nil\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\"\n\n    # Request to get LastModified\n    request = AHTTP::Request.new \"GET\", \"/\"\n    response.prepare request\n    last_modified = response.headers[\"last-modified\"]\n\n    # Request for a range of test file\n    request = AHTTP::Request.new \"GET\", \"/\", ::HTTP::Headers{\"if-range\" => last_modified, \"range\" => request_range}\n\n    expected_output = ::File.open(\"#{__DIR__}/assets/test.gif\", &.read_at(offset, length, &.gets_to_end))\n\n    response.prepare request\n    output = String.build do |io|\n      response.write io\n    end\n\n    output.should eq expected_output\n    response.status.should eq ::HTTP::Status::PARTIAL_CONTENT\n    response.headers[\"content-range\"]?.should eq response_range\n  end\n\n  def ranges : Tuple\n    {\n      {\"bytes=1-4\", 1, 4, \"bytes 1-4/35\"},\n      {\"bytes=-5\", 30, 5, \"bytes 30-34/35\"},\n      {\"bytes=30-\", 30, 5, \"bytes 30-34/35\"},\n      {\"bytes=30-30\", 30, 1, \"bytes 30-30/35\"},\n      {\"bytes=30-34\", 30, 5, \"bytes 30-34/35\"},\n      {\"bytes=30-40\", 30, 5, \"bytes 30-34/35\"},\n    }\n  end\n\n  @[DataProvider(\"full_file_ranges\")]\n  def test_full_file_requests(request_range : String) : Nil\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\", auto_etag: true\n    request = AHTTP::Request.new \"GET\", \"/\", ::HTTP::Headers{\"range\" => request_range}\n\n    expected_output = ::File.open \"#{__DIR__}/assets/test.gif\", &.read_string(35)\n\n    response.prepare request\n\n    output = String.build do |io|\n      response.write io\n    end\n\n    output.should eq expected_output\n    response.status.should eq ::HTTP::Status::OK\n  end\n\n  def full_file_ranges : Tuple\n    {\n      {\"bytes=0-\"},\n      {\"bytes=0-34\"},\n      {\"bytes=-35\"},\n      # Syntactical invalid range-request should also return the full resource\n      {\"bytes=20-10\"},\n      {\"bytes=50-40\"},\n      # range units other than bytes must be ignored\n      {\"unknown=10-20\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_ranges\")]\n  def test_invalid_requests(request_range : String) : Nil\n    response = AHTTP::BinaryFileResponse.new \"#{__DIR__}/assets/test.gif\", auto_etag: true\n    request = AHTTP::Request.new \"GET\", \"/\", ::HTTP::Headers{\"range\" => request_range}\n\n    response.prepare request\n    response.write IO::Memory.new\n\n    response.status.should eq ::HTTP::Status::RANGE_NOT_SATISFIABLE\n    response.headers[\"content-range\"]?.should eq \"bytes */35\"\n  end\n\n  def invalid_ranges : Tuple\n    {\n      {\"bytes=-40\"},\n      {\"bytes=40-50\"},\n    }\n  end\n\n  def test_content_disposition_to_s : Nil\n    AHTTP::BinaryFileResponse::ContentDisposition::Attachment.to_s.should eq \"attachment\"\n    AHTTP::BinaryFileResponse::ContentDisposition::Inline.to_s.should eq \"inline\"\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/ext/conversion_types_spec.cr",
    "content": "private enum Color\n  Red\n  Green\n  Blue\nend\n\ndescribe Athena::HTTP do\n  describe \".from_parameter\" do\n    describe Number do\n      it Int64 do\n        Int64.from_parameter(\"123\").should eq 123_i64\n      end\n\n      it \"Int with whitespace\" do\n        expect_raises ArgumentError, \"Invalid Int32\" do\n          Int32.from_parameter(\"   123\")\n        end\n      end\n\n      it Float32 do\n        Float32.from_parameter(\"3.14\").should eq 3.14_f32\n      end\n\n      it \"Float with whitespace\" do\n        expect_raises ArgumentError, \"Invalid Float64\" do\n          Float64.from_parameter(\"   123.5\")\n        end\n      end\n    end\n\n    describe Bool do\n      it \"true\" do\n        Bool.from_parameter(\"true\").should be_true\n        Bool.from_parameter(\"on\").should be_true\n        Bool.from_parameter(\"1\").should be_true\n        Bool.from_parameter(\"yes\").should be_true\n      end\n\n      it \"false\" do\n        Bool.from_parameter(\"false\").should be_false\n        Bool.from_parameter(\"off\").should be_false\n        Bool.from_parameter(\"0\").should be_false\n        Bool.from_parameter(\"no\").should be_false\n      end\n\n      it \"invalid\" do\n        expect_raises ArgumentError, \"Invalid Bool\" do\n          Bool.from_parameter(\"foo\")\n        end\n      end\n    end\n\n    it Enum do\n      Color.from_parameter(\"green\").should eq Color::Green\n    end\n\n    it Object do\n      str = \"foo\"\n\n      String.from_parameter(str).should be str\n    end\n\n    describe Array do\n      it \"single type\" do\n        Array(Int32).from_parameter([1, 2]).should eq [1, 2]\n      end\n\n      it \"Union type\" do\n        Array(Int32 | Bool).from_parameter([1, false]).should eq [1, false]\n      end\n    end\n\n    describe Nil do\n      it \"valid\" do\n        Nil.from_parameter(\"null\").should be_nil\n      end\n\n      it \"invalid\" do\n        expect_raises ArgumentError, \"Invalid Nil\" do\n          Nil.from_parameter(\"foo\")\n        end\n      end\n    end\n  end\n\n  describe \".from_parameter?\" do\n    describe Number do\n      it Int64 do\n        Int64.from_parameter?(\"123\").should eq 123_i64\n      end\n\n      it \"Int with whitespace\" do\n        Int32.from_parameter?(\"   123\").should be_nil\n      end\n\n      it Float32 do\n        Float32.from_parameter?(\"3.14\").should eq 3.14_f32\n      end\n\n      it \"Float with whitespace\" do\n        Float64.from_parameter?(\"   123.5\").should be_nil\n      end\n    end\n\n    describe Bool do\n      it \"true\" do\n        Bool.from_parameter?(\"true\").should be_true\n        Bool.from_parameter?(\"on\").should be_true\n        Bool.from_parameter?(\"1\").should be_true\n        Bool.from_parameter?(\"yes\").should be_true\n      end\n\n      it \"false\" do\n        Bool.from_parameter?(\"false\").should be_false\n        Bool.from_parameter?(\"off\").should be_false\n        Bool.from_parameter?(\"0\").should be_false\n        Bool.from_parameter?(\"no\").should be_false\n      end\n\n      it \"invalid\" do\n        Bool.from_parameter?(\"foo\").should be_nil\n      end\n    end\n\n    it Enum do\n      Color.from_parameter?(\"green\").should eq Color::Green\n      Color.from_parameter?(\"black\").should be_nil\n    end\n\n    it Object do\n      str = \"foo\"\n\n      String.from_parameter?(str).should be str\n    end\n\n    describe Array do\n      it \"single type\" do\n        Array(Int32).from_parameter?([1, 2]).should eq [1, 2]\n      end\n\n      it \"Union type\" do\n        Array(Int32 | Bool).from_parameter?([1, false]).should eq [1, false]\n      end\n    end\n\n    describe Nil do\n      it \"valid\" do\n        Nil.from_parameter?(\"null\").should be_nil\n      end\n\n      it \"invalid\" do\n        Nil.from_parameter?(\"foo\").should be_nil\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/file_spec.cr",
    "content": "struct FileTest < ASPEC::TestCase\n  def test_initialize_non_existent_file : Nil\n    ex = expect_raises ::AHTTP::Exception::FileNotFound, \"The file does not exist.\" do\n      AHTTP::File.new \"#{__DIR__}/assets/missing\"\n    end\n\n    ex.file.should eq \"#{__DIR__}/assets/missing\"\n  end\n\n  def test_mime_type : Nil\n    file = AHTTP::File.new \"#{__DIR__}/assets/test.gif\"\n    file.mime_type.should eq \"image/gif\"\n  end\n\n  def test_guess_extension_unknown : Nil\n    file = AHTTP::File.new \"#{__DIR__}/assets/directory/.empty\"\n    file.guess_extension.should be_nil\n  end\n\n  def test_guess_extension_known : Nil\n    pending! \"MIME guessing is not available\" if {{ flag?(\"windows\") && !flag?(\"gnu\") }}\n\n    file = AHTTP::File.new \"#{__DIR__}/assets/test\"\n    file.guess_extension.should eq \"gif\"\n  end\n\n  def test_move : Nil\n    path = \"#{Dir.tempdir}/test.copy.gif\"\n    target_dir = \"#{Dir.tempdir}/test\"\n    target_path = \"#{target_dir}/test.copy.gif\"\n    ::File.delete? path\n    ::File.delete? target_path\n    ::File.copy \"#{__DIR__}/assets/test.gif\", path\n    Dir.mkdir target_dir unless Dir.exists? target_dir\n\n    file = AHTTP::File.new path\n    moved_file = file.move target_dir\n    moved_file.should be_a AHTTP::File\n\n    ::File.exists?(target_path).should be_true\n    ::File.exists?(path).should be_false\n    ::File.realpath(target_path).should eq moved_file.realpath\n\n    FileUtils.rm_rf target_dir\n  end\n\n  def test_move_new_name : Nil\n    path = \"#{Dir.tempdir}/test.copy.gif\"\n    target_dir = \"#{Dir.tempdir}/test\"\n    target_path = \"#{target_dir}/test.new.gif\"\n    ::File.delete? path\n    ::File.delete? target_path\n    ::File.copy \"#{__DIR__}/assets/test.gif\", path\n    Dir.mkdir target_dir unless Dir.exists? target_dir\n\n    file = AHTTP::File.new path\n    moved_file = file.move target_dir, \"test.new.gif\"\n\n    ::File.exists?(target_path).should be_true\n    ::File.exists?(path).should be_false\n    ::File.realpath(target_path).should eq moved_file.realpath\n\n    FileUtils.rm_rf target_dir\n  end\n\n  def test_move_non_existent_directory : Nil\n    path = \"#{Dir.tempdir}/test.copy.gif\"\n    target_dir = \"#{Dir.tempdir}/test\"\n    target_path = \"#{target_dir}/test.copy.gif\"\n    ::File.delete? path\n    ::File.delete? target_path\n    ::File.copy \"#{__DIR__}/assets/test.gif\", path\n    FileUtils.rm_rf target_dir\n\n    file = AHTTP::File.new path\n    moved_file = file.move target_dir\n    moved_file.should be_a AHTTP::File\n\n    ::File.exists?(target_path).should be_true\n    ::File.exists?(path).should be_false\n    ::File.realpath(target_path).should eq moved_file.realpath\n\n    FileUtils.rm_rf target_dir\n  end\n\n  @[TestWith(\n    {\"original.gif\", \"original.gif\"},\n    {\"..\\\\..\\\\original.gif\", \"original.gif\"},\n    {\"../../original.gif\", \"original.gif\"},\n\n    {\"файлfile.gif\", \"файлfile.gif\"},\n    {\"..\\\\..\\\\файлfile.gif\", \"файлfile.gif\"},\n    {\"../../файлfile.gif\", \"файлfile.gif\"},\n  )]\n  def test_move_non_latin_names(filename : String, sanitized_filename : String) : Nil\n    path = \"#{Dir.tempdir}/#{sanitized_filename}\"\n    target_dir = \"#{Dir.tempdir}/test\"\n    target_path = \"#{target_dir}/#{sanitized_filename}\"\n    ::File.delete? path\n    ::File.delete? target_path\n    ::File.copy \"#{__DIR__}/assets/test.gif\", path\n    Dir.mkdir target_dir unless Dir.exists? target_dir\n\n    file = AHTTP::File.new path\n    moved_file = file.move target_dir, filename\n\n    ::File.exists?(target_path).should be_true\n    ::File.exists?(path).should be_false\n    ::File.realpath(target_path).should eq moved_file.realpath\n\n    FileUtils.rm_rf target_dir\n  end\n\n  def test_realpath : Nil\n    AHTTP::File.new(\"#{__DIR__}/../spec/assets/foo.txt\").realpath.should eq ::File.realpath(Path[__DIR__, \"assets\", \"foo.txt\"].to_s)\n  end\n\n  def test_basename : Nil\n    AHTTP::File.new(\"#{__DIR__}/assets/foo.txt\").basename.should eq \"foo.txt\"\n    AHTTP::File.new(\"#{__DIR__}/assets/foo.txt\").basename(\".txt\").should eq \"foo\"\n  end\n\n  def test_content : Nil\n    AHTTP::File.new(__FILE__).content.should eq ::File.read __FILE__\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/header_utils_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct AHTTP::HeaderUtilsTest < ASPEC::TestCase\n  @[TestWith(\n    {\"foo\", \"foo\"},\n    {\"az09!#$%&'*.^_`|~-\", \"az09!#$%&'*.^_`|~-\"},\n    {\"\\\"foo bar\\\"\", \"foo bar\"},\n    {\"\\\"foo [bar]\\\"\", \"foo [bar]\"},\n    {\"\\\"foo \\\\\\\"bar\\\\\\\"\\\"\", \"foo \\\"bar\\\"\"},\n    {\"\\\"foo \\\\\\\"\\\\b\\\\a\\\\r\\\\\\\"\\\"\", \"foo \\\"bar\\\"\"},\n    {\"\\\"foo \\\\\\\\ bar\\\"\", \"foo \\\\ bar\"},\n  )]\n  def test_unquote(input : String, expected : String) : Nil\n    AHTTP::HeaderUtils.unquote(input).should eq expected\n  end\n\n  @[TestWith(\n    {[[\"foo\", \"123\"]], {\"foo\" => \"123\"}},\n    {[[\"foo\"]], {\"foo\" => true}},\n    {[[\"Foo\"]], {\"foo\" => true}},\n    {[[\"foo\", \"123\"], [\"bar\"]], {\"foo\" => \"123\", \"bar\" => true}}\n  )]\n  def test_combine(input : Array, expected : Hash) : Nil\n    AHTTP::HeaderUtils.combine(input).should eq expected\n  end\n\n  def test_to_string : Nil\n    AHTTP::HeaderUtils.to_string({\"foo\" => true}, ',').should eq \"foo\"\n    AHTTP::HeaderUtils.to_string({\"foo\" => true, \"bar\" => true}, ';').should eq \"foo;bar\"\n    AHTTP::HeaderUtils.to_string({\"foo\" => 123}, ',').should eq \"foo=123\"\n    AHTTP::HeaderUtils.to_string({\"foo\" => \"1 2 3\"}, ',').should eq \"foo=1\\\\ 2\\\\ 3\"\n    AHTTP::HeaderUtils.to_string({\"foo\" => \"1 2 3\", \"bar\" => true}, ',').should eq \"foo=1\\\\ 2\\\\ 3,bar\"\n\n    # Named arg overload\n    AHTTP::HeaderUtils.to_string(\"-\", foo: true, bar: 2.0).should eq \"foo-bar=2.0\"\n\n    # IO overload\n    String.build do |io|\n      io << '~'\n      AHTTP::HeaderUtils.to_string io, {\"foo\" => true, \"bar\" => 100, \"baz\" => false}, \"|\"\n      io << '~'\n    end.should eq \"~foo|bar=100|baz=false~\"\n  end\n\n  @[DataProvider(\"headers_to_split\")]\n  def test_split(expected : Array, header : String, separator : String) : Nil\n    AHTTP::HeaderUtils.split(header, separator).should eq expected\n  end\n\n  def headers_to_split : Tuple\n    {\n      {[\"foo=123\", \"bar\"], \"foo=123,bar\", \",\"},\n      {[\"foo=123\", \"bar\"], \"foo=123, bar\", \",\"},\n      {[[\"foo=123\", \"bar\"]], \"foo=123; bar\", \",;\"},\n      {[[\"foo=123\"], [\"bar\"]], \"foo=123, bar\", \",;\"},\n      {[\"foo\", \"123, bar\"], \"foo=123, bar\", \"=\"},\n      {[\"foo\", \"123, bar\"], \" foo = 123, bar \", \"=\"},\n      {[[\"foo\", \"123\"], [\"bar\"]], \"foo=123, bar\", \",=\"},\n      {[[[\"foo\", \"123\"]], [[\"bar\"], [\"foo\", \"456\"]]], \"foo=123, bar;; foo=456\", \",;=\"},\n      {[[[\"foo\", \"a,b;c=d\"]]], \"foo=\\\"a,b;c=d\\\"\", \",;=\"},\n\n      {[\"foo\", \"bar\"], \"foo,,,, bar\", \",\"},\n      {[\"foo\", \"bar\"], \",foo, bar,\", \",\"},\n      {[\"foo\", \"bar\"], \" , foo, bar, \", \",\"},\n      {[\"foo bar\"], \"foo \\\"bar\\\"\", \",\"},\n      {[\"foo bar\"], \"\\\"foo\\\" bar\", \",\"},\n      {[\"foo bar\"], \"\\\"foo\\\" \\\"bar\\\"\", \",\"},\n\n      {[[\"foo_cookie\", \"foo=1&bar=2&baz=3\"], [\"expires\", \"Tue, 22-Sep-2020 06:27:09 GMT\"], [\"path\", \"/\"]], \"foo_cookie=foo=1&bar=2&baz=3; expires=Tue, 22-Sep-2020 06:27:09 GMT; path=/\", \";=\"},\n      {[[\"foo_cookie\", \"foo==\"], [\"expires\", \"Tue, 22-Sep-2020 06:27:09 GMT\"], [\"path\", \"/\"]], \"foo_cookie=foo==; expires=Tue, 22-Sep-2020 06:27:09 GMT; path=/\", \";=\"},\n      {[[\"foo_cookie\", \"foo=\"], [\"expires\", \"Tue, 22-Sep-2020 06:27:09 GMT\"], [\"path\", \"/\"]], \"foo_cookie=foo=; expires=Tue, 22-Sep-2020 06:27:09 GMT; path=/\", \";=\"},\n      {[[\"foo_cookie\", \"foo=a=b\"], [\"expires\", \"Tue, 22-Sep-2020 06:27:09 GMT\"], [\"path\", \"/\"]], \"foo_cookie=foo=\\\"a=b\\\"; expires=Tue, 22-Sep-2020 06:27:09 GMT; path=/\", \";=\"},\n\n      # These are not a valid header values. We test that they parse anyway, and that both the valid and invalid parts are returned.\n      {[] of String, \"\", \",\"},\n      {[] of String, \",,,\", \",\"},\n      {[[\"\", \"foo\"], [\"bar\", \"\"]], \"=foo,bar=\", \",=\"},\n      {[\"foo\", \"foobar\", \"baz\"], \"foo, foo\\\"bar\\\", \\\"baz\", \",\"},\n      {[\"foo\", \"bar, baz\"], \"foo, \\\"bar, baz\", \",\"},\n      {[\"foo\", \"bar, baz\\\\\"], \"foo, \\\"bar, baz\\\\\", \",\"},\n      {[\"foo\", \"bar, baz\\\\\"], \"foo, \\\"bar, baz\\\\\", \",\"},\n    }\n  end\n\n  @[DataProvider(\"dispositions\")]\n  def test_make_disposition(disposition : AHTTP::BinaryFileResponse::ContentDisposition, filename : String, fallback_filename : String?, expected : String) : Nil\n    AHTTP::HeaderUtils.make_disposition(disposition, filename, fallback_filename).should eq expected\n  end\n\n  def dispositions : Tuple\n    {\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"foo.html\", \"foo.html\", \"attachment; filename=foo.html\"},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"foo.html\", nil, \"attachment; filename=foo.html\"},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"foo bar.html\", nil, \"attachment; filename=foo\\\\ bar.html\"},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, %(foo \"bar\".html), nil, %(attachment; filename=foo\\\\ \\\\\"bar\\\\\".html)},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"foo%20bar.html\", \"foo bar.html\", \"attachment; filename=foo\\\\ bar.html; filename*=utf-8''foo%2520bar.html\"},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"föö.html\", \"foo.html\", \"attachment; filename=foo.html; filename*=utf-8''f%C3%B6%C3%B6.html\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_dispositions\")]\n  def test_invalid_dispositions(disposition : AHTTP::BinaryFileResponse::ContentDisposition, filename : String, expected : String, fallback_filename : String? = nil) : Nil\n    expect_raises ArgumentError, expected do\n      AHTTP::HeaderUtils.make_disposition disposition, filename, fallback_filename\n    end\n  end\n\n  def invalid_dispositions : Tuple\n    {\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"foo%20bar.html\", \"The fallback filename cannot contain the '%' character.\", nil},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"foo/bar.html\", \"The filename cannot include path separators.\", nil},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"/foo.html\", \"The filename cannot include path separators.\", nil},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"foo\\\\bar.html\", \"The filename cannot include path separators.\", nil},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"\\\\foo.html\", \"The filename cannot include path separators.\", nil},\n      {AHTTP::BinaryFileResponse::ContentDisposition::Attachment, \"foo.html\", \"The fallback filename cannot include path separators.\", \"f/oo.html\"},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/ip_utils_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct AHTTP::HeaderUtilsTest < ASPEC::TestCase\n  def test_separate_caches_per_protocol : Nil\n    ip = \"192.168.52.1\"\n    subnet = \"192.168.0.0/16\"\n\n    AHTTP::IPUtils.check_ipv6(ip, subnet).should be_false\n    AHTTP::IPUtils.check_ipv4(ip, subnet).should be_true\n\n    ip = \"2a01:198:603:0:396e:4789:8e99:890f\"\n    subnet = \"2a01:198:603:0::/65\"\n\n    AHTTP::IPUtils.check_ipv4(ip, subnet).should be_false\n    AHTTP::IPUtils.check_ipv6(ip, subnet).should be_true\n  end\n\n  @[DataProvider(\"ipv4_data\")]\n  def test_check_ipv4(is_match : Bool, remote_address : String, cidr : String | Array(String)) : Nil\n    AHTTP::IPUtils.check(remote_address, cidr).should eq is_match\n  end\n\n  def ipv4_data : Tuple\n    {\n      {true, \"192.168.1.1\", \"192.168.1.1\"},\n      {true, \"192.168.1.1\", \"192.168.1.1/1\"},\n      {true, \"192.168.1.1\", \"192.168.1.0/24\"},\n      {false, \"192.168.1.1\", \"1.2.3.4/1\"},\n      {false, \"192.168.1.1\", \"192.168.1.1/33\"}, # invalid subnet\n      {true, \"192.168.1.1\", [\"1.2.3.4/1\", \"192.168.1.0/24\"]},\n      {true, \"192.168.1.1\", [\"192.168.1.0/24\", \"1.2.3.4/1\"]},\n      {false, \"192.168.1.1\", [\"1.2.3.4/1\", \"4.3.2.1/1\"]},\n      {true, \"1.2.3.4\", \"0.0.0.0/0\"},\n      {true, \"1.2.3.4\", \"192.168.1.0/0\"},\n      {false, \"1.2.3.4\", \"256.256.256/0\"}, # invalid CIDR notation\n      {false, \"an_invalid_ip\", \"192.168.1.0/24\"},\n      {false, \"\", \"1.2.3.4/1\"},\n    }\n  end\n\n  @[DataProvider(\"ipv6_data\")]\n  def test_check_ipv6(is_match : Bool, remote_address : String, cidr : String | Array(String)) : Nil\n    AHTTP::IPUtils.check(remote_address, cidr).should eq is_match\n  end\n\n  def ipv6_data : Tuple\n    {\n      {true, \"2a01:198:603:0:396e:4789:8e99:890f\", \"2a01:198:603:0::/65\"},\n      {false, \"2a00:198:603:0:396e:4789:8e99:890f\", \"2a01:198:603:0::/65\"},\n      {false, \"2a01:198:603:0:396e:4789:8e99:890f\", \"::1\"},\n      {true, \"0:0:0:0:0:0:0:1\", \"::1\"},\n      {false, \"0:0:603:0:396e:4789:8e99:0001\", \"::1\"},\n      {true, \"0:0:603:0:396e:4789:8e99:0001\", \"::/0\"},\n      {true, \"0:0:603:0:396e:4789:8e99:0001\", \"2a01:198:603:0::/0\"},\n      {true, \"2a01:198:603:0:396e:4789:8e99:890f\", [\"::1\", \"2a01:198:603:0::/65\"]},\n      {true, \"2a01:198:603:0:396e:4789:8e99:890f\", [\"2a01:198:603:0::/65\", \"::1\"]},\n      {false, \"2a01:198:603:0:396e:4789:8e99:890f\", [\"::1\", \"1a01:198:603:0::/65\"]},\n      {false, \"}__test|O:21:&quot;JDatabaseDriverMysqli&quot;:3:{s:2\", \"::1\"},\n      {false, \"2a01:198:603:0:396e:4789:8e99:890f\", \"unknown\"},\n      {false, \"\", \"::1\"},\n      {false, \"127.0.0.1\", \"::1\"},\n      {false, \"0.0.0.0/8\", \"::1\"},\n      {false, \"::1\", \"127.0.0.1\"},\n      {false, \"::1\", \"0.0.0.0/8\"},\n      {true, \"::ffff:10.126.42.2\", \"::ffff:10.0.0.0/0\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_ip_address_data\")]\n  def test_invalid_ip_addresses(remote_address : String, cidr : String | Array(String)) : Nil\n    AHTTP::IPUtils.check_ipv4(remote_address, cidr).should be_false\n  end\n\n  def invalid_ip_address_data : Hash\n    {\n      \"invalid proxy wildcard\"                         => {\"192.168.20.13\", \"*\"},\n      \"invalid proxy missing netmask\"                  => {\"192.168.20.13\", \"0.0.0.0\"},\n      \"invalid request IP with invalid proxy wildcard\" => {\"0.0.0.0\", \"*\"},\n    }\n  end\n\n  @[DataProvider(\"ipv4_zero_mask_data\")]\n  def test_check_ipv4_zero_mask_data(is_match : Bool, remote_address : String, cidr : String | Array(String)) : Nil\n    AHTTP::IPUtils.check_ipv4(remote_address, cidr).should eq is_match\n  end\n\n  def ipv4_zero_mask_data : Tuple\n    {\n      {true, \"1.2.3.4\", \"0.0.0.0/0\"},\n      {true, \"1.2.3.4\", \"192.168.1.0/0\"},\n      {false, \"1.2.3.4\", \"256.256.256/0\"}, # invalid CIDR notation\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/parameter_bag_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate alias DATATYPE = Hash(String, Int32 | String)\n\ndescribe AHTTP::ParameterBag do\n  describe \"#has?\" do\n    it \"returns false if that value isn't in the bag\" do\n      bag = AHTTP::ParameterBag.new\n      bag.has?(\"value\").should be_false\n    end\n\n    it \"returns true if that value is in the bag\" do\n      bag = AHTTP::ParameterBag.new\n      bag.set \"value\", \"foo\"\n      bag.has?(\"value\").should be_true\n    end\n\n    describe \"with type\" do\n      it \"returns true with a valid type\" do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"value\", \"foo\"\n        bag.set \"num\", 1\n        bag.set \"nil\", nil\n        bag.has?(\"value\", String).should be_true\n        bag.has?(\"num\", Int32).should be_true\n        bag.has?(\"nil\", Nil).should be_true\n      end\n\n      it \"returns false with a invalid type\" do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"value\", \"foo\"\n        bag.set \"num\", 1\n        bag.set \"nil\", nil\n        bag.has?(\"value\", Int32).should be_false\n        bag.has?(\"nil\", Int32).should be_false\n        bag.has?(\"num\", String).should be_false\n        bag.has?(\"num\", String?).should be_false\n        bag.has?(\"num\", Int32?).should be_false\n        bag.has?(\"nil\", Int32?).should be_false\n      end\n    end\n  end\n\n  describe \"#get?\" do\n    it \"returns nil if the value is missing\" do\n      bag = AHTTP::ParameterBag.new\n      bag.get?(\"value\").should be_nil\n    end\n\n    it \"returns the value is set\" do\n      bag = AHTTP::ParameterBag.new\n      bag.set \"value\", \"foo\"\n      bag.get?(\"value\").should eq \"foo\"\n    end\n\n    describe \"with a complex T\" do\n      it \"returns an nilable T\" do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"data\", {\"foo\" => \"bar\", \"baz\" => 10}, DATATYPE\n\n        data = bag.get \"data\", DATATYPE\n        data.class.should eq DATATYPE\n        data[\"foo\"].should eq \"bar\"\n      end\n    end\n\n    it \"returns nil if the value is set, but of a different type\" do\n      bag = AHTTP::ParameterBag.new\n      bag.set \"value\", \"foo\"\n      bag.get?(\"value\", Int32).should be_nil\n    end\n  end\n\n  describe \"#get\" do\n    describe \"by name\" do\n      it \"raises if the value is missing\" do\n        bag = AHTTP::ParameterBag.new\n        expect_raises KeyError, \"No parameter exists with the name 'value'.\" do\n          bag.get \"value\"\n        end\n      end\n\n      it \"returns the value is set\" do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"value\", \"foo\"\n        bag.get(\"value\").should eq \"foo\"\n      end\n\n      it \"is able to get falsey values\" do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"n\", nil\n        bag.set \"f\", false\n        bag.get(\"n\").should be_nil\n        bag.get(\"f\").should be_false\n      end\n    end\n\n    describe \"by name and type\" do\n      it String do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"value\", \"foo\"\n        value = bag.get \"value\", String\n        value.should eq \"foo\"\n        value.class.should eq String\n      end\n\n      it Bool do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"value\", true\n        value = bag.get \"value\", Bool\n        value.should be_true\n        value.class.should eq Bool\n      end\n\n      it Int do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"value\", 123\n        value = bag.get \"value\", Int32\n        value.should eq 123\n        value.class.should eq Int32\n      end\n\n      it Float do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"value\", 3.14\n        value = bag.get \"value\", Float64\n        value.should eq 3.14\n        value.class.should eq Float64\n      end\n\n      it Union do\n        bag = AHTTP::ParameterBag.new\n        bag.set \"pi\", 3.14, Float64\n        bag.set \"e\", 2.71, Float64\n        bag.set \"fav\", 16, Int32\n        bag.set \"data\", {\"foo\" => \"bar\", \"baz\" => 10}, DATATYPE\n\n        a, b, c = bag.get(\"pi\", Float64), bag.get(\"e\", Float64), bag.get(\"fav\", Int32)\n        (a + b + c).should eq 21.85\n\n        data = bag.get \"data\", DATATYPE\n        data.class.should eq DATATYPE\n        data[\"foo\"].should eq \"bar\"\n      end\n    end\n  end\n\n  describe \"#set\" do\n    it \"with name, type, and value\" do\n      AHTTP::ParameterBag.new.set(\"value\", \"foo\", String)\n    end\n\n    it \"with name and value\" do\n    end\n\n    it \"with a hash\" do\n      bag = AHTTP::ParameterBag.new\n      bag.set({\"key\" => \"value\", \"age\" => 123})\n      bag.get(\"key\").should eq \"value\"\n      bag.get(\"age\").should eq 123\n    end\n  end\n\n  it \"#remove\" do\n    bag = AHTTP::ParameterBag.new\n    bag.set \"value\", \"foo\"\n    bag.has?(\"value\").should be_true\n    bag.remove \"value\"\n    bag.has?(\"value\").should be_false\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/redirect_response_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe AHTTP::RedirectResponse do\n  describe \".new\" do\n    it \"raises if the url is empty\" do\n      expect_raises(ArgumentError, \"Cannot redirect to an empty URL.\") do\n        AHTTP::RedirectResponse.new \"\"\n      end\n    end\n\n    it \"allows passing a `Path` instance\" do\n      response = AHTTP::RedirectResponse.new Path[\"/app/assets/foo.txt\"]\n\n      response.status.should eq ::HTTP::Status::FOUND\n      response.headers[\"location\"].should eq \"/app/assets/foo.txt\"\n      response.content.should be_empty\n    end\n  end\n\n  describe \"#status\" do\n    it \"defaults to 302\" do\n      AHTTP::RedirectResponse.new(\"address\").status.should eq ::HTTP::Status::FOUND\n    end\n\n    it \"disallows non redirect codes\" do\n      expect_raises(ArgumentError, \"'422' is not an HTTP redirect status code.\") do\n        AHTTP::RedirectResponse.new(\"address\", 422)\n      end\n    end\n\n    it Int do\n      AHTTP::RedirectResponse.new(\"address\", 301).status.should eq ::HTTP::Status::MOVED_PERMANENTLY\n    end\n\n    it ::HTTP::Status do\n      AHTTP::RedirectResponse.new(\"address\", ::HTTP::Status::MOVED_PERMANENTLY).status.should eq ::HTTP::Status::MOVED_PERMANENTLY\n    end\n  end\n\n  describe \"#headers\" do\n    it \"with an empty url\" do\n      expect_raises(ArgumentError, \"Cannot redirect to an empty URL.\") do\n        AHTTP::RedirectResponse.new(\"\")\n      end\n    end\n\n    it \"adds the location header\" do\n      AHTTP::RedirectResponse.new(\"address\").headers[\"location\"].should eq \"address\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/request_matcher/attributes_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct AttributesRequestMatcherTest < ASPEC::TestCase\n  @[TestWith(\n    {\"foo\", %r(foo_.*), true},\n    {\"foo\", %r(foo), true},\n    {\"foo\", %r(^foo_bar$), true},\n    {\"foo\", %r(barbar), false},\n    {\"some_num\", %r(\\d\\d), false},\n  )]\n  def test_matches(key : String, regex : Regex, is_match : Bool) : Nil\n    matcher = AHTTP::RequestMatcher::Attributes.new({key => regex})\n    request = AHTTP::Request.new \"GET\", \"/admin/foo\"\n    request.attributes.set \"foo\", \"foo_bar\"\n    request.attributes.set \"some_num\", 42\n    matcher.matches?(request).should eq is_match\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/request_matcher/header_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct HeaderRequestMatcherTest < ASPEC::TestCase\n  @[TestWith(\n    {::HTTP::Headers{\"x-foo\" => \"foo\", \"bar\" => \"bar\", \"baz\" => \"baz\"}, true},\n    {::HTTP::Headers{\"x-foo\" => \"foo\", \"bar\" => \"bar\"}, true},\n    {::HTTP::Headers{\"bar\" => \"bar\", \"baz\" => \"baz\"}, false},\n    {::HTTP::Headers{\"bar\" => \"bar\"}, false},\n    {::HTTP::Headers.new, false},\n  )]\n  def test_matches(headers : ::HTTP::Headers, is_match : Bool) : Nil\n    matcher = AHTTP::RequestMatcher::Header.new \"x-foo\", \"bar\"\n    request = AHTTP::Request.new \"GET\", \"/\"\n\n    headers.each do |k, v|\n      request.headers[k] = v\n    end\n\n    matcher.matches?(request).should eq is_match\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/request_matcher/hostname_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct HostnameRequestMatcherTest < ASPEC::TestCase\n  @[TestWith(\n    { %r(.*.example.com), true },\n    { %r(.example.com$), true },\n    { %r(^.*.example.com$), true },\n    { %r(.*.crystal.com), false },\n    { %r(.*.example.COM), true },\n    { %r(.example.COM$), true },\n    { %r(^.*.example.COM$), true },\n    { %r(.*.crystal.COM), false },\n  )]\n  def test_matches(regex : Regex, is_match : Bool) : Nil\n    matcher = AHTTP::RequestMatcher::Hostname.new regex\n    request = AHTTP::Request.new \"GET\", \"/\", ::HTTP::Headers{\"host\" => \"foo.example.com\"}\n    matcher.matches?(request).should eq is_match\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/request_matcher/method_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct MethodRequestMatcherTest < ASPEC::TestCase\n  @[TestWith(\n    {\"get\", \"get\", true},\n    {\"get\", [\"get\", \"post\"], true},\n    {\"get\", \"post\", false},\n    {\"get\", \"GET\", true},\n    {\"get\", [\"GET\", \"POST\"], true},\n    {\"get\", \"POST\", false},\n  )]\n  def test_matches(request_method : String, matcher_methods : String | Enumerable(String), is_match : Bool) : Nil\n    matcher = AHTTP::RequestMatcher::Method.new matcher_methods\n    request = AHTTP::Request.new request_method, \"/\"\n    matcher.matches?(request).should eq is_match\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/request_matcher/path_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct PathRequestMatcherTest < ASPEC::TestCase\n  @[TestWith(\n    { %r(/admin/.*), true },\n    { %r(/admin), true },\n    { %r(^/admin/.*$), true },\n    { %r(/blog/.*), false },\n  )]\n  def test_matches(regex : Regex, is_match : Bool) : Nil\n    matcher = AHTTP::RequestMatcher::Path.new regex\n    request = AHTTP::Request.new \"GET\", \"/admin/foo\"\n    matcher.matches?(request).should eq is_match\n  end\n\n  def test_encoded_characters : Nil\n    matcher = AHTTP::RequestMatcher::Path.new %r(^/admin/fo o*$)\n    request = AHTTP::Request.new \"GET\", \"/admin/fo%20o\"\n    matcher.matches?(request).should be_true\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/request_matcher/query_parameter_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct QueryParameterRequestMatcherTest < ASPEC::TestCase\n  @[TestWith(\n    {\"foo=&bar=\", true},\n    {\"foo=foo1&bar=bar1\", true},\n    {\"foo=foo1&bar=bar1&baz=baz1\", true},\n    {\"foo=\", false},\n    {\"\", false},\n  )]\n  def test_matches(query_string : String, is_match : Bool) : Nil\n    matcher = AHTTP::RequestMatcher::QueryParameter.new \"foo\", \"bar\"\n    request = AHTTP::Request.new \"GET\", \"/\"\n    request.query = query_string\n\n    matcher.matches?(request).should eq is_match\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/request_matcher_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe AHTTP::RequestMatcher do\n  it \"matches\" do\n    matcher = AHTTP::RequestMatcher.new(\n      AHTTP::RequestMatcher::Path.new(%r(/admin/foo)),\n      AHTTP::RequestMatcher::Method.new(\"GET\"),\n    )\n\n    matcher.matches?(AHTTP::Request.new \"GET\", \"/admin/foo\").should be_true\n  end\n\n  it \"does not match\" do\n    matcher = AHTTP::RequestMatcher.new(\n      AHTTP::RequestMatcher::Method.new(\"POST\"),\n      AHTTP::RequestMatcher::Path.new(%r(/admin/foo)),\n    )\n\n    matcher.matches?(AHTTP::Request.new \"GET\", \"/admin/foo\").should be_false\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/request_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct AHTTP::RequestTest < ASPEC::TestCase\n  def tear_down : Nil\n    AHTTP::Request.set_trusted_hosts [] of Regex\n    AHTTP::Request.set_trusted_proxies [] of String, :none\n    AHTTP::Request.trusted_header_overrides.clear\n  end\n\n  def test_construct_with_self : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    request2 = AHTTP::Request.new request\n\n    request2.should be request\n  end\n\n  # This spec tests the built-in `#hostname` method\n  def test_hostname : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    request.hostname.should be_nil\n\n    request = AHTTP::Request.new \"GET\", \"/\", ::HTTP::Headers{\"host\" => \"www.domain.com\"}\n    request.hostname.should eq \"www.domain.com\"\n\n    request = AHTTP::Request.new \"GET\", \"/\", ::HTTP::Headers{\"host\" => \"www.domain.com:8080\"}\n    request.hostname.should eq \"www.domain.com\"\n\n    request = AHTTP::Request.new \"GET\", \"/\", ::HTTP::Headers{\"host\" => \"[::1]:8080\"}\n    request.hostname.should eq \"::1\"\n  end\n\n  def test_content_type_format_present : Nil\n    AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"content-type\" => \"application/json\",\n    }).content_type_format.should eq \"json\"\n  end\n\n  def test_content_type_format_missing : Nil\n    AHTTP::Request.new(\"GET\", \"/\").content_type_format.should be_nil\n  end\n\n  @[DataProvider(\"mime_type_provider\")]\n  def test_mime_type(format : String, mime_types : Indexable(String)) : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    mime_types.each do |mt|\n      request.format(mt).should eq format\n    end\n\n    request.class.register_format format, mime_types\n\n    mime_types.each do |mt|\n      request.format(mt).should eq format\n\n      if !format.nil?\n        mime_types[0].should eq request.mime_type format\n      end\n    end\n  end\n\n  def test_request_format : Nil\n    request = AHTTP::Request.new \"GET\", \"/\"\n    request.request_format.should eq \"json\"\n\n    request.request_format(\"html\").should eq \"html\"\n    request.request_format(\"json\").should eq \"json\"\n\n    request = AHTTP::Request.new \"GET\", \"/\"\n    request.request_format(nil).should be_nil\n\n    request = AHTTP::Request.new \"GET\", \"/\"\n    request.request_format = \"foo\"\n    request.request_format.should eq \"foo\"\n  end\n\n  def mime_type_provider : Tuple\n    {\n      {\"txt\", {\"text/plain\"}},\n      {\"js\", {\"application/javascript\", \"application/x-javascript\", \"text/javascript\"}},\n      {\"css\", {\"text/css\"}},\n      {\"json\", {\"application/json\", \"application/x-json\"}},\n      {\"jsonld\", {\"application/ld+json\"}},\n      {\"xml\", {\"text/xml\", \"application/xml\", \"application/x-xml\"}},\n      {\"rdf\", {\"application/rdf+xml\"}},\n      {\"atom\", {\"application/atom+xml\"}},\n      {\"form\", {\"application/x-www-form-urlencoded\", \"multipart/form-data\"}},\n      {\"hal\", {\"application/hal+json\", \"application/hal+xml\"}},\n      {\"jsonapi\", {\"application/vnd.api+json\"}},\n      {\"pdf\", {\"application/pdf\"}},\n      {\"problem\", {\"application/problem+json\"}},\n      {\"soap\", {\"application/soap+xml\"}},\n      {\"wbxml\", {\"application/vnd.wap.wbxml\"}},\n      {\"yaml\", {\"text/yaml\", \"application/x-yaml\"}},\n    }\n  end\n\n  @[DataProvider(\"structured_suffix_format_provider\")]\n  def test_structured_suffix_format(mime_type : String, expected : String?) : Nil\n    AHTTP::Request.new(\"GET\", \"/\").format(mime_type).should eq expected\n  end\n\n  def structured_suffix_format_provider : Tuple\n    {\n      {\"application/vnd.github+json\", \"json\"},\n      {\"application/vnd.oci.image.manifest.v1+json\", \"json\"},\n      {\"application/foo+xml\", \"xml\"},\n      {\"application/foo+yaml\", \"yaml\"},\n      {\"application/foo+cbor\", \"cbor\"},\n      {\"application/ber-stream+ber\", \"asn1\"},\n      {\"application/foo+json; charset=utf-8\", \"json\"},\n      {\"application/ld+json\", \"jsonld\"},\n      {\"application/vnd.api+json\", \"jsonapi\"},\n      {\"text/vnd.foo+xml\", nil},\n    }\n  end\n\n  @[DataProvider(\"subtype_fallback_provider\")]\n  def test_format_subtype_fallback(mime_type : String, subtype_fallback : Bool, expected : String?) : Nil\n    AHTTP::Request.new(\"GET\", \"/\").format(mime_type, subtype_fallback: subtype_fallback).should eq expected\n  end\n\n  def subtype_fallback_provider : Tuple\n    {\n      {\"application/unknown\", false, nil},\n      {\"application/foo\", false, nil},\n      {\"application/x-foo\", false, nil},\n      {\"application/foo\", true, \"foo\"},\n      {\"application/x-foo\", true, \"foo\"},\n      {\"application/foo+bar\", true, nil},\n      {\"text/unknown\", true, \"unknown\"},\n      {\"garbage\", true, nil},\n      {\"application/json\", true, \"json\"},\n    }\n  end\n\n  def test_trusted_proxy_conflict : Nil\n    AHTTP::Request.set_trusted_proxies [\"3.3.3.3\"], AHTTP::Request::ProxyHeader[:forwarded, :forwarded_proto]\n\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"forwarded\"         => \"proto=http\",\n      \"x-forwarded-proto\" => \"https\",\n    })\n    request.remote_address = Socket::IPAddress.v4 3, 3, 3, 3, port: 1\n\n    expect_raises AHTTP::Exception::ConflictingHeaders, \"The request has both a trusted 'forwarded' header and a trusted 'x-forwarded-proto' header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.\" do\n      request.secure?\n    end\n  end\n\n  def test_trusted_proxies_cache : Nil\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"x-forwarded-for\"   => \"1.1.1.1, 2.2.2.2\",\n      \"x-forwarded-proto\" => \"https\",\n    })\n    request.remote_address = Socket::IPAddress.v4 3, 3, 3, 3, port: 1\n\n    request.secure?.should be_false\n\n    AHTTP::Request.set_trusted_proxies [\"3.3.3.3\", \"2.2.2.2\"], AHTTP::Request::ProxyHeader[:forwarded_for, :forwarded_host, :forwarded_port, :forwarded_proto]\n\n    request.secure?.should be_true\n\n    # Cache must not be hit due to change in header\n    request.headers[\"x-forwarded-proto\"] = \"http\"\n    request.secure?.should be_false\n  end\n\n  def test_trusted_proxies_forwarded_for : Nil\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"x-forwarded-for\"   => \"1.1.1.1, 2.2.2.2\",\n      \"x-forwarded-host\"  => \"foo.example.com:1234, real.example.com:8080\",\n      \"x-forwarded-proto\" => \"https\",\n      \"x-forwarded-port\"  => \"443\",\n    })\n    request.remote_address = Socket::IPAddress.v4 3, 3, 3, 3, port: 1\n\n    # No trusted proxies\n    request.from_trusted_proxy?.should be_false\n    request.host.should eq \"example.com\"\n    request.port.should eq 80\n    request.secure?.should be_false\n\n    # Disabling proxy trusting\n    AHTTP::Request.set_trusted_proxies [] of String, :forwarded_for\n    request.from_trusted_proxy?.should be_false\n    request.host.should eq \"example.com\"\n    request.port.should eq 80\n    request.secure?.should be_false\n\n    # Request is from non trusted proxy\n    AHTTP::Request.set_trusted_proxies [\"2.2.2.2\"], :forwarded_for\n    request.from_trusted_proxy?.should be_false\n    request.host.should eq \"example.com\"\n    request.port.should eq 80\n    request.secure?.should be_false\n\n    # Trusted proxy\n    AHTTP::Request.set_trusted_proxies [\"3.3.3.3\", \"2.2.2.2\"], AHTTP::Request::ProxyHeader[:forwarded_for, :forwarded_host, :forwarded_port, :forwarded_proto]\n    request.from_trusted_proxy?.should be_true\n    request.host.should eq \"foo.example.com\"\n    request.port.should eq 443\n    request.secure?.should be_true\n\n    # Trusted proxy\n    AHTTP::Request.set_trusted_proxies [\"3.3.3.4\", \"2.2.2.2\"], AHTTP::Request::ProxyHeader[:forwarded_for, :forwarded_host, :forwarded_port, :forwarded_proto]\n    request.from_trusted_proxy?.should be_false\n    request.host.should eq \"example.com\"\n    request.port.should eq 80\n    request.secure?.should be_false\n\n    # Alternate proto header values\n    AHTTP::Request.set_trusted_proxies [\"3.3.3.3\"], :forwarded_proto\n    request.headers[\"x-forwarded-proto\"] = \"ssl\"\n    request.secure?.should be_true\n\n    request.headers[\"x-forwarded-proto\"] = \"https, http\"\n    request.secure?.should be_true\n  end\n\n  def test_trusted_proxies_forwarded : Nil\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"      => \"example.com\",\n      \"forwarded\" => \"for=1.1.1.1, host=foo.example.com:8080, proto=https, for=2.2.2.2, host=real.example.com:8080\",\n    })\n    request.remote_address = Socket::IPAddress.v4 3, 3, 3, 3, port: 1\n\n    # No trusted proxies\n    request.from_trusted_proxy?.should be_false\n    request.host.should eq \"example.com\"\n    request.port.should eq 80\n    request.secure?.should be_false\n\n    # Disabling proxy trusting\n    AHTTP::Request.set_trusted_proxies [] of String, :forwarded\n    request.from_trusted_proxy?.should be_false\n    request.host.should eq \"example.com\"\n    request.port.should eq 80\n    request.secure?.should be_false\n\n    # Request is from non trusted proxy\n    AHTTP::Request.set_trusted_proxies [\"2.2.2.2\"], :forwarded\n    request.from_trusted_proxy?.should be_false\n    request.host.should eq \"example.com\"\n    request.port.should eq 80\n    request.secure?.should be_false\n\n    # Trusted proxy\n    AHTTP::Request.set_trusted_proxies [\"3.3.3.3\", \"2.2.2.2\"], :forwarded\n    request.from_trusted_proxy?.should be_true\n    request.host.should eq \"foo.example.com\"\n    request.port.should eq 8080\n    request.secure?.should be_true\n\n    # Trusted proxy\n    AHTTP::Request.set_trusted_proxies [\"3.3.3.4\", \"2.2.2.2\"], :forwarded\n    request.from_trusted_proxy?.should be_false\n    request.host.should eq \"example.com\"\n    request.port.should eq 80\n    request.secure?.should be_false\n\n    # Alternate proto header values\n    AHTTP::Request.set_trusted_proxies [\"3.3.3.3\"], :forwarded\n    request.headers[\"forwarded\"] = \"proto=ssl\"\n    request.secure?.should be_true\n\n    request.headers[\"forwarded\"] = \"proto=https, proto=http\"\n    request.secure?.should be_true\n  end\n\n  @[TestWith(\n    { %(a#{\".a\"*40_000}) },\n    {\":\" * 101}\n  )]\n  def test_very_long_hosts(host : String) : Nil\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\"host\" => host}\n    request.host.should eq host\n  end\n\n  @[TestWith(\n    {\".a\", false, nil, nil},\n    {\"a..\", false, nil, nil},\n    {\"a.\", true, nil, nil},\n    {\"é\", false, nil, nil},\n    {\"[::1]\", true, nil, nil},\n    {\"[::1]:80\", true, \"[::1]\", 80},\n    {\".\" * 101, false, nil, nil},\n  )]\n  def test_host_valididy(host : String, is_valid : Bool, expected_host : String?, expected_port : Int32?) : Nil\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\"host\" => host}\n\n    if is_valid\n      request.host.should eq expected_host || host\n\n      if expected_port\n        request.port.should eq expected_port\n      end\n    else\n      expect_raises AHTTP::Exception::SuspiciousOperation, \"Invalid Host: \" do\n        request.host\n      end\n    end\n  end\n\n  def test_trusted_host_localhost : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], :all\n\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"forwarded\"        => \"host=localhost:8080\",\n      \"x-forwarded-host\" => \"localhost:8080\",\n    })\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n\n    request.from_trusted_proxy?.should be_true\n    request.host.should eq \"localhost\"\n    request.port.should eq 8080\n  end\n\n  def test_trusted_host_ipv6 : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], :all\n\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"forwarded\"        => \"host=\\\"[::1]:443\\\"\",\n      \"x-forwarded-host\" => \"[::1]:443\",\n      \"x-forwarded-port\" => \"443\",\n    })\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n\n    request.from_trusted_proxy?.should be_true\n    request.host.should eq \"[::1]\"\n    request.port.should eq 443\n  end\n\n  def test_safe? : Nil\n    AHTTP::Request.new(\"GET\", \"/\").safe?.should be_true\n    AHTTP::Request.new(\"HEAD\", \"/\").safe?.should be_true\n    AHTTP::Request.new(\"OPTIONS\", \"/\").safe?.should be_true\n    AHTTP::Request.new(\"TRACE\", \"/\").safe?.should be_true\n    AHTTP::Request.new(\"POST\", \"/\").safe?.should be_false\n    AHTTP::Request.new(\"PUT\", \"/\").safe?.should be_false\n  end\n\n  def test_port_no_host_header : Nil\n    AHTTP::Request.new(\"GET\", \"/\").port.should eq 80\n  end\n\n  @[TestWith(\n    domain: {\"test.com:90\", 90},\n    ipv4: {\"127.0.0.1:90\", 90},\n    ipv6: {\"[::1]:90\", 90},\n    no_port: {\"test.com\", 80},\n  )]\n  def test_port(host : String, port : Int32?) : Nil\n    AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\"host\" => host}).port.should eq port\n  end\n\n  def test_port_trusted_port : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], :forwarded_port\n\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"             => \"example.com\",\n      \"forwarded\"        => \"host=localhost:8080\",\n      \"x-forwarded-port\" => \"8080\",\n    })\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n\n    request.port.should eq 8080\n\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"             => \"example.com\",\n      \"forwarded\"        => \"host=localhost\",\n      \"x-forwarded-port\" => \"80\",\n    })\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n\n    request.port.should eq 80\n\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"forwarded\"         => \"host=\\\"[::1]\\\"\",\n      \"x-forwarded-proto\" => \"https\",\n      \"x-forwarded-port\"  => \"443\",\n    })\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n\n    request.port.should eq 443\n  end\n\n  def test_port_trusted_does_not_default_to_0 : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], :forwarded_for\n\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"             => \"localhost\",\n      \"x-forwarded-host\" => \"test.example.com\",\n      \"x-forwarded-port\" => \"\",\n    })\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n\n    request.port.should eq 80\n  end\n\n  def test_port_trusted_proxies_none_set : Nil\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"x-forwarded-proto\" => \"https\",\n      \"x-forwarded-port\"  => \"443\",\n    })\n\n    # Ignored without trusted proxy\n    request.port.should eq 80\n  end\n\n  def test_port_trusted_proxies_proto_port_set : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], AHTTP::Request::ProxyHeader[:forwarded_proto, :forwarded_port]\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"x-forwarded-proto\" => \"https\",\n      \"x-forwarded-port\"  => \"8443\",\n    })\n\n    # Falls back on scheme on untrusted connection\n    request.port.should eq 80\n    request.scheme.should eq \"http\"\n\n    # Uses proxy value if trusted\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n    request.port.should eq 8443\n    request.scheme.should eq \"https\"\n  end\n\n  def test_port_trusted_proxies_proto_set_https : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], AHTTP::Request::ProxyHeader[:forwarded_proto, :forwarded_port]\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"x-forwarded-proto\" => \"https\",\n    })\n\n    # Falls back on scheme on untrusted connection\n    request.port.should eq 80\n\n    # With only proto, falls back on default port for this scheme\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n    request.port.should eq 443\n  end\n\n  def test_port_trusted_proxies_proto_set_http : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], AHTTP::Request::ProxyHeader[:forwarded_proto, :forwarded_port]\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"x-forwarded-proto\" => \"http\",\n    })\n\n    # Falls back on scheme on untrusted connection\n    request.port.should eq 80\n\n    # With only proto, falls back on default port for this scheme\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n    request.port.should eq 80\n  end\n\n  def test_port_trusted_proxies_proto_on : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], AHTTP::Request::ProxyHeader[:forwarded_proto, :forwarded_port]\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"x-forwarded-proto\" => \"On\",\n    })\n\n    # Falls back on scheme on untrusted connection\n    request.port.should eq 80\n\n    # With only proto, falls back on default port for this scheme\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n    request.port.should eq 443\n  end\n\n  def test_port_trusted_proxies_proto_one : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], AHTTP::Request::ProxyHeader[:forwarded_proto, :forwarded_port]\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"x-forwarded-proto\" => \"1\",\n    })\n\n    # Falls back on scheme on untrusted connection\n    request.port.should eq 80\n\n    # With only proto, falls back on default port for this scheme\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n    request.port.should eq 443\n  end\n\n  def test_port_trusted_proxies_proto_invalid : Nil\n    AHTTP::Request.set_trusted_proxies [\"1.1.1.1\"], AHTTP::Request::ProxyHeader[:forwarded_proto, :forwarded_port]\n    request = AHTTP::Request.new(\"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\"              => \"example.com\",\n      \"x-forwarded-proto\" => \"foo\",\n    })\n\n    request.remote_address = Socket::IPAddress.v4 1, 1, 1, 1, port: 1\n    request.port.should eq 80\n  end\n\n  def test_proxy_header_header_default : Nil\n    AHTTP::Request::ProxyHeader::FORWARDED_PROTO.header.should eq \"x-forwarded-proto\"\n  end\n\n  def test_proxy_header_header_override : Nil\n    AHTTP::Request.override_trusted_header :forwarded_proto, \"foo-proto\"\n\n    AHTTP::Request::ProxyHeader::FORWARDED_PROTO.header.should eq \"foo-proto\"\n  end\n\n  def test_truested_host_not_set : Nil\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\" => \"evil.com\",\n    }\n\n    request.host.should eq \"evil.com\"\n  end\n\n  def test_truested_host_untrusted : Nil\n    # Add trusted domain, including subdomains\n    AHTTP::Request.set_trusted_hosts([/^([a-z]{9}\\.)?trusted\\.com$/])\n\n    request = AHTTP::Request.new \"GET\", \"/\", headers: ::HTTP::Headers{\n      \"host\" => \"evil.com\",\n    }\n\n    # Untrusted host\n    expect_raises AHTTP::Exception::SuspiciousOperation, \"Untrusted Host: 'evil.com'\" do\n      request.host\n    end\n  end\n\n  def test_truested_host_trusted : Nil\n    # Add trusted domain, including subdomains\n    AHTTP::Request.set_trusted_hosts([/^([a-z]{9}\\.)?trusted\\.com$/])\n\n    request = AHTTP::Request.new \"GET\", \"/\"\n\n    # Trusted host\n    request.headers[\"host\"] = \"trusted.com\"\n    request.host.should eq \"trusted.com\"\n    request.port.should eq 80\n\n    request.headers[\"host\"] = \"trusted.com:8080\"\n    request.host.should eq \"trusted.com\"\n    request.port.should eq 8080\n\n    request.headers[\"host\"] = \"subdomain.trusted.com:8080\"\n    request.host.should eq \"subdomain.trusted.com\"\n  end\n\n  def test_truested_host_special_characters : Nil\n    AHTTP::Request.set_trusted_hosts([/localhost(\\.local){0,1}#,example.com/, /localhost/])\n\n    request = AHTTP::Request.new \"GET\", \"/\"\n\n    request.headers[\"host\"] = \"localhost\"\n    request.host.should eq \"localhost\"\n  end\n\n  def test_request_data : Nil\n    request = AHTTP::Request.new \"GET\", \"/\", body: \"foo=bar&biz=baz\"\n    params = request.request_data\n    params.should eq p = URI::Params.new({\"foo\" => [\"bar\"], \"biz\" => [\"baz\"]})\n    request.request_data.should eq p\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/response_headers_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe AHTTP::Response::Headers do\n  describe \"#initialize\" do\n    it \"sets the date on creation\" do\n      headers = AHTTP::Response::Headers.new\n      headers.has_key?(\"date\").should be_true\n      headers.date.should_not(be_nil).should be_close Time.utc, 1.second\n    end\n\n    it \"uses the provided date if supplied\" do\n      time = ::HTTP.format_time Time.utc 2021, 4, 7, 12, 0, 0\n      headers = AHTTP::Response::Headers{\"date\" => time}\n      headers[\"date\"].should eq time\n    end\n\n    it \"copies multiple headers with the same key\" do\n      headers = AHTTP::Response::Headers{\"foo\" => [\"one\", \"two\", \"three\"]}\n      headers[\"foo\"].should eq \"one,two,three\"\n    end\n\n    it \"sets the proper `cache-control` header based on the provided ::HTTP::Headers object\" do\n      headers = AHTTP::Response::Headers.new ::HTTP::Headers{\"expires\" => \"Sat, 10 Apr 2021 15:14:59 GMT\"}\n      headers[\"cache-control\"].should eq \"private, must-revalidate\"\n\n      headers = AHTTP::Response::Headers.new ::HTTP::Headers{\"expires\" => \"Sat, 10 Apr 2021 15:14:59 GMT\", \"cache-control\" => \"max-age=3600\"}\n      headers[\"cache-control\"].should eq \"max-age=3600, private\"\n    end\n  end\n\n  describe \"#<<\" do\n    it \"with a cookie\" do\n      headers = AHTTP::Response::Headers.new\n      headers.cookies.should be_empty\n      headers << ::HTTP::Cookie.new \"key\", \"value\"\n      headers.cookies[\"key\"].should eq ::HTTP::Cookie.new \"key\", \"value\"\n    end\n  end\n\n  describe \"#[]=\" do\n    it \"with string value\" do\n      headers = AHTTP::Response::Headers.new\n      headers[\"cache-control\"].should eq \"no-cache, private\"\n      headers.has_cache_control_directive?(\"no-cache\").should be_true\n\n      headers[\"cache-control\"] = \"public\"\n      headers[\"cache-control\"].should eq \"public\"\n      headers.has_cache_control_directive?(\"public\").should be_true\n\n      headers[\"Cache-Control\"] = \"private\"\n      headers[\"cache-control\"].should eq \"private\"\n      headers.has_cache_control_directive?(\"private\").should be_true\n    end\n\n    it \"string value with set-cookie key\" do\n      headers = AHTTP::Response::Headers.new\n      headers[\"set-cookie\"] = \"name=value; Secure\"\n      headers.cookies[\"name\"].value.should eq \"value\"\n      headers.cookies[\"name\"].secure.should be_true\n    end\n\n    it \"with an array of set-cookie values\" do\n      headers = AHTTP::Response::Headers.new\n      headers[\"set-cookie\"] = [\"name=value; Secure\", \"foo=bar\"]\n      headers.cookies[\"name\"].value.should eq \"value\"\n      headers.cookies[\"name\"].secure.should be_true\n      headers.cookies[\"foo\"].value.should eq \"bar\"\n      headers.cookies[\"foo\"].secure.should be_false\n    end\n\n    it \"with non string value\" do\n      headers = AHTTP::Response::Headers.new\n      headers[\"key\"] = 10\n      headers[\"key\"].should eq \"10\"\n    end\n\n    it \"with cookie value\" do\n      headers = AHTTP::Response::Headers.new\n      headers[\"name\"] = ::HTTP::Cookie.new \"name\", \"value\"\n      headers.cookies[\"name\"].value.should eq \"value\"\n    end\n  end\n\n  describe \"#date\" do\n    it \"with a missing key\" do\n      headers = AHTTP::Response::Headers.new\n      headers.date(\"foo\").should be_nil\n    end\n\n    it \"with a missing key and custom default\" do\n      headers = AHTTP::Response::Headers.new\n      time = Time.utc 2021, 4, 7, 12, 0, 0\n      headers.date(\"foo\", time).should eq time\n    end\n\n    it \"with an invalid datetime string\" do\n      AHTTP::Response::Headers{\"date\" => \"foo\"}.date.should be_nil\n    end\n  end\n\n  describe \"#delete\" do\n    it \"deletes the provided header\" do\n      headers = AHTTP::Response::Headers{\"foo\" => \"bar\"}\n      headers.has_key?(\"foo\").should be_true\n      headers.delete \"foo\"\n      headers.has_key?(\"foo\").should be_false\n    end\n\n    it \"removes cache-control header\" do\n      headers = AHTTP::Response::Headers{\"expires\" => \"Sat, 10 Apr 2021 15:14:59 GMT\"}\n      headers.has_cache_control_directive?(\"must-revalidate\").should be_true\n      headers.delete \"cache-control\"\n      headers.has_cache_control_directive?(\"must-revalidate\").should be_false\n    end\n\n    it \"reinitializes the date if deleted\" do\n      time = ::HTTP.format_time Time.utc 2021, 4, 7, 12, 0, 0\n      headers = AHTTP::Response::Headers{\"date\" => time}\n      headers.delete \"date\"\n\n      headers.has_key?(\"date\").should be_true\n      headers[\"date\"].should_not eq time\n    end\n\n    it \"removes cookies when deleting set-cookie\" do\n      headers = AHTTP::Response::Headers.new\n      headers.cookies << ::HTTP::Cookie.new \"name\", \"value\"\n      headers.cookies[\"name\"].value.should eq \"value\"\n      headers.delete \"set-cookie\"\n      headers.cookies.should be_empty\n    end\n  end\n\n  it \"#get_cache_control_directive\" do\n    headers = AHTTP::Response::Headers.new\n    headers.add_cache_control_directive \"private\"\n    headers.get_cache_control_directive(\"private\").should be_true\n    headers.get_cache_control_directive(\"public\").should be_nil\n  end\n\n  describe \"cache-control\" do\n    it \"uses defaults to conservative values\" do\n      headers = AHTTP::Response::Headers.new\n      headers[\"cache-control\"].should eq \"no-cache, private\"\n      headers.has_cache_control_directive?(\"no-cache\").should be_true\n    end\n\n    it \"uses what's provided if provided\" do\n      headers = AHTTP::Response::Headers{\"cache-control\" => \"public\"}\n      headers[\"cache-control\"].should eq \"public\"\n      headers.has_cache_control_directive?(\"public\").should be_true\n    end\n\n    it \"does not add anything if an etag is included\" do\n      headers = AHTTP::Response::Headers{\"etag\" => \"abc123\"}\n      headers[\"cache-control\"].should eq \"no-cache, private\"\n      headers.has_cache_control_directive?(\"private\").should be_true\n      headers.has_cache_control_directive?(\"no-cache\").should be_true\n      headers.has_cache_control_directive?(\"max-age\").should be_false\n    end\n\n    it \"includes special directive with last-modified header\" do\n      AHTTP::Response::Headers{\"expires\" => \"Sat, 10 Apr 2021 15:14:59 GMT\"}[\"cache-control\"].should eq \"private, must-revalidate\"\n      AHTTP::Response::Headers{\"last-modified\" => \"Sat, 10 Apr 2021 15:14:59 GMT\"}[\"cache-control\"].should eq \"private, must-revalidate\"\n      AHTTP::Response::Headers{\"last-modified\" => \"Sat, 10 Apr 2021 15:14:59 GMT\", \"etag\" => \"abc123\"}[\"cache-control\"].should eq \"private, must-revalidate\"\n      AHTTP::Response::Headers{\"last-modified\" => \"Sat, 10 Apr 2021 15:14:59 GMT\", \"expires\" => \"Sat, 10 Apr 2021 15:14:59 GMT\"}[\"cache-control\"].should eq \"private, must-revalidate\"\n    end\n\n    it \"adds 'private' to existing cache-control header that doesn't have private or public\" do\n      AHTTP::Response::Headers{\"expires\" => \"Sat, 10 Apr 2021 15:14:59 GMT\", \"cache-control\" => \"max-age=3600\"}[\"cache-control\"].should eq \"max-age=3600, private\"\n      AHTTP::Response::Headers{\"cache-control\" => \"max-age=3600\", \"expires\" => \"Sat, 10 Apr 2021 15:14:59 GMT\"}[\"cache-control\"].should eq \"max-age=3600, private\"\n    end\n\n    it \"does not add private or public with s-maxage\" do\n      AHTTP::Response::Headers{\"cache-control\" => \"s-maxage=100\"}[\"cache-control\"].should eq \"s-maxage=100\"\n    end\n\n    it \"does not alter with multiple directives\" do\n      AHTTP::Response::Headers{\"cache-control\" => \"private, max-age=100\"}[\"cache-control\"].should eq \"private, max-age=100\"\n      AHTTP::Response::Headers{\"cache-control\" => \"public, max-age=100\"}[\"cache-control\"].should eq \"public, max-age=100\"\n    end\n\n    it \"recacluates cache-control when new header is added after creation\" do\n      headers = AHTTP::Response::Headers.new\n      headers[\"last-modified\"] = \"Sat, 10 Apr 2021 15:14:59 GMT\"\n      headers[\"cache-control\"].should eq \"private, must-revalidate\"\n    end\n\n    it \"recacluates cache-control when multiple directives are added\" do\n      headers = AHTTP::Response::Headers.new\n      headers[\"cache-control\"] = \"public\"\n      headers.add \"cache-control\", \"immutable\"\n      headers[\"cache-control\"].should eq \"public, immutable\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/response_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate struct TestWriter < AHTTP::Response::Writer\n  def write(output : IO, & : IO -> Nil) : Nil\n    yield output\n    output.print \"EOF\"\n  end\nend\n\ndescribe AHTTP::Response do\n  describe \".new\" do\n    it \"defaults\" do\n      response = AHTTP::Response.new\n      response.headers.has_key?(\"date\").should be_true\n      response.headers.has_key?(\"cache-control\").should be_true\n      response.content.should be_empty\n      response.status.should eq ::HTTP::Status::OK\n    end\n\n    it \"accepts an Int status\" do\n      AHTTP::Response.new(status: 201).status.should eq ::HTTP::Status::CREATED\n    end\n\n    it \"accepts an ::HTTP::Status status\" do\n      AHTTP::Response.new(status: ::HTTP::Status::CREATED).status.should eq ::HTTP::Status::CREATED\n    end\n\n    it \"accepts string content\" do\n      AHTTP::Response.new(\"FOO\").content.should eq \"FOO\"\n    end\n\n    it \"accepts nil content\" do\n      AHTTP::Response.new(nil).content.should eq \"\"\n    end\n  end\n\n  describe \"#content=\" do\n    it \"accepts a string\" do\n      response = AHTTP::Response.new \"FOO\"\n      response.content.should eq \"FOO\"\n      response.content = \"BAR\"\n      response.content.should eq \"BAR\"\n    end\n  end\n\n  describe \"#send\" do\n    it \"writes the data to the provided IO\" do\n      io = IO::Memory.new\n      response = ::HTTP::Server::Response.new(io)\n      request = AHTTP::Request.new \"GET\", \"/\"\n\n      art_response = AHTTP::Response.new(\"DATA\", 418, ::HTTP::Headers{\"FOO\" => \"BAR\"})\n      art_response.headers << ::HTTP::Cookie.new \"key\", \"value\"\n\n      art_response.send request, response\n\n      response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n      response.headers[\"foo\"].should eq \"BAR\"\n      response.headers[\"content-length\"].should eq \"4\"\n      response.headers.has_key?(\"date\").should be_true\n      response.cookies[\"key\"].should eq ::HTTP::Cookie.new \"key\", \"value\"\n      response.closed?.should be_true\n\n      io.rewind.gets_to_end.should end_with \"DATA\"\n    end\n  end\n\n  describe \"#status=\" do\n    it \"accepts an Int\" do\n      response = AHTTP::Response.new\n      response.status = 201\n      response.status.should eq ::HTTP::Status::CREATED\n    end\n\n    it \"accepts an ::HTTP::Status\" do\n      response = AHTTP::Response.new\n      response.status = ::HTTP::Status::CREATED\n      response.status.should eq ::HTTP::Status::CREATED\n    end\n  end\n\n  describe \"#write\" do\n    it \"writes the content to the given output IO\" do\n      io = IO::Memory.new\n      AHTTP::Response.new(\"FOO BAR\").write io\n\n      io.to_s.should eq \"FOO BAR\"\n    end\n\n    it \"supports customization via an AHTTP::Response::Writer\" do\n      io = IO::Memory.new\n      response = AHTTP::Response.new(\"FOO BAR\")\n\n      response.writer = TestWriter.new\n      response.write io\n\n      io.to_s.should eq \"FOO BAREOF\"\n    end\n  end\n\n  describe \"#prepare\" do\n    it \"sets content-type based on format\" do\n      request = AHTTP::Request.new \"GET\", \"/\"\n      request.request_format = \"json\"\n      response = AHTTP::Response.new \"CONTENT\"\n\n      response.prepare request\n\n      response.headers[\"content-type\"].should eq \"application/json\"\n    end\n\n    it \"does not override content-type if already set\" do\n      request = AHTTP::Request.new \"GET\", \"/\"\n      request.request_format = \"json\"\n      response = AHTTP::Response.new \"CONTENT\", headers: ::HTTP::Headers{\"content-type\" => \"application/json; charset=utf-8\"}\n\n      response.prepare request\n\n      response.headers[\"content-type\"].should eq \"application/json; charset=utf-8\"\n    end\n\n    it \"adds the charset to text based formats\" do\n      request = AHTTP::Request.new \"GET\", \"/\"\n      request.request_format = \"csv\"\n      response = AHTTP::Response.new \"CONTENT\"\n\n      response.prepare request\n\n      response.headers[\"content-type\"].should eq \"text/csv; charset=utf-8\"\n    end\n\n    it \"allows customizing the charset\" do\n      request = AHTTP::Request.new \"GET\", \"/\"\n      request.request_format = \"csv\"\n      response = AHTTP::Response.new \"CONTENT\"\n      response.charset = \"ISO-8859-1\"\n\n      response.prepare request\n\n      response.headers[\"content-type\"].should eq \"text/csv; charset=ISO-8859-1\"\n    end\n\n    it \"does not override the charset if already included\" do\n      request = AHTTP::Request.new \"GET\", \"/\"\n      request.request_format = \"csv\"\n      response = AHTTP::Response.new \"CONTENT\", headers: ::HTTP::Headers{\"content-type\" => \"text/csv; charset=ISO-8859-1\"}\n\n      response.prepare request\n\n      response.headers[\"content-type\"].should eq \"text/csv; charset=ISO-8859-1\"\n    end\n\n    it \"removes content for informational responses & empty responses\" do\n      request = AHTTP::Request.new \"GET\", \"/\"\n      response = AHTTP::Response.new \"CONTENT\"\n\n      response.headers[\"content-length\"] = \"5\"\n      response.headers[\"content-type\"] = \"text/plain\"\n      response.status = 101\n\n      response.prepare request\n\n      response.content.should be_empty\n      response.headers.has_key?(\"content-length\").should be_false\n      response.headers.has_key?(\"content-type\").should be_false\n    end\n\n    it \"removes content for empty responses\" do\n      request = AHTTP::Request.new \"GET\", \"/\"\n      response = AHTTP::Response.new \"CONTENT\"\n\n      response.content = \"CONTENT\"\n      response.headers[\"content-length\"] = \"5\"\n      response.headers[\"content-type\"] = \"text/plain\"\n      response.status = 204\n\n      response.prepare request\n\n      response.content.should be_empty\n      response.headers.has_key?(\"content-length\").should be_false\n      response.headers.has_key?(\"content-type\").should be_false\n    end\n\n    it \"removes content-length if transfer-encoding is set\" do\n      request = AHTTP::Request.new \"GET\", \"/\"\n\n      response = AHTTP::Response.new \"CONTENT\"\n      response.headers[\"content-length\"] = \"100\"\n\n      response.prepare request\n\n      response.headers[\"content-length\"].should eq \"100\"\n\n      response.headers[\"transfer-encoding\"] = \"chunked\"\n\n      response.prepare request\n\n      response.headers.has_key?(\"content-length\").should be_false\n    end\n\n    it \"handles multi-byte characters\" do\n      request = AHTTP::Request.new \"GET\", \"/\"\n      response = AHTTP::Response.new str = \"Añasco\"\n\n      # Emulate sending the data over the wire\n      mem = IO::Memory.new\n      mem.print str\n      mem.rewind\n\n      response.prepare request\n\n      response.headers[\"content-length\"].should eq mem.size.to_s\n    end\n\n    it \"removes content and preserves content-length for head requests\" do\n      response = AHTTP::Response.new \"CONTENT\"\n      request = AHTTP::Request.new \"HEAD\", \"/\"\n      response.headers[\"content-length\"] = \"5\"\n\n      response.prepare request\n\n      response.content.should be_empty\n      response.headers[\"content-length\"].should eq \"5\"\n    end\n\n    it \"sets pragma & expires headers on HTTP/1.0 request\" do\n      request = AHTTP::Request.new \"HEAD\", \"/\", version: \"HTTP/1.0\"\n\n      response = AHTTP::Response.new \"CONTENT\"\n      response.headers.add_cache_control_directive \"no-cache\"\n\n      response.prepare request\n\n      response.content.should be_empty\n      response.headers[\"pragma\"]?.should eq \"no-cache\"\n      response.headers[\"expires\"]?.should eq \"-1\"\n\n      request.version = \"HTTP/1.1\"\n      response = AHTTP::Response.new \"CONTENT\"\n\n      response.prepare request\n\n      response.headers.has_key?(\"pragma\").should be_false\n      response.headers.has_key?(\"expires\").should be_false\n    end\n  end\n\n  it \"#set_public\" do\n    response = AHTTP::Response.new\n    response.set_public\n\n    response.headers[\"cache-control\"].should contain \"public\"\n    response.headers[\"cache-control\"].should_not contain \"private\"\n  end\n\n  describe \"#set_etag\" do\n    it \"sets the etag\" do\n      response = AHTTP::Response.new\n      response.set_etag \"ETAG\"\n      response.etag.should eq %(\"ETAG\")\n    end\n\n    it \"removes the etag if value is `nil`\" do\n      response = AHTTP::Response.new headers: ::HTTP::Headers{\"etag\" => \"ETAG\"}\n      response.set_etag nil\n      response.etag.should be_nil\n    end\n\n    it \"allows setting a weak etag\" do\n      response = AHTTP::Response.new\n      response.set_etag \"ETAG\", true\n      response.etag.should eq %(W/\"ETAG\")\n    end\n  end\n\n  describe \"#last_modified=\" do\n    it \"sets the last-modified header\" do\n      now = Time.utc\n\n      response = AHTTP::Response.new\n      response.last_modified = now\n      response.last_modified.should eq now.at_beginning_of_second\n    end\n\n    it \"removes the header if the value is `nil`\" do\n      response = AHTTP::Response.new headers: ::HTTP::Headers{\"last-modified\" => \"TIME\"}\n      response.last_modified = nil\n      response.last_modified.should be_nil\n    end\n  end\n\n  describe \"#redirect?\" do\n    it \"valid redirection response\" do\n      [301, 302, 303, 307].each do |status|\n        response = AHTTP::Response.new status: status\n        response.redirect?.should be_true\n      end\n    end\n\n    it \"invalid redirection status\" do\n      [304, 200, 404].each do |status|\n        AHTTP::Response.new(status: status).redirect?.should be_false\n      end\n    end\n\n    it \"with specific redirect location\" do\n      response = AHTTP::Response.new status: 301, headers: ::HTTP::Headers{\"location\" => \"/good-uri\"}\n      response.redirect?.should be_true\n      response.redirect?(\"/bad-uri\").should be_false\n      response.redirect?(\"/good-uri\").should be_true\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"athena-spec\"\nrequire \"../src/athena-http\"\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/http/spec/streamed_response_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate struct TestWriter < AHTTP::Response::Writer\n  def write(output : IO, & : IO -> Nil) : Nil\n    yield output\n    output.print \"EOF\"\n  end\nend\n\ndescribe AHTTP::StreamedResponse do\n  describe \".new\" do\n    it \"accepts a block\" do\n      io = IO::Memory.new\n\n      response = (AHTTP::StreamedResponse.new &.<<(\"BAZ\"))\n\n      response.write io\n\n      io.to_s.should eq \"BAZ\"\n    end\n\n    it \"accepts a proc\" do\n      io = IO::Memory.new\n      proc = ->(i : IO) { i << \"FOO\" }\n\n      response = AHTTP::StreamedResponse.new proc\n\n      response.write io\n\n      io.to_s.should eq \"FOO\"\n    end\n\n    it \"allows overriding the callback\" do\n      io = IO::Memory.new\n\n      response = (AHTTP::StreamedResponse.new &.<<(\"BAZ\"))\n      response.content = ->(i : IO) { i << \"BAR\" }\n\n      response.write io\n\n      io.to_s.should eq \"BAR\"\n    end\n\n    it \"accepts an Int status\" do\n      (AHTTP::StreamedResponse.new(status: 201, &.<<(\"BAZ\"))).status.should eq ::HTTP::Status::CREATED\n    end\n\n    it \"accepts an ::HTTP::Status status\" do\n      (AHTTP::StreamedResponse.new(status: :created, &.<<(\"BAZ\"))).status.should eq ::HTTP::Status::CREATED\n    end\n  end\n\n  describe \"#content=\" do\n    it \"raises on not nil content\" do\n      response = (AHTTP::StreamedResponse.new &.<<(\"BAZ\"))\n\n      expect_raises AHTTP::Exception::Logic, \"The content cannot be set on a StreamedResponse instance.\" do\n        response.content = \"FOO\"\n      end\n    end\n\n    it \"allows nil\" do\n      io = IO::Memory.new\n\n      response = (AHTTP::StreamedResponse.new &.<<(\"BAZ\"))\n\n      response.content = nil\n\n      response.write io\n\n      io.to_s.should be_empty\n    end\n  end\n\n  describe \"#write\" do\n    it \"supports customization via an AHTTP::Response::Writer\" do\n      io = IO::Memory.new\n      response = (AHTTP::StreamedResponse.new &.<<(\"FOO BAR\"))\n\n      response.writer = TestWriter.new\n      response.write io\n\n      io.to_s.should eq \"FOO BAREOF\"\n    end\n\n    it \"does not allow writing more than once\" do\n      io = IO::Memory.new\n      response = (AHTTP::StreamedResponse.new &.<<(\"FOO BAR\"))\n\n      response.write io\n      response.write io\n\n      io.to_s.should eq \"FOO BAR\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http/spec/uploaded_file_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct UploadedFileTest < ASPEC::TestCase\n  def test_initialize_non_existent_file : Nil\n    ex = expect_raises ::AHTTP::Exception::FileNotFound, \"The file does not exist.\" do\n      AHTTP::UploadedFile.new \"#{__DIR__}/assets/missing\", \"original.gif\"\n    end\n\n    ex.file.should eq \"#{__DIR__}/assets/missing\"\n  end\n\n  def test_no_mime_type : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\"\n\n    file.client_mime_type.should eq \"application/octet-stream\"\n    file.mime_type.should eq \"image/gif\"\n    file.status.ok?.should be_true\n  end\n\n  def test_unknown_mime_type : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/.unknownextension\", \"original.gif\"\n\n    file.client_mime_type.should eq \"application/octet-stream\"\n  end\n\n  def test_guess_client_extension : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\"\n\n    file.guess_client_extension.should eq \"gif\"\n  end\n\n  def test_guess_client_extension_with_incorrect_mime_type : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/png\"\n\n    file.guess_client_extension.should eq \"png\"\n  end\n\n  def test_case_sensitive_mime_type : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/case-sensitive-mime-type.xlsm\", \"text.xlsm\", \"application/vnd.ms-excel.sheet.macroEnabled.12\"\n\n    file.guess_client_extension.should eq \"xlsm\"\n  end\n\n  def test_client_original_name : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\"\n\n    file.client_original_name.should eq \"original.gif\"\n  end\n\n  def test_client_original_extension : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\"\n\n    file.client_original_extension.should eq \"gif\"\n  end\n\n  def test_move_local_file_is_not_allowed : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\"\n\n    expect_raises ::AHTTP::Exception::File, \"The file 'original.gif' was not uploaded due to an unknown error.\" do\n      file.move \"#{__DIR__}/assets/directory\"\n    end\n  end\n\n  def test_move_local_file_is_allowed_in_test_mode : Nil\n    path = \"#{Dir.tempdir}/test.copy.gif\"\n    target_dir = \"#{Dir.tempdir}/test\"\n    target_path = \"#{target_dir}/test.copy.gif\"\n    ::File.delete? path\n    ::File.delete? target_path\n    ::File.copy \"#{__DIR__}/assets/test.gif\", path\n    Dir.mkdir target_dir unless Dir.exists? target_dir\n\n    file = AHTTP::UploadedFile.new path, \"original.gif\", \"image/gif\", test: true\n    moved_file = file.move target_dir\n    moved_file.should be_a AHTTP::File\n\n    ::File.exists?(target_path).should be_true\n    ::File.exists?(path).should be_false\n    ::File.realpath(target_path).should eq moved_file.realpath\n\n    FileUtils.rm_rf target_dir\n  end\n\n  def test_move_failed_too_big : Nil\n    AHTTP::UploadedFile.max_file_size = 1024 * 5\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\", :size_limit_exceeded\n\n    expect_raises ::AHTTP::Exception::FileSizeLimitExceeded, \"The file 'original.gif' exceeds your max_file_size configuration value (limit is 5.0kiB).\" do\n      file.move \"#{__DIR__}/assets/directory\"\n    end\n  ensure\n    AHTTP::UploadedFile.max_file_size = 0\n  end\n\n  def test_client_original_name_sanitize_filename : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"../../original.gif\", \"image/gif\"\n\n    file.client_original_name.should eq \"original.gif\"\n  end\n\n  def test_size : Nil\n    file = AHTTP::UploadedFile.new path = \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\"\n    file.size.should eq File.size path\n\n    file = AHTTP::UploadedFile.new path = \"#{__DIR__}/assets/test\", \"original.gif\", \"image/gif\"\n    file.size.should eq File.size path\n  end\n\n  def test_extname : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\"\n\n    file.extname.should eq \"gif\"\n  end\n\n  def test_client_original_path : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\"\n\n    file.client_original_path.should eq \"original.gif\"\n  end\n\n  def test_client_original_path_webkit_directory : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/webkitdirectory/test.txt\", \"webkitdirectory/test.txt\", \"text/plain\"\n\n    file.client_original_path.should eq \"webkitdirectory/test.txt\"\n  end\n\n  def test_valid : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\", test: true\n    file.valid?.should be_true\n  end\n\n  def test_invalid : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\", :size_limit_exceeded\n    file.valid?.should be_false\n  end\n\n  def test_invalid_non_http_upload : Nil\n    file = AHTTP::UploadedFile.new \"#{__DIR__}/assets/test.gif\", \"original.gif\", \"image/gif\"\n    file.valid?.should be_false\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/abstract_file.cr",
    "content": "require \"file_utils\"\n\nrequire \"athena-mime\"\n\n# Represents a file on the filesystem without opening a file descriptor.\n# This base type is needed as you can't inherit from non-abstract structs,\n# and it makes sense to have a generic `Athena::HTTP::File` type while also being able to share the logic with other sub-types.\n#\n# TODO: Add more methods as needed.\nabstract struct Athena::HTTP::AbstractFile\n  # Returns the path to this file, which may be relative.\n  getter path : String\n\n  private getter info : ::File::Info { ::File.info @path }\n\n  # Create a new instance for the file at the provided *path*.\n  # If *check_path* is `true`, then an `AHTTP::Exception::FileNotFound` exception is raised if the file at the provided *path* does not exist.\n  def initialize(path : String | Path, check_path : Bool = true)\n    if check_path && !::File.file?(path)\n      raise Athena::HTTP::Exception::FileNotFound.new \"The file does not exist.\", file: path\n    end\n\n    @path = path.to_s\n  end\n\n  # Returns the extension based on the MIME type of this file, or `nil` if it is unknown.\n  # Uses the MIME type as guessed by `#mime_type` to guess the file extension.\n  #\n  # ```\n  # file = AHTTP::File.new \"/path/to/foo.txt\"\n  # file.guess_extension # => \"txt\"\n  # ```\n  def guess_extension : String?\n    return unless mime_type = self.mime_type\n\n    AMIME::Types.default.extensions(mime_type).first?\n  end\n\n  # Returns the MIME type of this file, using `AMIME::Types` under the hood.\n  #\n  # ```\n  # file = AHTTP::File.new \"/path/to/foo.txt\"\n  # file.mime_type # => \"text/plain\"\n  # ```\n  def mime_type : String?\n    AMIME::Types.default.guess_mime_type @path\n  end\n\n  # Moves this file to the provided *directory*, optionally with the provided *name*.\n  # If no *name* is provided, its current `#basename` will be used.\n  def move(directory : Path | String, name : String? = nil) : self\n    target = self.target_file directory, name\n\n    FileUtils.mv @path, target.path\n\n    target\n  end\n\n  # Returns the contents of this file as a string.\n  #\n  # ```\n  # file = AHTTP::File.new \"/path/to/foo.txt\"\n  # file.content # => \"foo\" (content of foo.txt)\n  # ```\n  def content : String\n    ::File.read @path\n  end\n\n  # Returns the last component of this file's path.\n  # If *suffix* is present at the end of the path, it is removed.\n  #\n  # ```\n  # file = AHTTP::File.new \"/path/to/file.txt\"\n  # file.basename        # => \"file.txt\"\n  # file.basename \".txt\" # => \"file\"\n  # ```\n  def basename(suffix : String? = nil) : String\n    suffix ? ::File.basename(@path, suffix) : ::File.basename(@path)\n  end\n\n  # Resolves the real path of this file by following symbolic links.\n  #\n  # ```\n  # file = AHTTP::File.new \"./../../etc/passwd\"\n  # file.realpath # => \"/etc/passwd\"\n  # ```\n  def realpath : String\n    ::File.realpath @path\n  end\n\n  # Returns the size in bytes of this file.\n  def size : Int\n    ::File.size @path\n  end\n\n  # Returns `true` if this file is readable by user of this process, otherwise returns `false`.\n  def readable? : Bool\n    ::File::Info.readable? @path\n  end\n\n  # Returns the time this file was last modified.\n  def modification_time : Time\n    self.info.modification_time\n  end\n\n  # Returns the extension of this file, or an empty string if it does not have one.\n  #\n  # ```\n  # file = AHTTP::File.new \"/path/to/file.txt\"\n  # file.extname # => \"txt\"\n  # ```\n  def extname : String\n    ::File.extname(@path).lchop '.'\n  end\n\n  private def target_file(directory : String | Path, name : String? = nil) : Athena::HTTP::File\n    if !::File.directory? directory\n      Dir.mkdir_p directory\n    elsif !::File::Info.writable?(directory)\n      raise ArgumentError.new \"Unable to write in the '#{directory}' directory.\"\n    end\n\n    Athena::HTTP::File.new Path[directory, (file_name = name.presence) ? self.clean_name(file_name) : self.basename], false\n  end\n\n  private def clean_name(name : String) : String\n    original_name = name.gsub \"\\\\\", \"/\"\n\n    Path.new(original_name).basename\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/athena-http.cr",
    "content": "require \"./ext/conversion_types\"\n\nrequire \"./abstract_file\"\nrequire \"./binary_file_response\"\nrequire \"./file\"\nrequire \"./header_utils\"\nrequire \"./ip_utils\"\nrequire \"./parameter_bag\"\nrequire \"./redirect_response\"\nrequire \"./response\"\nrequire \"./response_headers\"\nrequire \"./request\"\nrequire \"./request_matcher\"\nrequire \"./request_store\"\nrequire \"./streamed_response\"\nrequire \"./uploaded_file\"\n\nrequire \"./exception/*\"\nrequire \"./request_matcher/*\"\n\n# Convenience alias to make referencing `Athena::HTTP` types easier.\nalias AHTTP = Athena::HTTP\n\nmodule Athena::HTTP\n  VERSION = \"0.1.0\"\n\n  # Both acts as a namespace for exceptions related to the `Athena::HTTP` component, as well as a way to check for exceptions from the component.\n  module Exception; end\nend\n"
  },
  {
    "path": "src/components/http/src/binary_file_response.cr",
    "content": "require \"./response\"\nrequire \"digest/sha256\"\nrequire \"athena-mime\"\n\n# Represents a static file that should be returned the client; includes various options to enhance the response headers. See `.new` for details.\n#\n# This response supports [Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests) requests\n# and [Conditional](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) requests via the\n# [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match),\n# [If-Modified-Since](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since),\n# and [If-Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range) headers.\n#\n# See `AHTTP::HeaderUtils.make_disposition` for an example of handling dynamic files.\nclass Athena::HTTP::BinaryFileResponse < Athena::HTTP::Response\n  # Returns a `AHTTP::AbstractFile` instance representing the file that will be sent to the client.\n  getter file : AHTTP::AbstractFile\n\n  # Determines if the file should be deleted after being sent to the client.\n  setter delete_file_after_send : Bool = false\n\n  @offset : Int64 = 0\n  @max_length : Int64? = nil\n\n  # Represents the possible [content-disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header values.\n  enum ContentDisposition\n    # Indicates that the file should be downloaded.\n    Attachment\n\n    # Indicates that the browser should display the file inside the Web page, or as the Web page.\n    Inline\n\n    # :inherit:\n    def to_s : String\n      case self\n      in .attachment? then \"attachment\"\n      in .inline?     then \"inline\"\n      end\n    end\n  end\n\n  # Instantiates `self` wrapping the provided *file*, optionally with the provided *status*, and *headers*.\n  #\n  # By default the response is `AHTTP::Response#set_public` and includes a `last-modified` header,\n  # but these can be controlled via the *public* and *auto_last_modified* arguments respectively.\n  #\n  # The *content_disposition* argument can be used to set the `content-disposition` header on `self` if it should be downloadable.\n  #\n  # The *auto_etag* argument can be used to automatically set `ETag` header based on a `SHA256` hash of the file.\n  def initialize(\n    file : String | Path | AHTTP::AbstractFile | ::File,\n    status : ::HTTP::Status | Int32 = ::HTTP::Status::OK,\n    headers : ::HTTP::Headers | AHTTP::Response::Headers = AHTTP::Response::Headers.new,\n    public : Bool = true,\n    content_disposition : AHTTP::BinaryFileResponse::ContentDisposition? = nil,\n    auto_etag : Bool = false,\n    auto_last_modified : Bool = true,\n  )\n    super nil, status, headers\n\n    # This has to be here too to make compiler happy about it being defined.\n    @file = case file\n            in String, Path        then AHTTP::File.new file.to_s\n            in ::File              then AHTTP::File.new file.path\n            in AHTTP::AbstractFile then file\n            end\n\n    self.set_file @file, content_disposition, auto_etag, auto_last_modified\n    self.set_public if public\n  end\n\n  # Sets the *file* that will be streamed to the client.\n  # Includes the same optional parameters as `.new`.\n  def set_file(\n    file : String | Path | AHTTP::AbstractFile | ::File,\n    content_disposition : AHTTP::BinaryFileResponse::ContentDisposition? = nil,\n    auto_etag : Bool = false,\n    auto_last_modified : Bool = false,\n  ) : self\n    file = case file\n           in String, Path        then AHTTP::File.new file.to_s\n           in ::File              then AHTTP::File.new file.path\n           in AHTTP::AbstractFile then file\n           end\n\n    unless file.readable?\n      raise Athena::HTTP::Exception::File.new \"The file must be readable.\", file: file.path\n    end\n\n    @file = file\n\n    self.set_auto_etag if auto_etag\n    self.auto_last_modified if auto_last_modified\n    self.set_content_disposition content_disposition if content_disposition\n\n    self\n  end\n\n  # CAUTION: Cannot set the response content via this method on `self`.\n  def content=(data) : self\n    raise AHTTP::Exception::Logic.new \"The content cannot be set on a BinaryFileResponse instance.\" unless data.nil?\n\n    self\n  end\n\n  # CAUTION: Cannot get the response content via this method on `self`.\n  def content : String\n    \"\"\n  end\n\n  # Sets the `content-disposition` header on `self` to the provided *disposition*.\n  # *filename* defaults to the basename of `#file_path`.\n  #\n  # See `AHTTP::HeaderUtils.make_disposition`.\n  def set_content_disposition(disposition : AHTTP::BinaryFileResponse::ContentDisposition, filename : String? = nil, fallback_filename : String? = nil) : self\n    if filename.nil?\n      filename = @file.basename\n    end\n\n    @headers[\"content-disposition\"] = AHTTP::HeaderUtils.make_disposition disposition, filename, fallback_filename\n\n    self\n  end\n\n  # Sets the `etag` header on `self` based on a `SHA256` hash of the file.\n  def set_auto_etag : self\n    self.set_etag Digest::SHA256.base64digest &.file(@file.path)\n\n    self\n  end\n\n  # Sets the `last-modified` header on `self` based on the modification time of the file.\n  def auto_last_modified : self\n    self.last_modified = @file.modification_time\n\n    self\n  end\n\n  # TODO: Support multiple ranges.\n  # TODO: Support `X-Sendfile`.\n  #\n  # OPTIMIZE: Make this less complex.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  protected def prepare(request : AHTTP::Request) : Nil\n    if self.cache_request?(request)\n      self.status = :not_modified\n      return super\n    end\n\n    unless @headers.has_key? \"content-type\"\n      @headers[\"content-type\"] = @file.mime_type || \"application/octet-stream\"\n    end\n\n    file_size = @file.size\n\n    @headers[\"content-length\"] = file_size.to_s\n\n    unless @headers.has_key? \"accept-ranges\"\n      @headers[\"accept-ranges\"] = request.safe? ? \"bytes\" : \"none\"\n    end\n\n    if request.headers.has_key?(\"range\") && \"GET\" == request.method\n      if !request.headers.has_key?(\"if-range\") || self.valid_if_range_header?(request.headers[\"if-range\"]?)\n        if range = request.headers[\"range\"].lchop? \"bytes=\"\n          s, e = range.split '-', 2\n\n          e = e.empty? ? file_size - 1 : e.to_i64\n\n          if s.empty?\n            s = file_size - e\n            e = file_size - 1\n          else\n            s = s.to_i64\n          end\n\n          if s <= e\n            e = Math.min e, file_size - 1\n\n            if s < 0 || s > e\n              self.status = :range_not_satisfiable\n              @headers[\"content-range\"] = \"bytes */#{file_size}\"\n            elsif e - s < file_size - 1\n              @max_length = e < file_size ? (e - s + 1).to_i64 : nil\n              @offset = s.to_i64\n\n              self.status = :partial_content\n              @headers[\"content-range\"] = \"bytes #{s}-#{e}/#{file_size}\"\n              @headers[\"content-length\"] = \"#{e - s + 1}\"\n            end\n          end\n        end\n      end\n    end\n  end\n\n  # :nodoc:\n  def write(output : IO) : Nil\n    unless @status.success?\n      return super output\n    end\n\n    if @max_length.try &.zero?\n      return\n    end\n\n    @writer.write(output) do |writer_io|\n      ::File.open(@file.path, \"rb\") do |file|\n        file.skip @offset\n\n        if limit = @max_length\n          IO.copy file, writer_io, limit\n        else\n          IO.copy file, writer_io\n        end\n      end\n    end\n\n    if @delete_file_after_send && ::File.file?(@file.path)\n      ::File.delete @file.path\n    end\n  end\n\n  private def cache_request?(request : AHTTP::Request) : Bool\n    # According to RFC 7232:\n    # A recipient must ignore If-Modified-Since if the request contains an If-None-Match header field\n    if (if_none_match = request.if_none_match) && (etag = self.etag)\n      match = {\"*\", etag}\n      if_none_match.any? { |et| match.includes? et }\n    elsif if_modified_since = request.headers[\"if-modified-since\"]?\n      header_time = ::HTTP.parse_time if_modified_since\n      last_modified = self.last_modified || @file.modification_time\n\n      # File mtime probably has a higher resolution than the header value.\n      # An exact comparison might be slightly off, so we add 1s padding.\n      # Static files should generally not be modified in subsecond intervals, so this is perfectly safe.\n      !!(header_time && last_modified <= header_time + 1.second)\n    else\n      false\n    end\n  end\n\n  private def valid_if_range_header?(header : String?) : Bool\n    return true if self.etag == header\n\n    return false unless last_modified = self.last_modified\n\n    ::HTTP.format_time(last_modified) == header\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/exception/conflicting_headers.cr",
    "content": "require \"./request_exception_interface\"\n\nclass Athena::HTTP::Exception::ConflictingHeaders < ArgumentError\n  include Athena::HTTP::Exception::RequestExceptionInterface\nend\n"
  },
  {
    "path": "src/components/http/src/exception/file.cr",
    "content": "class Athena::HTTP::Exception::File < ::File::Error\n  include Athena::HTTP::Exception\nend\n"
  },
  {
    "path": "src/components/http/src/exception/file_not_found.cr",
    "content": "class Athena::HTTP::Exception::FileNotFound < ::File::NotFoundError\n  include Athena::HTTP::Exception\nend\n"
  },
  {
    "path": "src/components/http/src/exception/file_size_limit_exceeded.cr",
    "content": "class Athena::HTTP::Exception::FileSizeLimitExceeded < ::File::Error\n  include Athena::HTTP::Exception\nend\n"
  },
  {
    "path": "src/components/http/src/exception/logic.cr",
    "content": "# Represents a code logic error that should lead directly to a fix in your code.\nclass Athena::HTTP::Exception::Logic < ::Exception\n  include Athena::HTTP::Exception\nend\n"
  },
  {
    "path": "src/components/http/src/exception/request_exception_interface.cr",
    "content": "# Exceptions that include this module should result in a 400 Bad Request response.\nmodule Athena::HTTP::Exception::RequestExceptionInterface\n  include Athena::HTTP::Exception\nend\n"
  },
  {
    "path": "src/components/http/src/exception/suspicious_operation.cr",
    "content": "class Athena::HTTP::Exception::SuspiciousOperation < ArgumentError\n  include Athena::HTTP::Exception::RequestExceptionInterface\nend\n"
  },
  {
    "path": "src/components/http/src/ext/conversion_types.cr",
    "content": "# :nodoc:\ndef Object.from_parameter(value)\n  value\nend\n\ndef Object.from_parameter?(value)\n  value\nend\n\n# :nodoc:\ndef Array.from_parameter(value : Array)\n  {% if T <= AHTTP::UploadedFile %}\n    return value\n  {% else %}\n    value.map { |item| T.from_parameter(item).as T }\n  {% end %}\nend\n\n# :nodoc:\ndef Bool.from_parameter(value : String) : Bool\n  case value\n  when \"true\", \"1\", \"yes\", \"on\"  then true\n  when \"false\", \"0\", \"no\", \"off\" then false\n  else\n    raise ArgumentError.new \"Invalid Bool: #{value}\"\n  end\nend\n\n# :nodoc:\ndef Bool.from_parameter?(value : String) : Bool?\n  case value\n  when \"true\", \"1\", \"yes\", \"on\"  then true\n  when \"false\", \"0\", \"no\", \"off\" then false\n  end\nend\n\n# :nodoc:\ndef Enum.from_parameter(value : String)\n  parse value\nend\n\n# :nodoc:\ndef Enum.from_parameter?(value : String)\n  parse? value\nend\n\n# :nodoc:\ndef Union.from_parameter(value : String)\n  # Process non nilable types first as they are more likely to work.\n  {% for type in T.sort_by { |t| t.nilable? ? 1 : 0 } %}\n    begin\n      return {{type}}.from_parameter value\n    rescue\n      # Noop to allow next T to be tried.\n    end\n  {% end %}\n  raise ArgumentError.new \"Invalid #{self}: #{value}\"\nend\n\n# :nodoc:\ndef Number.from_parameter(value : String) : Number\n  new value, whitespace: false\nend\n\n# :nodoc:\ndef Number.from_parameter?(value : String)\n  new value, whitespace: false rescue nil\nend\n\n# :nodoc:\ndef Nil.from_parameter(value : String) : Nil\n  raise ArgumentError.new \"Invalid Nil: #{value}\" unless value == \"null\"\nend\n\n# :nodoc:\ndef Nil.from_parameter?(value : String) : Nil\nend\n"
  },
  {
    "path": "src/components/http/src/file.cr",
    "content": "require \"./abstract_file\"\n\n# Represents a file on the filesystem without opening a file descriptor.\n# See `AHTTP::AbstractFile` for the available API.\nstruct Athena::HTTP::File < Athena::HTTP::AbstractFile\nend\n"
  },
  {
    "path": "src/components/http/src/header_utils.cr",
    "content": "# Includes various HTTP header utility methods.\nmodule Athena::HTTP::HeaderUtils\n  # Combines a 2D array of *parts* into a single Hash.\n  #\n  # Each child array should have one or two elements, with the first representing the key and the second representing the value.\n  # If there is no second value, `true` will be used.\n  # The keys of the resulting hash are all downcased.\n  #\n  # ```\n  # AHTTP::HeaderUtils.combine [[\"foo\", \"abc\"], [\"bar\"]] # => {\"foo\" => \"abc\", \"bar\" => true}\n  # ```\n  def self.combine(parts : Enumerable) : Hash(String, String | Bool)\n    parts.each_with_object({} of String => String | Bool) do |part, hash|\n      # Typing gets real funky due to the nested nature of the arrays from `.split`.\n      # Maybe there is a better way to go about it that could avoid that, but for now this seems to work :shrug:.\n      next if part.is_a?(String)\n      next if part.nil?\n\n      key = part[0]\n      value = part[1]?\n\n      next unless key.is_a?(String)\n\n      hash[key.downcase] = value.as?(String) || true\n    end\n  end\n\n  # Generates a `HTTP` [content-disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header value with the provided *disposition* and *filename*.\n  #\n  # If *filename* contains non `ASCII` characters, a sanitized version will be used as part of the `filename` directive,\n  # while an encoded version of it will be used as the `filename*` directive.\n  # The *fallback_filename* argument can be used to customize the `filename` directive value in this case.\n  #\n  # ```\n  # AHTTP::HeaderUtils.make_disposition :attachment, \"download.txt\"         # => attachment; filename=\"download.txt\"\n  # AHTTP::HeaderUtils.make_disposition :attachment, \"föö.html\"             # => attachment; filename=\"f__.html\"; filename*=utf-8''f%C3%B6%C3%B6.html\n  # AHTTP::HeaderUtils.make_disposition :attachment, \"föö.html\", \"foo.html\" # => attachment; filename=\"foo.html\"; filename*=utf-8''f%C3%B6%C3%B6.html\n  # ```\n  #\n  # This method can be used to enable downloads of dynamically generated files.\n  # I.e. that can't be handled via a static file event listener.\n  #\n  # ```\n  # AHTTP::Response.new(\n  #   file_contents,\n  #   headers: ::HTTP::Headers{\"content-disposition\" => AHTTP::HeaderUtils.make_disposition(:attachment, \"foo.pdf\")}\n  # )\n  # ```\n  #\n  # TIP: Checkout the [Getting Started](/getting_started/routing/#static-files) docs for an example of how to serve static files.\n  def self.make_disposition(disposition : AHTTP::BinaryFileResponse::ContentDisposition, filename : String, fallback_filename : String? = nil) : String\n    if fallback_filename.nil? && (!filename.ascii_only? || filename.includes?('%'))\n      fallback_filename = filename.gsub { |chr| chr.ascii? ? chr : '_' }\n    end\n\n    if fallback_filename.nil?\n      fallback_filename = filename\n    end\n\n    # The `%` character isn't valid in the fallback filename.\n    if fallback_filename.includes? '%'\n      raise ArgumentError.new \"The fallback filename cannot contain the '%' character.\"\n    end\n\n    # The fallback filename may not contain path separators.\n    if {'/', '\\\\'}.any? { |s| filename.includes? s }\n      raise ArgumentError.new \"The filename cannot include path separators.\"\n    elsif {'/', '\\\\'}.any? { |s| fallback_filename.includes? s }\n      raise ArgumentError.new \"The fallback filename cannot include path separators.\"\n    end\n\n    params = {\n      \"filename\" => fallback_filename,\n    }\n\n    if filename != fallback_filename\n      params[\"filename*\"] = \"utf-8''#{URI.encode_path_segment filename}\"\n    end\n\n    String.build do |io|\n      disposition.to_s.downcase io\n      io << \"; \"\n\n      self.to_string io, params, \"; \"\n    end\n  end\n\n  # Parses the directives out a the provided *header* string.\n  #\n  # ```\n  # AHTTP::HeaderUtils.parse \"max-age=0, must-revalidate\" # => {\"max-age\" => \"0\", \"must-revalidate\" => true}\n  # ```\n  def self.parse(header : String) : Hash(String, String | Bool)\n    values = Hash(String, String | Bool).new\n\n    header.strip.scan /(?:[^,\\\"]*+(?:\"[^\"]*+\\\")?)+[^,\\\"]*+/ do |match|\n      match_string = match[0].strip\n\n      next if match_string.blank?\n\n      if match_string.includes? '='\n        key, value = match_string.split '='\n        values[key] = value\n      else\n        values[match_string] = true\n      end\n    end\n\n    values\n  end\n\n  # Splits an HTTP *header* by one or more *separators*, provided in priority order.\n  # Returns an array with as many levels as there are *separators*.\n  #\n  # ```\n  # # First splits on `,`, then `;` as defined via the order of the separators.\n  # AHTTP::HeaderUtils.split \"da, en-gb;q=0.8\", \",;\" # => [[\"da\"], [\"en-gb\", \"q=0.8\"]]\n  # AHTTP::HeaderUtils.split \"da, en-gb;q=0.8\", \";,\" # => [[\"da\", \"en-gb\"], [\"q=0.8\"]]]\n  # ```\n  def self.split(header : String, separators : String) : Array\n    raise ArgumentError.new \"At least one separator must be specified.\" if separators.blank?\n\n    quoted_separators = Regex.escape separators\n\n    matches = header.strip.scan(\n      /\n      (?!\\s)\n        (?:\n          # quoted-string\n          \"(?:[^\"\\\\]|\\\\.)*(?:\"|\\\\|$)\n        |\n          # token\n          [^\"#{quoted_separators}]+\n        )+\n      (?<!\\s)\n    |\n      # separator\n      \\s*\n      (?<separator>[#{quoted_separators}])\n      \\s*\n    /x)\n\n    self.group_parts matches.map &.to_a, separators\n  end\n\n  # Joins the provided key/value *parts* into a string for use within an `HTTP` header.\n  #\n  # The key and value of each entry is joined with `=`, quoting the value if needed.\n  # All entries are then joined by the provided *separator*.\n  def self.to_string(separator : String | Char, **parts) : String\n    self.to_string parts.to_h, separator\n  end\n\n  # Joins a key/value pair *collection* into a string for use within an `HTTP` header.\n  #\n  # The key and value of each entry is joined with `=`, quoting the value if needed.\n  # All entries are then joined by the provided *separator*.\n  #\n  # ```\n  # AHTTP::HeaderUtils.to_string({\"foo\" => \"bar\", \"key\" => true}, \", \")          # => foo=bar, key\n  # AHTTP::HeaderUtils.to_string({\"foo\" => %q(\"foo\\ bar\"), \"key\" => true}, \", \") # => foo=\\\"foo\\\\\\ bar\\\", key\n  # ```\n  def self.to_string(collection : Hash, separator : String | Char) : String\n    String.build do |io|\n      self.to_string io, collection, separator\n    end\n  end\n\n  # Joins a key/value pair *collection* for use within an `HTTP` header; writing the data to the provided *io*.\n  #\n  # The key and value of each entry is joined with `=`, quoting the value if needed.\n  # All entries are then joined by the provided *separator*.\n  def self.to_string(io : IO, collection : Hash, separator : String | Char) : Nil\n    collection.join(io, separator) do |(k, v), join_io|\n      if true == v\n        join_io << k\n      else\n        join_io << k << '='\n        ::HTTP.quote_string v.to_s, join_io\n      end\n    end\n  end\n\n  # Decodes a quoted string.\n  def self.unquote(string : String) : String\n    string.gsub /\\\\(.)|\"/, \"\\\\1\"\n  end\n\n  private def self.group_parts(matches : Array, separators : String, first : Bool = true) : Array\n    separator = separators[0].to_s\n    separators = separators[1..].to_s\n    i = 0\n\n    if separators.empty? && !first\n      parts = [\"\"]\n\n      matches.each do |match|\n        if i.zero? && !match[1]?.nil?\n          i = 1\n          parts.insert 1, \"\"\n        else\n          parts[i] += self.unquote match.not_nil![0].not_nil!.to_s\n        end\n      end\n\n      return parts\n    end\n\n    parts = [] of String\n    part_matches = Hash(Int32, Array(Array(String?))).new { |hash, key| hash[key] = Array(Array(String?)).new }\n\n    matches.each do |match|\n      if match[1]? == separator\n        i += 1\n      else\n        part_matches[i] << match\n      end\n    end\n\n    part_matches.each_value.map do |m|\n      if separators.empty? && (unquoted = self.unquote m[0][0].to_s) && !unquoted.empty?\n        unquoted\n      elsif grouped_parts = self.group_parts(m, separators, false)\n        grouped_parts\n      end\n    end.to_a\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/ip_utils.cr",
    "content": "# Includes various IP address utility methods.\nmodule Athena::HTTP::IPUtils\n  @@checked_ips = Hash(String, Bool).new\n\n  # Returns `true` if the provided IPv4 or IPv6 *request_ip* is contained within the list of *ips* or subnets.\n  def self.check(request_ip : String, ips : String | Enumerable(String)) : Bool\n    ips = ips.is_a?(String) ? {ips} : ips\n\n    is_ipv6 = request_ip.count(':') > 1\n\n    ips.any? { |ip| is_ipv6 ? self.check_ipv6(request_ip, ip) : self.check_ipv4(request_ip, ip) }\n  end\n\n  # Returns `true` if *request_ip* matches *ip*, or is within the CIDR subnet.\n  def self.check_ipv4(request_ip : String, ip : String) : Bool\n    cache_key = \"#{request_ip}-#{ip}-v4\"\n\n    self.get_cached_result(cache_key).try do |result|\n      return result\n    end\n\n    unless request_ip_bytes = Socket::IPAddress.parse_v4_fields? request_ip\n      return self.set_cached_result cache_key, false\n    end\n\n    if ip.includes? '/'\n      address, netmask = ip.split '/', 2\n      netmask = netmask.to_i\n\n      if netmask.zero?\n        return self.set_cached_result cache_key, Socket::IPAddress.valid_v4?(address)\n      end\n\n      if netmask < 0 || netmask > 32\n        return self.set_cached_result cache_key, false\n      end\n    else\n      address = ip\n      netmask = 32\n    end\n\n    unless address_bytes = Socket::IPAddress.parse_v4_fields?(address)\n      return self.set_cached_result cache_key, false\n    end\n\n    request_ip_decimal = IO::ByteFormat::BigEndian.decode(UInt32, request_ip_bytes.to_slice)\n    address_decimal = IO::ByteFormat::BigEndian.decode(UInt32, address_bytes.to_slice)\n    mask = UInt32::MAX << (32 - netmask)\n\n    (request_ip_decimal & mask) == (address_decimal & mask)\n  end\n\n  # :ditto:\n  def self.check_ipv6(request_ip : String, ip : String) : Bool\n    cache_key = \"#{request_ip}-#{ip}-v6\"\n\n    self.get_cached_result(cache_key).try do |result|\n      return result\n    end\n\n    unless request_ip_bytes = Socket::IPAddress.parse_v6_fields? request_ip\n      return self.set_cached_result cache_key, false\n    end\n\n    if ip.includes? '/'\n      address, netmask = ip.split '/', 2\n      netmask = netmask.to_i\n\n      unless address_bytes = Socket::IPAddress.parse_v6_fields? address\n        return self.set_cached_result cache_key, false\n      end\n\n      if netmask.zero?\n        # If it made it this far `address_bytes` is valid so it would always be a valid IP\n        return self.set_cached_result cache_key, true\n      end\n\n      if netmask < 1 || netmask > 128\n        return self.set_cached_result cache_key, false\n      end\n    else\n      unless address_bytes = Socket::IPAddress.parse_v6_fields? ip\n        return self.set_cached_result cache_key, false\n      end\n\n      netmask = 128\n    end\n\n    0.upto(netmask // 16) do |i|\n      left = netmask - 16 * i\n      left = (left <= 16) ? left : 16\n      mask = ~(0xFFFF >> left) & 0xFFFF\n\n      if ((address_bytes[i]? || 0) & mask) != ((request_ip_bytes[i]? || 0) & mask)\n        return self.set_cached_result cache_key, false\n      end\n    end\n\n    self.set_cached_result cache_key, true\n  end\n\n  private def self.get_cached_result(key : String) : Bool?\n    if @@checked_ips.has_key? key\n      # Move the item last in the cache\n      value = @@checked_ips[key]\n      @@checked_ips.delete key\n      return @@checked_ips[key] = value\n    end\n\n    nil\n  end\n\n  private def self.set_cached_result(key : String, value : Bool) : Bool\n    @@checked_ips[key] = value\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/parameter_bag.cr",
    "content": "# A container for storing key/value pairs. Can be used to store arbitrary data within the context of a request.\n# It can be accessed via `AHTTP::Request#attributes`.\nstruct Athena::HTTP::ParameterBag\n  private abstract struct Param\n    abstract def value\n    abstract def type_name : String\n\n    def inspect(io : IO) : Nil\n      io << \"#<Param(\" << self.type_name << \")>\"\n    end\n  end\n\n  private record Parameter(T) < Param, value : T do\n    def type_name : String\n      {{ T.stringify }}\n    end\n  end\n\n  @parameters : Hash(String, Param) = Hash(String, Param).new\n\n  # Returns `true` if a parameter with the provided *name* exists, otherwise `false`.\n  def has?(name : String) : Bool\n    @parameters.has_key? name\n  end\n\n  # Returns `true` if a parameter with the provided *name* exists and is of the provided *type*, otherwise `false`.\n  def has?(name : String, type : T.class) : Bool forall T\n    @parameters[name]?.try { |p| p.value.class == T } || false\n  end\n\n  # Returns the value of the parameter with the provided *name* if it exists, otherwise `nil`.\n  def get?(name : String)\n    @parameters[name]?.try &.value\n  end\n\n  # Returns the value of the parameter with the provided *name* casted to the provided *type* if it exists, otherwise `nil`.\n  def get?(name : String, type : T?.class) : T? forall T\n    self.get?(name).as? T?\n  end\n\n  # Returns the value of the parameter with the provided *name*.\n  #\n  # Raises a `KeyError` if no parameter with that name exists.\n  def get(name : String)\n    @parameters.fetch(name) { raise KeyError.new \"No parameter exists with the name '#{name}'.\" }.value\n  end\n\n  # Returns the value of the parameter with the provided *name*, casted to the provided *type*.\n  #\n  # Raises a `KeyError` if no parameter with that name exists.\n  def get(name : String, type : T.class) : T forall T\n    self.get(name).as T\n  end\n\n  {% for type in [Bool, String] + Number::Primitive.union_types %}\n    # Returns the value of the parameter with the provided *name* as a `{{type}}`.\n    def get(name : String, _type : {{type}}.class) : {{type}}\n      {{type}}.from_parameter(self.get(name)).as {{type}}\n    end\n\n    # Returns the value of the parameter with the provided *name* as a `{{type}}`, or `nil` if it does not exist.\n    def get?(name : String, _type : {{type}}?.class) : {{type}}?\n      return nil unless (value = self.get? name)\n      {{type}}.from_parameter?(value).as? {{type}}?\n    end\n  {% end %}\n\n  def set(hash : Hash) : Nil\n    hash.each do |key, value|\n      self.set key, value\n    end\n  end\n\n  # Sets a parameter with the provided *name* to *value*.\n  def set(name : String, value : T) : Nil forall T\n    self.set name, value, T\n  end\n\n  # Sets a parameter with the provided *name* to *value*, restricted to the given *type*.\n  def set(name : String, value : T, type : T.class) : Nil forall T\n    @parameters[name] = Parameter(T).new value\n  end\n\n  # Removes the parameter with the provided *name*.\n  def remove(name : String) : Nil\n    @parameters.delete name\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/redirect_response.cr",
    "content": "require \"./response\"\n\n# Represents an HTTP response that does a redirect.\n#\n# Can be used as an easier way to handle redirects as well as providing type safety that a route should redirect.\n#\n# ```\n# require \"athena\"\n#\n# class RedirectController < ATH::Controller\n#   @[ARTA::Get(path: \"/go_to_crystal\")]\n#   def redirect_to_crystal : AHTTP::RedirectResponse\n#     AHTTP::RedirectResponse.new \"https://crystal-lang.org\"\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /go_to_crystal # => (redirected to https://crystal-lang.org)\n# ```\nclass Athena::HTTP::RedirectResponse < Athena::HTTP::Response\n  # The url that the request will be redirected to.\n  getter url : String\n\n  # Creates a response that should redirect to the provided *url* with the provided *status*, defaults to 302.\n  #\n  # An ArgumentError is raised if *url* is blank, or if *status* is not a valid redirection status code.\n  def initialize(url : String | Path | URI, status : ::HTTP::Status | Int32 = ::HTTP::Status::FOUND, headers : ::HTTP::Headers | AHTTP::Response::Headers = AHTTP::Response::Headers.new)\n    @url = url.to_s\n\n    raise ArgumentError.new \"Cannot redirect to an empty URL.\" if @url.blank?\n\n    headers[\"location\"] = @url\n\n    super \"\", status, headers\n\n    raise ArgumentError.new \"'#{@status.value}' is not an HTTP redirect status code.\" unless @status.redirection?\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/request.cr",
    "content": "# Wraps an [HTTP::Request](https://crystal-lang.org/api/HTTP/Request.html) instance to provide additional functionality.\n#\n# Forwards all additional methods to the wrapped [`HTTP::Request`](https://crystal-lang.org/api/HTTP/Request.html) instance.\nclass Athena::HTTP::Request\n  # Represents the supported [Proxy Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling#forwarding_client_information_through_proxies).\n  # Can be used via `AHTTP::Request.set_trusted_proxies` to whitelist which headers are allowed.\n  #\n  # See the [external documentation](/guides/proxies) for more information.\n  @[Flags]\n  enum ProxyHeader\n    # The [`forwarded`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded) header as defined by [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239).\n    FORWARDED\n\n    # The [`x-forwarded-for`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) header.\n    FORWARDED_FOR\n\n    # The [`x-forwarded-host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) header.\n    FORWARDED_HOST\n\n    # The [`x-forwarded-proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) header.\n    FORWARDED_PROTO\n\n    # Similar to `FORWARDED_HOST`, but exclusive to the port number.\n    FORWARDED_PORT\n\n    # Returns the string header name for a given proxy header.\n    #\n    # ```\n    # AHTTP::Request::ProxyHeader::FORWARDED_PROTO.header => \"x-forwarded-proto\"\n    # ```\n    def header : String\n      if override = AHTTP::Request.trusted_header_overrides[self]?\n        return override\n      end\n\n      case self\n      when .forwarded?       then \"forwarded\"\n      when .forwarded_for?   then \"x-forwarded-for\"\n      when .forwarded_host?  then \"x-forwarded-host\"\n      when .forwarded_proto? then \"x-forwarded-proto\"\n      when .forwarded_port?  then \"x-forwarded-port\"\n      else\n        raise \"BUG: requested header of unexpected proxy header type\"\n      end\n    end\n\n    # Returns the `forwarded` param related to a given proxy header.\n    #\n    # ```\n    # AHTTP::Request::ProxyHeader::FORWARDED_PROTO.forwarded_param => \"proto\"\n    # ```\n    def forwarded_param : String?\n      case self\n      when .forwarded_for?   then \"for\"\n      when .forwarded_host?  then \"host\"\n      when .forwarded_proto? then \"proto\"\n      when .forwarded_port?  then \"host\"\n      end\n    end\n  end\n\n  # Represents the supported built in formats; mapping the format name to its valid `MIME` type(s).\n  #\n  # Additional formats may be registered via `.register_format`.\n  FORMATS = {\n    \"atom\"    => Set{\"application/atom+xml\"},\n    \"css\"     => Set{\"text/css\"},\n    \"csv\"     => Set{\"text/csv\"},\n    \"form\"    => Set{\"application/x-www-form-urlencoded\", \"multipart/form-data\"},\n    \"hal\"     => Set{\"application/hal+json\", \"application/hal+xml\"},\n    \"html\"    => Set{\"text/html\", \"application/xhtml+xml\"},\n    \"js\"      => Set{\"application/javascript\", \"application/x-javascript\", \"text/javascript\"},\n    \"json\"    => Set{\"application/json\", \"application/x-json\"},\n    \"jsonapi\" => Set{\"application/vnd.api+json\"},\n    \"jsonld\"  => Set{\"application/ld+json\"},\n    \"pdf\"     => Set{\"application/pdf\"},\n    \"problem\" => Set{\"application/problem+json\"},\n    \"rdf\"     => Set{\"application/rdf+xml\"},\n    \"rss\"     => Set{\"application/rss+xml\"},\n    \"soap\"    => Set{\"application/soap+xml\"},\n    \"txt\"     => Set{\"text/plain\"},\n    \"wbxml\"   => Set{\"application/vnd.wap.wbxml\"},\n    \"xml\"     => Set{\"text/xml\", \"application/xml\", \"application/x-xml\"},\n    \"yaml\"    => Set{\"text/yaml\", \"application/x-yaml\"},\n  }\n\n  # Maps a [structured syntax suffix](https://www.iana.org/assignments/media-type-structured-suffix/media-type-structured-suffix.xhtml)\n  # (per [RFC 6839](https://datatracker.ietf.org/doc/html/rfc6839) / [RFC 7303](https://datatracker.ietf.org/doc/html/rfc7303))\n  # to its underlying format.\n  #\n  # Used by `#format` when the MIME type isn't an exact match in `FORMATS`; e.g. `application/vnd.github+json` -> `json`.\n  private STRUCTURED_SUFFIX_FORMATS = {\n    \"json\"  => \"json\",\n    \"xml\"   => \"xml\",\n    \"xhtml\" => \"html\",\n    \"cbor\"  => \"cbor\",\n    \"zip\"   => \"zip\",\n    \"ber\"   => \"asn1\",\n    \"der\"   => \"asn1\",\n    \"tlv\"   => \"tlv\",\n    \"wbxml\" => \"xml\",\n    \"yaml\"  => \"yaml\",\n  }\n\n  # Returns which `AHTTP::Request::ProxyHeader`s have been whitelisted by the application as set via `.set_trusted_proxies`, defaulting to all of them.\n  class_getter trusted_header_set : AHTTP::Request::ProxyHeader = :all\n\n  # Returns the list of trusted proxy IP addresses as set via `.set_trusted_proxies`.\n  class_getter trusted_proxies : Array(String) = [] of String\n\n  # Returns the list of trusted host patterns set via `.set_trusted_hosts`.\n  class_getter trusted_host_patterns : Array(Regex) = [] of Regex\n\n  protected class_getter trusted_header_overrides : Hash(AHTTP::Request::ProxyHeader, String) = {} of AHTTP::Request::ProxyHeader => String\n  protected class_getter trusted_hosts : Array(String) = [] of String\n\n  # Allows setting a list of *host_patterns* used to whitelist the allowed hostnames of requests.\n  # If there is at least one pattern defined, the `#host` method will raise an exception if the request's hostname does _NOT_ match any of the patterns.\n  #\n  # See the [external documentation](/guides/proxies) for more information if using the [full framework](/getting_started).\n  def self.set_trusted_hosts(host_patterns : Array(Regex)) : Nil\n    @@trusted_host_patterns = host_patterns.map! { |pattern| /#{pattern}/i }\n    @@trusted_hosts.clear\n  end\n\n  # Allows setting a list of *trusted_proxies*, and which `AHTTP::Request::ProxyHeader` should be whitelisted.\n  # The provided proxies are expected to be either IPv4 and/or IPv6 addresses.\n  # The special `\"REMOTE_ADDRESS\"` string is also supported that will map to the current request's remote address.\n  #\n  # See the [external documentation](/guides/proxies) for more information.\n  def self.set_trusted_proxies(trusted_proxies : Enumerable(String), @@trusted_header_set : AHTTP::Request::ProxyHeader) : Nil\n    @@trusted_proxies = trusted_proxies.to_a\n  end\n\n  # Allows overriding the header name to look for off the request for a given `AHTTP::Request::ProxyHeader`.\n  # In some cases a proxy might not use the exact `x-forwarded-*` header name.\n  #\n  # See the [external documentation](/guides/proxies/#custom-headers) for more information.\n  def self.override_trusted_header(header : AHTTP::Request::ProxyHeader, name : String) : Nil\n    @@trusted_header_overrides[header] = name\n  end\n\n  # Registers the provided *format* with the provided *mime_types*.\n  # Can also be used to change the *mime_types* supported for an existing *format*.\n  #\n  # ```\n  # AHTTP::Request.register_format \"some_format\", {\"some/mimetype\"}\n  # ```\n  def self.register_format(format : String, mime_types : Indexable(String)) : Nil\n    FORMATS[format] = mime_types.to_set\n  end\n\n  # Returns the `MIME` types for the provided *format*.\n  #\n  # ```\n  # AHTTP::Request.mime_types \"txt\" # => Set{\"text/plain\"}\n  # ```\n  def self.mime_types(format : String) : Set(String)\n    FORMATS[format]? || Set(String).new\n  end\n\n  # See `AHTTP::ParameterBag`.\n  getter attributes : AHTTP::ParameterBag = AHTTP::ParameterBag.new\n\n  # If [enabled](/Framework/Bundle/Schema/FileUploads/), Athena will populate this hash with files from the request body of `multipart/form-data` requests.\n  #\n  # The keys of the hash map to the [name](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#name) attribute of the related file input control.\n  # The value of the hash is an array in order to handle multi-file uploads using the same name.\n  getter files : Hash(String, Array(AHTTP::UploadedFile)) = Hash(String, Array(AHTTP::UploadedFile)).new { |hash, key| hash[key] = [] of AHTTP::UploadedFile }\n\n  @request_data : ::HTTP::Params?\n\n  # Sets the `#request_format` to the explicitly passed format.\n  setter request_format : String? = nil\n\n  # Returns the raw wrapped `::HTTP::Request` instance.\n  getter request : ::HTTP::Request\n\n  @is_host_valid : Bool = true\n  @is_forwarded_valid : Bool = true\n\n  @trusted_values_cache = Hash(String, Array(String)).new\n\n  # :nodoc:\n  forward_missing_to @request\n\n  def self.new(method : String, path : String, headers : ::HTTP::Headers? = nil, body : String | Bytes | IO | Nil = nil, version : String = \"HTTP/1.1\") : self\n    new ::HTTP::Request.new method.upcase, path, headers, body, version\n  end\n\n  def self.new(request : self) : self\n    request\n  end\n\n  def initialize(@request : ::HTTP::Request); end\n\n  # Returns the first `MIME` type for the provided *format* if defined, otherwise returns `nil`.\n  #\n  # ```\n  # request.mime_type \"txt\" # => \"text/plain\"\n  # ```\n  def mime_type(format : String) : String?\n    self.class.mime_types(format).first?\n  end\n\n  # Returns the [Format](/HTTP/Request/#Athena::HTTP::Request::FORMATS) of the request based on its `content-type` header, or `nil` if the header is missing.\n  def content_type_format : String?\n    self.format @request.headers.fetch \"content-type\", \"\"\n  end\n\n  # Returns the format for the provided *mime_type*, or `nil` if it cannot be resolved.\n  #\n  # Resolution order:\n  # 1. Exact match against `FORMATS`.\n  # 2. Canonical MIME type match (i.e. the portion before any `;` parameters).\n  # 3. Structured syntax suffix per [RFC 6839](https://datatracker.ietf.org/doc/html/rfc6839) / [RFC 7303](https://datatracker.ietf.org/doc/html/rfc7303); e.g. `application/vnd.github+json` -> `json`.\n  # 4. If *subtype_fallback* is `true` and the subtype contains no `+`, returns the subtype with any `x-` prefix stripped; e.g. `application/x-foo` -> `foo`.\n  #\n  # ```\n  # request.format \"text/plain\"                                # => \"txt\"\n  # request.format \"application/vnd.github+json\"               # => \"json\"\n  # request.format \"application/x-foo\", subtype_fallback: true # => \"foo\"\n  # ```\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  def format(mime_type : String, subtype_fallback : Bool = false) : String?\n    canonical_mime_type = nil\n\n    if mime_type.includes? ';'\n      canonical_mime_type = mime_type.split(';', 2).first.strip\n    end\n\n    FORMATS.each do |format, mime_types|\n      return format if mime_types.includes? mime_type\n      return format if canonical_mime_type && mime_types.includes? canonical_mime_type\n    end\n\n    canonical_mime_type ||= mime_type\n\n    if canonical_mime_type.starts_with?(\"application/\") && (plus_idx = canonical_mime_type.rindex('+'))\n      suffix = canonical_mime_type[(plus_idx + 1)..]\n      if format = STRUCTURED_SUFFIX_FORMATS[suffix]?\n        return format\n      end\n    end\n\n    if subtype_fallback && (slash_idx = canonical_mime_type.index('/'))\n      subtype = canonical_mime_type[(slash_idx + 1)..]\n      subtype = subtype[2..] if subtype.starts_with?(\"x-\")\n      return subtype unless subtype.includes?('+')\n    end\n  end\n\n  # Returns the host name the request originated from.\n  #\n  # Supports reading from `AHTTP::Request::ProxyHeader::FORWARDED_HOST`, falling back on the `\"host\"` header.\n  #\n  # See the [external documentation](/guides/proxies) for more information.\n  def host : String?\n    if self.from_trusted_proxy? && (host = self.get_trusted_values(ProxyHeader::FORWARDED_HOST)) && !host.empty?\n      host = host.first\n    elsif !(host = @request.headers[\"host\"]?)\n      return\n    end\n\n    # Trim and ensure there is no port number\n    # downcase as per RFC 952/2181\n    host = host.strip.gsub(/:\\d+$/, \"\").downcase\n\n    # Ensure host does not contain forbidden characters as pert RFC 952/2181\n    if host.presence && !host.gsub(/(?:^\\[)?[a-zA-Z0-9-:\\]_]+\\.?/, \"\").empty?\n      return unless @is_host_valid\n      @is_host_valid = false\n\n      raise AHTTP::Exception::SuspiciousOperation.new \"Invalid Host: '#{host}'.\"\n    end\n\n    unless @@trusted_host_patterns.empty?\n      return host if @@trusted_hosts.includes? host\n\n      @@trusted_host_patterns.each do |pattern|\n        if host.matches? pattern\n          @@trusted_hosts << host\n          return host\n        end\n      end\n\n      return unless @is_host_valid\n      @is_host_valid = false\n\n      raise AHTTP::Exception::SuspiciousOperation.new \"Untrusted Host: '#{host}'.\"\n    end\n\n    host\n  end\n\n  # Returns an `::HTTP::Params` instance based on this request's form data body.\n  def request_data\n    @request_data ||= self.parse_request_data\n  end\n\n  # Returns the format for this request.\n  #\n  # First checks if a format was explicitly set via `#request_format=`.\n  # Next, will check for the `_format` request `#attributes`, finally falling back on the provided *default*.\n  def request_format(default : String? = \"json\") : String?\n    if @request_format.nil?\n      @request_format = self.attributes.get? \"_format\", String\n    end\n\n    @request_format || default\n  end\n\n  # Returns `true` if this request's `#method` is [safe](https://tools.ietf.org/html/rfc7231#section-4.2.1).\n  # Otherwise returns `false`.\n  def safe? : Bool\n    @request.method.in? \"GET\", \"HEAD\", \"OPTIONS\", \"TRACE\"\n  end\n\n  # Returns the port on which the request is made.\n  #\n  # Supports reading from both `AHTTP::Request::ProxyHeader::FORWARDED_PORT` and `AHTTP::Request::ProxyHeader::FORWARDED_HOST`, falling back on the `\"host\"` header, then `#scheme`.\n  #\n  # See the [external documentation](/guides/proxies) for more information.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  def port : Int32\n    if self.from_trusted_proxy? && (host = self.get_trusted_values(ProxyHeader::FORWARDED_PORT)) && !host.empty?\n      host = host.first\n    elsif self.from_trusted_proxy? && (host = self.get_trusted_values(ProxyHeader::FORWARDED_HOST)) && !host.empty?\n      host = host.first\n    elsif !(host = @request.headers[\"host\"]?)\n      return self.secure? ? 443 : 80\n    end\n\n    pos = if host.starts_with? '['\n            # Assume the host will have a closing `]` if it has a beginning one\n            host.index ':', host.index!(']')\n          else\n            host.index ':'\n          end\n\n    if pos && (port = host[(pos + 1)..]?)\n      return port.to_i\n    end\n\n    self.secure? ? 443 : 80\n  end\n\n  # Returns the scheme of this request.\n  def scheme : String\n    self.secure? ? \"https\" : \"http\"\n  end\n\n  # Returns `true` the request was made over HTTPS, otherwise returns `false`.\n  #\n  # Supports reading from `AHTTP::Request::ProxyHeader::FORWARDED_PROTO`.\n  #\n  # See the [external documentation](/guides/proxies) for more information.\n  def secure? : Bool\n    if self.from_trusted_proxy? && (proto = self.get_trusted_values(ProxyHeader::FORWARDED_PROTO)) && !proto.empty?\n      return proto.first.downcase.in? \"https\", \"on\", \"ssl\", \"1\"\n    end\n\n    # TODO: Possibly have this be based on if server was started with `bind_tls`\n    # or if there is eventually some way to access TLS info off `@request`.\n    false\n  end\n\n  # Returns `true` if this request originated from a trusted proxy.\n  #\n  # See the [external documentation](/guides/proxies) for more information.\n  def from_trusted_proxy? : Bool\n    return false unless trusted_proxies = self.trusted_proxies\n    return false unless remote_address = self.remote_address\n\n    AHTTP::IPUtils.check remote_address, trusted_proxies\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity:\n  private def get_trusted_values(type : ProxyHeader) : Array(String)\n    cache_key = \"#{type}-#{@@trusted_header_set.includes?(type) ? @request.headers[type.header]? : \"\"}-#{@request.headers[ProxyHeader::FORWARDED.header]?}\"\n\n    if result = @trusted_values_cache[cache_key]?\n      return result\n    end\n\n    client_values = [] of String\n    forwarded_values = [] of String\n\n    if @@trusted_header_set.includes?(type) && (header_value = @request.headers[type.header]?)\n      header_value.split(',').each do |part|\n        client_values << \"#{type.forwarded_port? ? \"0.0.0.0:\" : \"\"}#{part.strip}\"\n      end\n    end\n\n    if @@trusted_header_set.includes?(ProxyHeader::FORWARDED) && (forwarded_param = type.forwarded_param) && (forwarded = @request.headers[ProxyHeader::FORWARDED.header]?)\n      AHTTP::HeaderUtils.split(forwarded, \",;=\").each do |sub_parts|\n        # In this particular context compiler gets confused, so lets make it happy by skipping unexpected typed parts, which should never happen.\n        next if sub_parts.is_a?(String)\n        next if sub_parts.nil?\n\n        unless v = HeaderUtils.combine(sub_parts)[forwarded_param]?.as?(String?)\n          next\n        end\n\n        if type.forwarded_port?\n          last_colon_idx = v.rindex(':')\n          if v.ends_with?(']') || last_colon_idx.nil?\n            v = self.secure? ? \":443\" : \":80\"\n          end\n\n          v = \"0.0.0.0#{v[last_colon_idx..-1]}\"\n        end\n        forwarded_values << v\n      end\n    end\n\n    if forwarded_values == client_values || client_values.empty?\n      return @trusted_values_cache[cache_key] = forwarded_values\n    end\n\n    if forwarded_values.empty?\n      return @trusted_values_cache[cache_key] = client_values\n    end\n\n    unless @is_forwarded_valid\n      return @trusted_values_cache[cache_key] = [] of String\n    end\n    @is_forwarded_valid = false\n\n    raise AHTTP::Exception::ConflictingHeaders.new \"The request has both a trusted '#{ProxyHeader::FORWARDED.header}' header and a trusted '#{type.header}' header, conflicting with each other. \\\n      You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.\"\n  end\n\n  private def trusted_proxies : Array(String)?\n    return if (trusted_proxies = @@trusted_proxies).empty?\n    return unless remote_address = self.remote_address\n\n    trusted_proxies.map do |proxy|\n      \"REMOTE_ADDRESS\" == proxy ? remote_address : proxy\n    end\n  end\n\n  private def remote_address : String?\n    return unless (remote_address = @request.remote_address).is_a? Socket::IPAddress\n\n    remote_address.address\n  end\n\n  private def parse_request_data : ::HTTP::Params\n    ::HTTP::Params.parse @request.body.try(&.gets_to_end) || \"\"\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/request_matcher/attributes.cr",
    "content": "# Checks if all specified `AHTTP::Request#attributes` match the provided patterns.\nstruct Athena::HTTP::RequestMatcher::Attributes\n  include Interface\n\n  def initialize(@regexes : Hash(String, Regex)); end\n\n  # :inherit:\n  def matches?(request : AHTTP::Request) : Bool\n    @regexes.each do |key, regex|\n      attribute = request.attributes.get key\n      return false unless attribute.is_a? String\n      return false unless attribute.matches? regex\n    end\n\n    true\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/request_matcher/header.cr",
    "content": "# Checks the presence of HTTP headers in an `AHTTP::Request`.\nstruct Athena::HTTP::RequestMatcher::Header\n  include Interface\n\n  @headers : Array(String)\n\n  def self.new(*headers : String)\n    new headers.to_a\n  end\n\n  def initialize(@headers : Array(String)); end\n\n  # :inherit:\n  def matches?(request : AHTTP::Request) : Bool\n    return true if @headers.empty?\n\n    headers = request.headers\n\n    @headers.each do |header|\n      return false unless headers.has_key? header\n    end\n\n    true\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/request_matcher/hostname.cr",
    "content": "# Checks if the `AHTTP::Request#hostname` matches the allowed pattern.\nstruct Athena::HTTP::RequestMatcher::Hostname\n  include Interface\n\n  def initialize(regex : Regex)\n    @regex = Regex.new regex.source, :ignore_case\n  end\n\n  # :inherit:\n  def matches?(request : AHTTP::Request) : Bool\n    return false unless hostname = request.host\n\n    hostname.matches? @regex\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/request_matcher/method.cr",
    "content": "# Checks if the `AHTTP::Request#method` is allowed.\nstruct Athena::HTTP::RequestMatcher::Method\n  include Interface\n\n  @methods : Array(String)\n\n  def self.new(*methods : String)\n    new methods.to_a\n  end\n\n  def initialize(@methods : Array(String))\n    methods.map! &.upcase\n  end\n\n  # :inherit:\n  def matches?(request : AHTTP::Request) : Bool\n    return false if @methods.empty?\n\n    @methods.includes? request.method\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/request_matcher/path.cr",
    "content": "# Checks if the `AHTTP::Request#path` matches the allowed pattern.\nstruct Athena::HTTP::RequestMatcher::Path\n  include Interface\n\n  def initialize(@regex : Regex); end\n\n  # :inherit:\n  def matches?(request : AHTTP::Request) : Bool\n    URI.decode(request.path).matches? @regex\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/request_matcher/query_parameter.cr",
    "content": "# Checks the presence of HTTP query parameters in an `AHTTP::Request`.\nstruct Athena::HTTP::RequestMatcher::QueryParameter\n  include Interface\n\n  @parameters : Array(String)\n\n  def self.new(*parameters : String)\n    new parameters.to_a\n  end\n\n  def initialize(@parameters : Array(String)); end\n\n  # :inherit:\n  def matches?(request : AHTTP::Request) : Bool\n    return true if @parameters.empty?\n\n    query_params = request.query_params\n\n    @parameters.each do |parameter|\n      return false unless query_params.has_key? parameter\n    end\n\n    true\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/request_matcher.cr",
    "content": "# Verifies that all [checks](/HTTP/RequestMatcher/Interface/) match against an `AHTTP::Request` instance.\n#\n# ```\n# matcher = AHTTP::RequestMatcher.new(\n#   AHTTP::RequestMatcher::Path.new(%r(/admin/foo)),\n#   AHTTP::RequestMatcher::Method.new(\"GET\"),\n# )\n#\n# matcher.matches?(AHTTP::Request.new \"GET\", \"/admin/foo\")  # => true\n# matcher.matches?(AHTTP::Request.new \"POST\", \"/admin/foo\") # => false\n# ```\nclass Athena::HTTP::RequestMatcher\n  # Represents a strategy that can be used to match an `AHTTP::Request`.\n  # This interface can be used as a generic way to determine if some logic should be enabled for a given request based on the configured rules.\n  module Interface\n    # Decides whether the rule(s) implemented by the strategy matches the provided *request*.\n    abstract def matches?(request : AHTTP::Request) : Bool\n  end\n\n  include Interface\n\n  def self.new(*matchers : AHTTP::RequestMatcher::Interface)\n    new matchers.map &.as(AHTTP::RequestMatcher::Interface)\n  end\n\n  def initialize(@matchers : Iterable(AHTTP::RequestMatcher::Interface)); end\n\n  # :inherit:\n  def matches?(request : AHTTP::Request) : Bool\n    @matchers.all? &.matches? request\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/request_store.cr",
    "content": "# Stores a `AHTTP::Request` object.\nclass Athena::HTTP::RequestStore\n  property! request : AHTTP::Request\n\n  # Resets the store, removing the reference to the request.\n  #\n  # Used internally after the response has been returned.\n  protected def reset : Nil\n    @request = nil\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/response.cr",
    "content": "# Represents an `HTTP` response that should be returned to the client.\n#\n# Contains the content, status, and headers that should be applied to the actual `HTTP::Server::Response`.\n#\n# The `#content` is written all at once to the server response's `IO`.\nclass Athena::HTTP::Response\n  # Determines how the content of an `AHTTP::Response` will be written to the requests' response `IO`.\n  #\n  # By default the content is written directly to the requests' response `IO` via `AHTTP::Response::DirectWriter`.\n  # However, custom writers can be implemented to customize that behavior. The most common use case would be for compression.\n  #\n  # Writers can also be defined as services and injected into a listener if they require additional external dependencies.\n  #\n  # ### Example\n  #\n  # ```\n  # require \"athena\"\n  # require \"compress/gzip\"\n  #\n  # # Define a custom writer to gzip the response\n  # struct GzipWriter < AHTTP::Response::Writer\n  #   def write(output : IO, & : IO -> Nil) : Nil\n  #     Compress::Gzip::Writer.open(output) do |gzip_io|\n  #       yield gzip_io\n  #     end\n  #   end\n  # end\n  #\n  # # Define a new event listener to handle applying this writer\n  # @[ADI::Register]\n  # struct CompressionListener\n  #   @[AEDA::AsEventListener(priority: -256)]\n  #   def on_response(event : AHK::Events::Response) : Nil\n  #     # If the request supports gzip encoding\n  #     if event.request.headers.includes_word?(\"accept-encoding\", \"gzip\")\n  #       # Change the `AHTTP::Response` object's writer to be our `GzipWriter`\n  #       event.response.writer = GzipWriter.new\n  #\n  #       # Set the encoding of the response to gzip\n  #       event.response.headers[\"content-encoding\"] = \"gzip\"\n  #     end\n  #   end\n  # end\n  #\n  # class ExampleController < ATH::Controller\n  #   @[ARTA::Get(\"/users\")]\n  #   def users : Array(User)\n  #     User.all\n  #   end\n  # end\n  #\n  # ATH.run\n  #\n  # # GET /users # => [{\"id\":1,...},...] (gzipped)\n  # ```\n  abstract struct Writer\n    # Accepts an *output* `IO` that the content of the response should be written to.\n    abstract def write(output : IO, & : IO -> Nil) : Nil\n  end\n\n  # The default `AHTTP::Response::Writer` for an `AHTTP::Response`.\n  #\n  # Writes directly to the *output* `IO`.\n  struct DirectWriter < Writer\n    # :inherit:\n    #\n    # The *output* `IO` is yielded directly.\n    def write(output : IO, & : IO -> Nil) : Nil\n      yield output\n    end\n  end\n\n  # See `AHTTP::Response::Writer`.\n  setter writer : AHTTP::Response::Writer = AHTTP::Response::DirectWriter.new\n\n  # Returns the `::HTTP::Status` of this response.\n  getter status : ::HTTP::Status\n\n  # Returns the character set this response is encoded as.\n  property charset : String = \"utf-8\"\n\n  # Returns the response headers of this response.\n  getter headers : AHTTP::Response::Headers\n\n  # Returns the contents of this response.\n  getter content : String\n\n  # Creates a new response with optional *content*, *status*, and *headers* arguments.\n  def initialize(content : String? = nil, status : ::HTTP::Status | Int32 = ::HTTP::Status::OK, headers : ::HTTP::Headers | AHTTP::Response::Headers = AHTTP::Response::Headers.new)\n    @content = content || \"\"\n    @status = ::HTTP::Status.new status\n    @headers = AHTTP::Response::Headers.new headers\n  end\n\n  # Sets the response content.\n  def content=(content : String?) : self\n    @content = content || \"\"\n\n    self\n  end\n\n  # Sends `self` to the client based on the provided *context*.\n  #\n  # How the content gets written can be customized via an `AHTTP::Response::Writer`.\n  def send(request : AHTTP::Request, response : ::HTTP::Server::Response) : self\n    # Ensure the response is valid.\n    self.prepare request\n\n    # Apply the `AHTTP::Response` to the actual `::HTTP::Server::Response` object.\n    response.headers.merge! @headers\n    response.status = @status\n\n    @headers.cookies.each do |c|\n      response.cookies << c\n    end\n\n    # Write the response content last on purpose.\n    # See https://github.com/crystal-lang/crystal/issues/8712\n    self.write response\n\n    # Close the response.\n    response.close\n\n    self\n  end\n\n  # Sets the `::HTTP::Status` of this response.\n  def status=(code : ::HTTP::Status | Int32) : self\n    @status = ::HTTP::Status.new code\n\n    self\n  end\n\n  # :nodoc:\n  #\n  # Do any preparation to ensure the response is RFC compliant.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  def prepare(request : AHTTP::Request) : self\n    # Set the content length if not already manually set\n    @headers[\"content-length\"] = @content.bytesize unless @headers.has_key? \"content-length\"\n\n    if @status.informational? || @status.no_content? || @status.not_modified?\n      self.content = nil\n      @headers.delete \"content-type\"\n      @headers.delete \"content-length\"\n    else\n      # Set `content-type` based on the request's format.\n      unless @headers.has_key? \"content-type\"\n        if (format = request.request_format nil) && (mime_type = request.mime_type format)\n          @headers[\"content-type\"] = mime_type\n        end\n      end\n\n      # Add charset to `text/` based content types.\n      charset = self.charset\n\n      if (content_type = @headers[\"content-type\"]?) && content_type.starts_with?(\"text/\") && !content_type.includes?(\"charset\")\n        @headers[\"content-type\"] = \"#{content_type}; charset=#{charset}\"\n      end\n\n      @headers.delete \"content-length\" if @headers.has_key? \"transfer-encoding\"\n\n      if \"HEAD\" == request.method\n        # See https://tools.ietf.org/html/rfc2616#section-14.13.\n        length = @headers[\"content-length\"]?\n        self.content = nil\n        @headers[\"content-length\"] = length if length\n      end\n    end\n\n    if \"HTTP/1.0\" == request.version && @headers.has_cache_control_directive?(\"no-cache\")\n      @headers[\"pragma\"] = \"no-cache\"\n      @headers[\"expires\"] = \"-1\"\n    end\n\n    self\n  end\n\n  # Marks `self` as \"public\".\n  #\n  # Adds the `public` `cache-control` directive and removes the `private` directive.\n  def set_public : self\n    @headers.add_cache_control_directive \"public\"\n    @headers.remove_cache_control_directive \"private\"\n\n    self\n  end\n\n  # Returns the value of the `etag` header if set, otherwise `nil`.\n  def etag : String?\n    @headers[\"etag\"]?\n  end\n\n  # Updates the `etag` header to the provided, optionally *weak*, *etag*.\n  # Removes the header if *etag* is `nil`.\n  def set_etag(etag : String? = nil, weak : Bool = false) : self\n    if etag.nil?\n      @headers.delete \"etag\"\n      return self\n    end\n\n    unless etag.includes? '\"'\n      etag = %(\"#{etag}\")\n    end\n\n    @headers[\"etag\"] = \"#{weak ? \"W/\" : \"\"}#{etag}\"\n\n    self\n  end\n\n  # Returns a `Time`representing the `last-modified` header if set, otherwise `nil`.\n  def last_modified : Time?\n    if header = @headers[\"last-modified\"]?\n      ::HTTP.parse_time header\n    end\n  end\n\n  # Updates the `last-modified` header to the provided *time*.\n  # Removes the header if *time* is `nil`.\n  def last_modified=(time : Time? = nil) : self\n    if time.nil?\n      @headers.delete \"last-modified\"\n      return self\n    end\n\n    @headers[\"last-modified\"] = ::HTTP.format_time time\n\n    self\n  end\n\n  # Returns `true` if this response is a redirect, optionally to the provided *location*.\n  # Otherwise, returns `false`.\n  def redirect?(location : String? = nil) : Bool\n    case @status\n    when .created?, .moved_permanently?, .found?, .see_other?, .temporary_redirect?, .permanent_redirect?\n      # valid redirections statuses\n    else return false\n    end\n\n    location ? location == @headers[\"location\"] : location.nil?\n  end\n\n  # :nodoc:\n  def write(output : IO) : Nil\n    @writer.write(output, &.print(@content))\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/response_headers.cr",
    "content": "# Wraps an [::HTTP::Headers](https://crystal-lang.org/api/HTTP/Headers.html) instance to provide additional functionality.\n#\n# Forwards all additional methods to the wrapped `::HTTP::Headers` instance.\nclass Athena::HTTP::Response::Headers\n  # Returns an [::HTTP::Cookies](https://crystal-lang.org/api/HTTP/Cookies.html) instance that stores cookies related to `self`.\n  getter cookies : ::HTTP::Cookies { ::HTTP::Cookies.new }\n\n  # A Hash representing the current [cache-control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header directives.\n  @cache_control : Hash(String, String | Bool) = Hash(String, String | Bool).new\n  @computed_cache_control : Hash(String, String | Bool) = Hash(String, String | Bool).new\n\n  @headers = ::HTTP::Headers.new\n\n  # :nodoc:\n  forward_missing_to @headers\n\n  # Utility constructor to allow calling `.new` with a union of `self` and `::HTTP::Headers`.\n  #\n  # Returns the provided *headers* object.\n  def self.new(headers : self) : self\n    headers\n  end\n\n  # Creates a new `self`, including the data from the provided *headers*.\n  def initialize(headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    # Defer setting the cache-control header until other headers are set.\n    # This is due to some other header values determining what the resulting\n    # cache-control header value should be. E.g. expires.\n    cache_control_header = headers.delete \"cache-control\"\n\n    headers.each do |k, v|\n      self.[k] = v\n    end\n\n    if cache_control_header\n      self.[\"cache-control\"] = cache_control_header\n    elsif !@headers.has_key? \"cache-control\"\n      self.[\"cache-control\"] = \"\"\n    end\n\n    # See https://tools.ietf.org/html/rfc2616#section-14.18.\n    unless @headers.has_key? \"date\"\n      self.init_date\n    end\n  end\n\n  # Adds the provided *cookie* to the `#cookies` container.\n  def <<(cookie : ::HTTP::Cookie) : Nil\n    self.cookies << cookie\n  end\n\n  # Sets a cookie with the provided *key* and *value*.\n  #\n  # NOTE: The *key* and cookie name must match.\n  def []=(key : String, value : ::HTTP::Cookie) : Nil\n    self.cookies[key] = value\n  end\n\n  # Sets a header with the provided *key* to the provided *value*.\n  #\n  # NOTE: This method will override the *value* of the provided *key*.\n  def []=(key : String, value : Array(String)) : Nil\n    if \"set-cookie\" == key.downcase\n      value.each do |v|\n        if cookie = ::HTTP::Cookie::Parser.parse_set_cookie v\n          self.cookies << cookie\n        else\n          raise ArgumentError.new \"Invalid cookie header: #{v}.\"\n        end\n      end\n\n      return\n    end\n\n    @headers[key] = value\n  end\n\n  # :ditto:\n  def []=(key : String, value : String) : Nil\n    if \"set-cookie\" == key.downcase\n      if cookie = ::HTTP::Cookie::Parser.parse_set_cookie value\n        self.cookies << cookie\n      else\n        raise ArgumentError.new \"Invalid cookie header: #{value}.\"\n      end\n\n      return\n    end\n\n    @headers[key] = value\n\n    if \"cache-control\" == key.downcase\n      @cache_control = AHTTP::HeaderUtils.parse value\n    end\n\n    if key.downcase.in?(\"cache-control\", \"etag\", \"last-modified\", \"expires\") && (computed = self.compute_cache_control_value.presence)\n      @headers[\"cache-control\"] = computed\n      @computed_cache_control = AHTTP::HeaderUtils.parse computed\n    end\n  end\n\n  # :ditto:\n  def []=(key : String, value : _) : Nil\n    self.[key] = value.to_s\n  end\n\n  # Returns `true` if `self` is equal to the provided `::HTTP::Headers` instance.\n  # Otherwise returns `false`.\n  def ==(other : ::HTTP::Headers) : Bool\n    @headers == other\n  end\n\n  # Adds the provided *value* to the the provided *key*.\n  #\n  # NOTE: This method will concatenate the *value* to the provided *key*.\n  def add(key : String, value : String) : Nil\n    @headers.add key, value\n\n    if \"cache-control\" == key.downcase\n      @cache_control = AHTTP::HeaderUtils.parse @headers[\"cache-control\"]\n      @headers[\"cache-control\"] = self.cache_control_header\n    end\n  end\n\n  # Adds the provided *directive*; updating the `cache-control` header.\n  def add_cache_control_directive(directive : String, value : String | Bool = true)\n    @cache_control[directive] = value == true ? value : value.to_s\n    @headers[\"cache-control\"] = self.cache_control_header\n  end\n\n  # Returns a `Time` instance by parsing the datetime string from the header with the provided *key*.\n  #\n  # Returns the provided *default* if no value with the provided *key* exists, or if parsing its value fails.\n  #\n  # ```\n  # time = HTTP.format_time Time.utc 2021, 4, 7, 12, 0, 0\n  # headers = AHTTP::Response::Headers{\"date\" => time}\n  #\n  # headers.date                 # => 2021-04-07 12:00:00.0 UTC\n  # headers.date \"foo\"           # => nil\n  # headers.date \"foo\", Time.utc # => 2021-05-02 14:32:35.257505806 UTC\n  # ```\n  def date(key : String = \"date\", default : Time? = nil) : Time?\n    unless time = @headers[key]?\n      return default\n    end\n\n    ::HTTP.parse_time time\n  end\n\n  # Deletes the header with the provided *key*.\n  #\n  # Clears the `#cookies` instance if *key* is `set-cookie`.\n  #\n  # Clears the `cache-control` header if *key* is `cache-control`.\n  #\n  # Reinitializes the `date` header if *key* is `date`.\n  def delete(key : String) : Nil\n    if \"set-cookie\" == key.downcase\n      return self.cookies.clear\n    end\n\n    @headers.delete key\n\n    if \"cache-control\" == key.downcase\n      @cache_control.clear\n      @computed_cache_control.clear\n    end\n\n    if \"date\" == key.downcase\n      self.init_date\n    end\n  end\n\n  # Returns the provided [directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives) from the `cache-control` header, or `nil` if it is not set.\n  def get_cache_control_directive(directive : String) : String | Bool | Nil\n    @computed_cache_control[directive]?\n  end\n\n  # Returns `true` if the current `cache-control` header has the provided [directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives).\n  # Otherwise returns `false`.\n  def has_cache_control_directive?(directive : String) : Bool\n    @computed_cache_control.has_key? directive\n  end\n\n  # Removes the provided [directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives) from the `cache-control` header.\n  def remove_cache_control_directive(directive : String) : Nil\n    @cache_control.delete directive\n    @headers[\"cache-control\"] = self.cache_control_header\n  end\n\n  private def compute_cache_control_value : String\n    if @cache_control.empty?\n      if @headers.has_key?(\"last-modified\") || @headers.has_key?(\"expires\")\n        # Allows for heuristic expiration (RFC 7234 Section 4.2.2) in the case of \"last-modified\"\n        return \"private, must-revalidate\"\n      end\n\n      # Be Conservative by default\n      return \"no-cache, private\"\n    end\n\n    header = self.cache_control_header\n    if @cache_control.has_key?(\"public\") || @cache_control.has_key?(\"private\")\n      return header\n    end\n\n    # Public if s-maxage is defined, private otherwise\n    unless @cache_control.has_key? \"s-maxage\"\n      return \"#{header}, private\"\n    end\n\n    header\n  end\n\n  private def cache_control_header : String\n    AHTTP::HeaderUtils.to_string @cache_control, \", \"\n  end\n\n  private def init_date : Nil\n    @headers[\"date\"] = ::HTTP.format_time Time.utc\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/streamed_response.cr",
    "content": "# Represents an `AHTTP::Response` whose content should be streamed to the client as opposed to being written all at once.\n# This can be useful in cases where the response content is too large to fit into memory.\n#\n# The content is stored in a proc that gets called when `self` is being written to the response IO.\n# How the output gets written can be customized via an `AHTTP::Response::Writer`.\nclass Athena::HTTP::StreamedResponse < Athena::HTTP::Response\n  @streamed : Bool = false\n\n  # Creates a new response with optional *status*, and *headers* arguments.\n  #\n  # The block is captured and called when `self` is being written to the response's `IO`.\n  # This can be useful to reduce memory overhead when needing to return large responses.\n  #\n  # ```\n  # require \"athena\"\n  #\n  # class ExampleController < ATH::Controller\n  #   @[ARTA::Get(\"/users\")]\n  #   def users : AHTTP::Response\n  #     AHTTP::StreamedResponse.new headers: HTTP::Headers{\"content-type\" => \"application/json; charset=utf-8\"} do |io|\n  #       User.all.to_json io\n  #     end\n  #   end\n  # end\n  #\n  # ATH.run\n  #\n  # # GET /users # => [{\"id\":1,...},...]\n  # ```\n  def self.new(status : ::HTTP::Status | Int32 = ::HTTP::Status::OK, headers : ::HTTP::Headers | AHTTP::Response::Headers = AHTTP::Response::Headers.new, &block : IO -> Nil)\n    new block, status, headers\n  end\n\n  # Creates a new response with the provided *callback* and optional *status*, and *headers* arguments.\n  #\n  # The proc is called when `self` is being written to the response's `IO`.\n  def initialize(@callback : Proc(IO, Nil), status : ::HTTP::Status | Int32 = ::HTTP::Status::OK, headers : ::HTTP::Headers | AHTTP::Response::Headers = AHTTP::Response::Headers.new)\n    # Manually add `transfer-encoding: chunked` so `ART::Response#prepare` knows how to properly handle this type of response.\n    super nil, status, headers.merge!({\"transfer-encoding\" => \"chunked\"})\n  end\n\n  # Updates the callback of `self`.\n  def content=(@callback : Proc(IO, Nil))\n  end\n\n  # :nodoc:\n  def content=(content : String?) : Nil\n    raise AHTTP::Exception::Logic.new \"The content cannot be set on a StreamedResponse instance.\" unless content.nil?\n\n    @streamed = true\n  end\n\n  # :nodoc:\n  def write(output : IO) : Nil\n    return if @streamed\n\n    @streamed = true\n\n    @writer.write(output) do |writer_io|\n      @callback.call writer_io\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http/src/uploaded_file.cr",
    "content": "require \"file_utils\"\nrequire \"./file\"\n\n# Represents a file uploaded to the server.\n# Exposes related information from the client request.\n# See the [Getting Started](/getting_started/routing/#file-uploads) docs for more information.\nstruct Athena::HTTP::UploadedFile < Athena::HTTP::AbstractFile\n  # Represents the status of an uploaded file.\n  # A successful upload would have a status of `OK`,\n  # otherwise the enum member denotes the reason why the upload failed.\n  #\n  # TODO: Maybe add more status members?\n  enum Status\n    # Represents a successful upload.\n    OK\n\n    # Represents a failed upload due to the file being larger than the configured [max allowed size](/Framework/Bundle/Schema/FileUploads/#Athena::Framework::Bundle::Schema::FileUploads#max_file_size).\n    SIZE_LIMIT_EXCEEDED\n  end\n\n  protected class_getter max_file_size : Int64 { 0_i64 }\n\n  # :nodoc:\n  #\n  # Is expected to be set internally based on framework configuration value.\n  def self.max_file_size=(@@max_file_size : Int64) : Nil; end\n\n  # Returns the status of this uploaded file.\n  getter status : Athena::HTTP::UploadedFile::Status\n\n  @original_name : String\n  @original_path : String\n\n  protected def initialize(\n    path : String | Path,\n    original_name : String,\n    mime_type : String? = nil,\n    @status : Athena::HTTP::UploadedFile::Status = :ok,\n    @test : Bool = false,\n  )\n    super path, @status.ok?\n\n    @original_name = clean_name original_name\n    @original_path = original_name.gsub \"\\\\\", \"/\"\n    @mime_type = mime_type || \"application/octet-stream\"\n  end\n\n  # Returns the original name of the file as determined by the client.\n  # It should not be considered a safe value to use for a file on your server.\n  def client_original_name : String\n    @original_name\n  end\n\n  # Returns the original extension of the file as determined by the client.\n  def client_original_extension : String\n    ::File.extname(@original_name).lchop '.'\n  end\n\n  # Returns the original full file path as determined by the client.\n  # It should not be considered a safe value to use for a file name/path on your server.\n  #\n  # If the file was uploaded with the `webkitdirectory` directive, this will contain the path of the file relative to the uploaded root directory.\n  # Otherwise will be identical to `#client_original_name`.\n  def client_original_path : String\n    @original_path\n  end\n\n  # Returns the file's MIME type as determined by the client.\n  # It should not be considered as a safe value.\n  #\n  # For a trusted MIME type, use [#mime_type][Athena::HTTP::AbstractFile#mime_type] (which guesses the MIME type based on the file's contents).\n  def client_mime_type : String\n    @mime_type\n  end\n\n  # Returns the extension based on the client MIME type, or `nil` if the MIME type is unknown.\n  # This method uses `#client_mime_type`, and as such should not be trusted.\n  #\n  # For a trusted extension, use `#guess_extension` which guesses the extension based on the guessed MIME type for the file).\n  def guess_client_extension : String?\n    AMIME::Types.default.extensions(self.client_mime_type).first?\n  end\n\n  # Returns `true` if this file was successfully uploaded via HTTP, otherwise returns `true`.\n  def valid? : Bool\n    is_ok = @status.ok?\n\n    @test ? is_ok : is_ok && self.uploaded_file?\n  end\n\n  # :inherit:\n  def move(directory : Path | String, name : String? = nil) : Athena::HTTP::File\n    if self.valid?\n      return super\n    end\n\n    case @status\n    when .size_limit_exceeded? then raise Athena::HTTP::Exception::FileSizeLimitExceeded.new self.error_message, file: @path\n    end\n\n    raise Athena::HTTP::Exception::File.new self.error_message, file: @path\n  end\n\n  # Returns an informational message as to why the upload failed.\n  #\n  # NOTE: Return value is only valid when the uploaded file's `#status` is _NOT_ `Status::OK`.\n  def error_message : String\n    original_name = self.client_original_name\n\n    case @status\n    when .size_limit_exceeded? then \"The file '#{original_name}' exceeds your max_file_size configuration value (limit is #{self.class.max_file_size.humanize_bytes}).\"\n    else\n      \"The file '#{original_name}' was not uploaded due to an unknown error.\"\n    end\n  end\n\n  # This is an anti-pattern but I can't think of a better way to handle it\n  # without making this DTO type depend upon a service.\n  #\n  # Or requiring DI in the validator component.\n  private def uploaded_file?\n    return false if (path = @path).empty?\n\n    {% if @top_level.has_constant? \"ADI\" %}\n      container = ADI.container\n\n      if container.responds_to? :athena_framework_file_parser\n        return container.athena_framework_file_parser.uploaded_file? path\n      end\n    {% end %}\n\n    false\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/http_kernel/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/http_kernel/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.1.0] - 2026-04-19\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/http-kernel/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/http_kernel/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/http_kernel/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2026 George Dietrich\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": "src/components/http_kernel/README.md",
    "content": "# HTTPKernel\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/workflows/CI/badge.svg)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/http-kernel.svg)](https://github.com/athena-framework/http-kernel/releases)\n\nProvides a structured process for converting a Request into a Response.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/HTTPKernel).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/http_kernel/docs/README.md",
    "content": "The `Athena::HTTPKernel` component provides a structured process for converting an [AHTTP::Request](/HTTP/Request) into an [AHTTP::Response](/HTTP/Response) by dispatching events throughout the request lifecycle.\nIt serves as the foundation for the [Athena Framework](/getting_started), but can also be used standalone to build custom HTTP-based applications.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-http_kernel:\n    github: athena-framework/http-kernel\n    version: ~> 0.1.0\n```\n\n## Usage\n\nThe core of this component is [AHK::HTTPKernel](/HTTPKernel/HTTPKernel/), which orchestrates the request handling process by dispatching a series of events.\n\n### Request Lifecycle\n\nWhen a request is handled, the following events are dispatched in order:\n\n1. **[AHK::Events::Request](/HTTPKernel/Events/Request/)** - Dispatched before anything else. Listeners can return a response early or modify the request.\n2. **[AHK::Events::Action](/HTTPKernel/Events/Action/)** - Dispatched after the action is determined but before it executes. Useful for accessing action metadata.\n3. **[AHK::Events::View](/HTTPKernel/Events/View/)** - Dispatched if the action returns something other than a response. Listeners convert the return value into a response.\n4. **[AHK::Events::Response](/HTTPKernel/Events/Response/)** - Dispatched after a response is created. Listeners can modify the response before it's sent.\n5. **[AHK::Events::Terminate](/HTTPKernel/Events/Terminate/)** - Dispatched after the response is sent. Useful for \"heavy\" post-response processing.\n\nIf an exception is raised at any point, [AHK::Events::Exception](/HTTPKernel/Events/Exception/) is dispatched to allow converting the exception into a response.\n\n### Basic Example\n\n```crystal\nrequire \"athena-http_kernel\"\n\n# Create the required dependencies\nevent_dispatcher = AED::EventDispatcher.new\nrequest_store = AHTTP::RequestStore.new\n\n# Create a simple action resolver that always returns the same action.\n# In practice, this would come from some sort of \"controller\".\nclass SimpleActionResolver\n  include AHK::ActionResolverInterface\n\n  def resolve(request : AHTTP::Request) : AHK::ActionBase?\n    AHK::Action.new(\n      action: Proc(typeof(Tuple.new), String).new { \"Hello, World!\" },\n      parameters: Tuple.new,\n      _return_type: String\n    )\n  end\nend\n\n# Create an argument resolver (no arguments needed for this simple example since it doesn't accept arguments)\nargument_resolver = AHK::Controller::ArgumentResolver.new([] of AHK::Controller::ValueResolvers::Interface)\n\n# Register a listener to convert the string return value into a response\nevent_dispatcher.listener AHK::Events::View do |event|\n  event.response = AHTTP::Response.new(\n    status: :ok,\n    content: event.action_result.to_s\n  )\nend\n\n# Register the error listener\nerror_renderer = AHK::ErrorRenderer.new(debug: true)\nerror_listener = AHK::Listeners::Error.new(error_renderer)\nevent_dispatcher.listener error_listener\n\n# Create the kernel\nkernel = AHK::HTTPKernel.new(\n  event_dispatcher,\n  request_store,\n  argument_resolver,\n  SimpleActionResolver.new\n)\n\n# Handle a request\nrequest = AHTTP::Request.new(\"GET\", \"/\")\nresponse = kernel.handle(request)\n\nresponse.status  # => HTTP::Status::OK\nresponse.content # => \"Hello, World!\"\n\n# Don't forget to terminate\nkernel.terminate(request, response)\n```\n\n### HTTP Exceptions\n\nThe component provides a hierarchy of HTTP exceptions under [AHK::Exception](/HTTPKernel/Exception/).\nThese exceptions automatically set the appropriate HTTP status code and can include custom headers.\n\n```crystal\n# Raise a 404 Not Found\nraise AHK::Exception::NotFound.new \"Resource not found\"\n\n# Raise a 400 Bad Request\nraise AHK::Exception::BadRequest.new \"Invalid input\"\n\n# Raise a 401 Unauthorized with WWW-Authenticate header\nraise AHK::Exception::Unauthorized.new \"Authentication required\", \"Bearer\"\n\n# Raise a 503 Service Unavailable with Retry-After header\nraise AHK::Exception::ServiceUnavailable.new \"Try again later\", retry_after: 300\n```\n\nNon-HTTP exceptions are treated as `500 Internal Server Error` by default.\n\n### Error Handling\n\nThe [AHK::Listeners::Error](/HTTPKernel/Listeners/Error/) listener handles exceptions by converting them into responses via an [AHK::ErrorRendererInterface](/HTTPKernel/ErrorRendererInterface/).\nThe default [AHK::ErrorRenderer](/HTTPKernel/ErrorRenderer/) produces JSON responses:\n\n```json\n{\n  \"code\": 404,\n  \"message\": \"Resource not found\"\n}\n```\n\nCustom error rendering can be implemented by creating a type that includes `AHK::ErrorRendererInterface`:\n\n```crystal\nclass HTMLErrorRenderer\n  include AHK::ErrorRendererInterface\n\n  def render(exception : ::Exception) : AHTTP::Response\n    status = exception.is_a?(AHK::Exception::HTTPException) ? exception.status : HTTP::Status::INTERNAL_SERVER_ERROR\n\n    AHTTP::Response.new(\n      status: status,\n      headers: HTTP::Headers{\"content-type\" => \"text/html\"},\n      body: \"<h1>Error #{status.code}</h1><p>#{HTML.escape(exception.message || \"Unknown error\")}</p>\"\n    )\n  end\nend\n```\n\n### Value Resolvers\n\nValue resolvers determine how arguments are passed to controller actions.\nThe component includes several built-in resolvers:\n\n- [AHK::Controller::ValueResolvers::Request](/HTTPKernel/Controller/ValueResolvers/Request/) - Injects the current request if the parameter type is `AHTTP::Request`\n- [AHK::Controller::ValueResolvers::RequestAttribute](/HTTPKernel/Controller/ValueResolvers/RequestAttribute/) - Resolves values from request attributes (e.g., route parameters)\n- [AHK::Controller::ValueResolvers::DefaultValue](/HTTPKernel/Controller/ValueResolvers/DefaultValue/) - Uses the parameter's default value or `nil` if nilable\n\nCustom resolvers can be created by implementing [AHK::Controller::ValueResolvers::Interface](/HTTPKernel/Controller/ValueResolvers/Interface/):\n\n```crystal\nstruct CurrentTimeResolver\n  include AHK::Controller::ValueResolvers::Interface\n\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : Time?\n    return unless parameter.type == Time\n    Time.utc\n  end\nend\n```\n\n## Integration with Athena Framework\n\nWhen used with the [Athena Framework](/getting_started), the HTTPKernel is automatically configured with:\n\n- Dependency injection for all components\n- The routing component for URL matching\n- Additional value resolvers for query parameters, request body deserialization, enums, UUIDs, and time parsing\n- View handling for content negotiation and serialization\n- CORS support\n\nSee the [Getting Started](/getting_started) guide for full framework documentation.\n\n## Learn More\n\n- [Middleware Architecture](/getting_started/middleware) - Detailed explanation of the event-driven request lifecycle\n- [Error Handling](/getting_started/error_handling) - Working with HTTP exceptions\n- [AHK::HTTPKernel](/HTTPKernel/HTTPKernel/) - API documentation for the kernel\n- [AHK::Events](/HTTPKernel/Events/) - All available lifecycle events\n- [AHK::Exception](/HTTPKernel/Exception/) - Available HTTP exception types\n"
  },
  {
    "path": "src/components/http_kernel/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: HTTPKernel\nsite_url: https://athenaframework.org/HTTPKernel/\nrepo_url: https://github.com/athena-framework/http-kernel\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-contracts/src/athena-contracts.cr\n            - ./lib/athena-event_dispatcher/src/athena-event_dispatcher.cr\n            - ./lib/athena-http/src/athena-http.cr\n            - ./lib/athena-http_kernel/src/athena-http_kernel.cr\n          source_locations:\n            lib/athena-http_kernel: https://github.com/athena-framework/http-kernel/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/http_kernel/shard.yml",
    "content": "name: athena-http_kernel\n\nversion: 0.1.0\n\ncrystal: ~> 1.4\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/http-kernel\n\ndocumentation: https://athenaframework.org/HTTPKernel\n\ndescription: |\n  Provides a structured process for converting a Request into a Response.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\ndependencies:\n  athena-http:\n    github: athena-framework/http\n    version: ~> 0.1.0\n  athena-event_dispatcher:\n    github: athena-framework/event-dispatcher\n    version: ~> 0.4.0\n"
  },
  {
    "path": "src/components/http_kernel/spec/controller/argument_resolver_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate struct TrueResolver\n  include AHK::Controller::ValueResolvers::Interface\n\n  # :inherit:\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata)\n    17\n  end\nend\n\ndescribe AHK::Controller::ArgumentResolver do\n  describe \"#get_arguments\" do\n    describe \"when a value was able to be resolved\" do\n      it \"should return an array of values\" do\n        route = new_action arguments: {new_parameter}\n\n        AHK::Controller::ArgumentResolver.new([TrueResolver.new] of AHK::Controller::ValueResolvers::Interface).get_arguments(new_request, route).should eq [17]\n      end\n    end\n\n    describe \"when a value was not able to be resolved\" do\n      it \"should raise a runtime error\" do\n        route = new_action arguments: {new_parameter}\n\n        expect_raises(RuntimeError, \"AHK::Action requires that you provide a value for the 'id' parameter. Either the parameter is nilable and no nil value has been provided, or no default value has been provided.\") do\n          AHK::Controller::ArgumentResolver.new([] of AHK::Controller::ValueResolvers::Interface).get_arguments(new_request, route)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/controller/parameter_metadata_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AHK::Controller::ParameterMetadata do\n  describe \"#nilable?\" do\n    it \"type is not nilable\" do\n      AHK::Controller::ParameterMetadata(Int32).new(\"foo\").nilable?.should be_false\n      AHK::Controller::ParameterMetadata(String | Bool).new(\"foo\").nilable?.should be_false\n    end\n\n    it \"type is nilable\" do\n      AHK::Controller::ParameterMetadata(Nil).new(\"foo\").nilable?.should be_true\n      AHK::Controller::ParameterMetadata(Int32?).new(\"foo\").nilable?.should be_true\n      AHK::Controller::ParameterMetadata(String | Bool | Nil).new(\"foo\").nilable?.should be_true\n    end\n  end\n\n  describe \"#has_default?\" do\n    it true do\n      AHK::Controller::ParameterMetadata(String).new(\"foo\", true, \"bar\").has_default?.should be_true\n    end\n\n    it false do\n      AHK::Controller::ParameterMetadata(Int32).new(\"foo\", false, nil).has_default?.should be_false\n    end\n  end\n\n  describe \"#default_value\" do\n    it \"with a default\" do\n      AHK::Controller::ParameterMetadata(String).new(\"foo\", true, \"bar\").default_value.should eq \"bar\"\n    end\n\n    it \"without a default\" do\n      expect_raises Exception, \"Argument 'foo' does not have a default value.\" do\n        AHK::Controller::ParameterMetadata(String).new(\"foo\", false, nil).default_value\n      end\n    end\n  end\n\n  describe \"#default_value?\" do\n    it \"with a default\" do\n      AHK::Controller::ParameterMetadata(String).new(\"foo\", true, \"bar\").default_value?.should eq \"bar\"\n    end\n\n    it \"without a default\" do\n      AHK::Controller::ParameterMetadata(String).new(\"foo\", false, nil).default_value?.should be_nil\n    end\n  end\n\n  describe \"#instance_of?\" do\n    it \"with a scalar type\" do\n      AHK::Controller::ParameterMetadata(Int32).new(\"foo\").instance_of?(Int32).should be_true\n      AHK::Controller::ParameterMetadata(Int32).new(\"foo\").instance_of?(Number).should be_true\n      AHK::Controller::ParameterMetadata(Int32).new(\"foo\").instance_of?(String).should be_false\n    end\n\n    it \"with a union\" do\n      AHK::Controller::ParameterMetadata(String | Bool).new(\"foo\").instance_of?(String).should be_true\n      AHK::Controller::ParameterMetadata(Array(Bool) | Array(String)).new(\"foo\").instance_of?(Array(String)).should be_true\n    end\n\n    it \"nilable\" do\n      AHK::Controller::ParameterMetadata(String | Bool | Nil).new(\"foo\").instance_of?(Bool).should be_true\n      AHK::Controller::ParameterMetadata(String | Bool | Nil).new(\"foo\").instance_of?(Int32).should be_false\n      AHK::Controller::ParameterMetadata(Array(Bool) | Array(String) | Nil).new(\"foo\").instance_of?(Array(String)).should be_true\n      AHK::Controller::ParameterMetadata(Array(Bool) | Array(String) | Nil).new(\"foo\").instance_of?(Array(Float64)).should be_false\n    end\n  end\n\n  describe \"#first_type_of\" do\n    it \"with a single type var\" do\n      AHK::Controller::ParameterMetadata(Int32).new(\"foo\").first_type_of(Int32).should eq Int32\n      AHK::Controller::ParameterMetadata(Array(Int32)).new(\"foo\").first_type_of(Array).should eq Array(Int32)\n    end\n\n    it \"with a union\" do\n      AHK::Controller::ParameterMetadata(String | Int32 | Bool).new(\"foo\").first_type_of(Int32).should eq Int32\n      AHK::Controller::ParameterMetadata(Array(Int32) | Array(String)).new(\"foo\").first_type_of(Array).should eq Array(Int32)\n    end\n\n    it \"with a union of multiple valid type vars\" do\n      # Is Float64 because the union gets alphabetized\n      AHK::Controller::ParameterMetadata(String | Int8 | Float64 | Int64).new(\"foo\").first_type_of(Number).should eq Float64\n    end\n\n    it \"with no matching type var\" do\n      AHK::Controller::ParameterMetadata(String | Int32 | Bool).new(\"foo\").first_type_of(Array).should be_nil\n      AHK::Controller::ParameterMetadata(String | Int32 | Bool).new(\"foo\").first_type_of(Float64).should be_nil\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/controller/value_resolvers/default_value_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe AHK::Controller::ValueResolvers::DefaultValue do\n  describe \"#resolve\" do\n    it \"does not have a default nor is nilable\" do\n      parameter = AHK::Controller::ParameterMetadata(String).new \"foo\", false, nil\n\n      AHK::Controller::ValueResolvers::DefaultValue.new.resolve(new_request, parameter).should be_nil\n    end\n\n    it \"does not have a default but is nilable\" do\n      parameter = AHK::Controller::ParameterMetadata(String?).new \"foo\", false, nil\n\n      AHK::Controller::ValueResolvers::DefaultValue.new.resolve(new_request, parameter).should be_nil\n    end\n\n    it \"has a nil default value and is nilable\" do\n      parameter = AHK::Controller::ParameterMetadata(String?).new \"foo\", true, nil\n\n      AHK::Controller::ValueResolvers::DefaultValue.new.resolve(new_request, parameter).should be_nil\n    end\n\n    it \"with a default value\" do\n      parameter = AHK::Controller::ParameterMetadata(String).new \"foo\", true, \"bar\"\n\n      AHK::Controller::ValueResolvers::DefaultValue.new.resolve(new_request, parameter).should eq \"bar\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/controller/value_resolvers/request_attribute_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe AHK::Controller::ValueResolvers::RequestAttribute do\n  describe \"#resolve\" do\n    it \"that does not exist in the request attributes\" do\n      AHK::Controller::ValueResolvers::RequestAttribute.new.resolve(new_request, new_parameter).should be_nil\n    end\n\n    it \"that exists in the request attributes\" do\n      request = new_request\n      request.attributes.set \"id\", 1\n\n      AHK::Controller::ValueResolvers::RequestAttribute.new.resolve(request, new_parameter).should eq 1\n    end\n\n    describe \"that needs to be converted\" do\n      it String do\n        parameter = AHK::Controller::ParameterMetadata(Int32).new \"id\"\n\n        request = new_request\n        request.attributes.set \"id\", \"1\"\n\n        AHK::Controller::ValueResolvers::RequestAttribute.new.resolve(request, parameter).should eq 1\n      end\n\n      it Bool do\n        parameter = AHK::Controller::ParameterMetadata(Bool).new \"id\"\n\n        request = new_request\n        request.attributes.set \"id\", \"false\"\n\n        AHK::Controller::ValueResolvers::RequestAttribute.new.resolve(request, parameter).should be_false\n      end\n\n      it \"that fails conversion\" do\n        parameter = AHK::Controller::ParameterMetadata(Int32).new \"id\"\n\n        request = new_request\n        request.attributes.set \"id\", \"foo\"\n\n        expect_raises AHK::Exception::BadRequest, \"Parameter 'id' with value 'foo' could not be converted into a valid 'Int32'.\" do\n          AHK::Controller::ValueResolvers::RequestAttribute.new.resolve request, parameter\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/controller/value_resolvers/request_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe AHK::Controller::ValueResolvers::Request do\n  describe \"#resolve\" do\n    it TestController do\n      parameter = AHK::Controller::ParameterMetadata(TestController).new \"foo\"\n\n      AHK::Controller::ValueResolvers::Request.new.resolve(new_request, parameter).should be_nil\n    end\n\n    it \"with a valid value\" do\n      parameter = AHK::Controller::ParameterMetadata(AHTTP::Request).new \"foo\"\n      request = new_request\n\n      AHK::Controller::ValueResolvers::Request.new.resolve(request, parameter).should be request\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/error_renderer_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate class MockException < ::Exception\n  def initialize(@first_line : String)\n    super \"ERR\"\n  end\n\n  def backtrace? : Array(String)\n    [@first_line]\n  end\nend\n\nprivate class MockRequestException < ::Exception\n  include AHTTP::Exception::RequestExceptionInterface\nend\n\ndescribe AHK::ErrorRenderer do\n  it AHK::Exception::HTTPException do\n    exception = AHK::Exception::TooManyRequests.new \"cool your jets\", 42\n\n    renderer = AHK::ErrorRenderer.new false\n\n    response = renderer.render exception\n\n    response.headers[\"retry-after\"].should eq \"42\"\n    response.headers[\"content-type\"].should eq \"application/json; charset=utf-8\"\n    response.status.should eq ::HTTP::Status::TOO_MANY_REQUESTS\n    response.content.should eq %({\"code\":429,\"message\":\"cool your jets\"})\n  end\n\n  it AHTTP::Exception::RequestExceptionInterface do\n    exception = MockRequestException.new \"ERR\"\n\n    renderer = AHK::ErrorRenderer.new false\n\n    response = renderer.render exception\n\n    response.headers[\"content-type\"].should eq \"application/json; charset=utf-8\"\n    response.headers[\"x-debug-exception-message\"]?.should be_nil\n    response.headers[\"x-debug-exception-class\"]?.should be_nil\n    response.headers[\"x-debug-exception-file\"]?.should be_nil\n    response.headers[\"x-debug-exception-code\"]?.should be_nil\n    response.status.should eq ::HTTP::Status::BAD_REQUEST\n    response.content.should eq %({\"code\":400,\"message\":\"Bad Request\"})\n  end\n\n  it ::Exception do\n    exception = Exception.new \"ERR\"\n\n    renderer = AHK::ErrorRenderer.new false\n\n    response = renderer.render exception\n\n    response.headers[\"content-type\"].should eq \"application/json; charset=utf-8\"\n    response.headers[\"x-debug-exception-message\"]?.should be_nil\n    response.headers[\"x-debug-exception-class\"]?.should be_nil\n    response.headers[\"x-debug-exception-file\"]?.should be_nil\n    response.headers[\"x-debug-exception-code\"]?.should be_nil\n    response.status.should eq ::HTTP::Status::INTERNAL_SERVER_ERROR\n    response.content.should eq %({\"code\":500,\"message\":\"Internal Server Error\"})\n  end\n\n  describe \"debug mode\" do\n    it \"line + column\" do\n      path = Path[\"src\", \"components\", \"framework\", \"spec\", \"error_renderer_spec.cr\"]\n      exception = MockException.new \"#{path}:10:20\"\n\n      renderer = AHK::ErrorRenderer.new true\n\n      response = renderer.render exception\n\n      response.headers[\"content-type\"].should eq \"application/json; charset=utf-8\"\n      response.headers[\"x-debug-exception-message\"].should eq \"ERR\"\n      response.headers[\"x-debug-exception-class\"].should eq \"MockException\"\n      response.headers[\"x-debug-exception-file\"].should match /#{URI.encode_path path.to_s}:\\d+:\\d+$/\n      response.headers[\"x-debug-exception-code\"].should eq \"500\"\n      response.status.should eq ::HTTP::Status::INTERNAL_SERVER_ERROR\n      response.content.should eq %({\"code\":500,\"message\":\"Internal Server Error\"})\n    end\n\n    it \"only line\" do\n      path = Path[\"src\", \"components\", \"framework\", \"spec\", \"error_renderer_spec.cr\"]\n      exception = MockException.new \"#{path}:10\"\n\n      renderer = AHK::ErrorRenderer.new true\n\n      response = renderer.render exception\n\n      response.headers[\"content-type\"].should eq \"application/json; charset=utf-8\"\n      response.headers[\"x-debug-exception-message\"].should eq \"ERR\"\n      response.headers[\"x-debug-exception-class\"].should eq \"MockException\"\n      response.headers[\"x-debug-exception-file\"].should match /#{URI.encode_path path.to_s}:\\d+$/\n      response.headers[\"x-debug-exception-code\"].should eq \"500\"\n      response.status.should eq ::HTTP::Status::INTERNAL_SERVER_ERROR\n      response.content.should eq %({\"code\":500,\"message\":\"Internal Server Error\"})\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/exception/bad_gateway_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AHK::Exception::BadGateway do\n  describe \"#initialize\" do\n    it \"sets the message, and status\" do\n      exception = AHK::Exception::BadGateway.new \"MESSAGE\"\n      exception.headers.should be_empty\n      exception.status.should eq ::HTTP::Status::BAD_GATEWAY\n      exception.message.should eq \"MESSAGE\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/exception/http_exception_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AHK::Exception::HTTPException do\n  describe \"#initialize\" do\n    it \"sets the message, and status\" do\n      exception = AHK::Exception::HTTPException.new 200, \"MESSAGE\"\n      exception.headers.should be_empty\n      exception.status.should eq ::HTTP::Status::OK\n      exception.message.should eq \"MESSAGE\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/exception/service_unavailable_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AHK::Exception::ServiceUnavailable do\n  describe \"#initialize\" do\n    it \"sets the message, and status\" do\n      exception = AHK::Exception::ServiceUnavailable.new \"MESSAGE\"\n      exception.headers.should be_empty\n      exception.status.should eq ::HTTP::Status::SERVICE_UNAVAILABLE\n      exception.message.should eq \"MESSAGE\"\n    end\n\n    it \"sets the retry-after if given as a string\" do\n      exception = AHK::Exception::ServiceUnavailable.new \"MESSAGE\", \"17\"\n      exception.headers.should eq ::HTTP::Headers{\"retry-after\" => \"17\"}\n    end\n\n    it \"sets the retry-after if given\" do\n      exception = AHK::Exception::ServiceUnavailable.new \"MESSAGE\", 123\n      exception.headers.should eq ::HTTP::Headers{\"retry-after\" => \"123\"}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/exception/too_many_requests_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AHK::Exception::TooManyRequests do\n  describe \"#initialize\" do\n    it \"sets the message, and status\" do\n      exception = AHK::Exception::TooManyRequests.new \"MESSAGE\"\n      exception.headers.should be_empty\n      exception.status.should eq ::HTTP::Status::TOO_MANY_REQUESTS\n      exception.message.should eq \"MESSAGE\"\n    end\n\n    it \"sets the retry-after if given as a string\" do\n      exception = AHK::Exception::TooManyRequests.new \"MESSAGE\", \"17\"\n      exception.headers.should eq ::HTTP::Headers{\"retry-after\" => \"17\"}\n    end\n\n    it \"sets the retry-after if given\" do\n      exception = AHK::Exception::TooManyRequests.new \"MESSAGE\", 123\n      exception.headers.should eq ::HTTP::Headers{\"retry-after\" => \"123\"}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/exception/unauthorized_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AHK::Exception::Unauthorized do\n  describe \"#initialize\" do\n    it \"sets the message, status, and headers\" do\n      exception = AHK::Exception::Unauthorized.new \"MESSAGE\", \"CHALLENGE\"\n      exception.headers.should eq ::HTTP::Headers{\"www-authenticate\" => \"CHALLENGE\"}\n      exception.status.should eq ::HTTP::Status::UNAUTHORIZED\n      exception.message.should eq \"MESSAGE\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/http_kernel_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate struct MockArgumentResolver\n  include Athena::HTTPKernel::Controller::ArgumentResolverInterface\n\n  def initialize(@exception : ::Exception? = nil); end\n\n  def get_arguments(request : AHTTP::Request, action : AHK::ActionBase) : Array\n    if ex = @exception\n      raise ex\n    end\n\n    [] of String\n  end\nend\n\nprivate struct MockActionResolver\n  include Athena::HTTPKernel::ActionResolverInterface\n\n  def initialize(@action : AHK::ActionBase? = nil); end\n\n  def resolve(request : AHTTP::Request) : AHK::ActionBase?\n    @action\n  end\nend\n\ndescribe Athena::HTTPKernel::HTTPKernel do\n  describe \"#handle\" do\n    describe \"request\" do\n      describe AHTTP::Response do\n        it \"should use the returned response\" do\n          dispatcher = AED::Spec::TracableEventDispatcher.new\n          action = create_action(AHTTP::Response) do\n            AHTTP::Response.new \"TEST\"\n          end\n          handler = AHK::HTTPKernel.new dispatcher, AHTTP::RequestStore.new, MockArgumentResolver.new, MockActionResolver.new action\n\n          response = handler.handle new_request(action: action)\n\n          response.status.should eq ::HTTP::Status::OK\n          response.content.should eq \"TEST\"\n\n          dispatcher.emitted_events.should eq [AHK::Events::Request, AHK::Events::Action, AHK::Events::Response]\n        end\n\n        it \"should raise if the action is unable to be resolved\" do\n          dispatcher = AED::Spec::TracableEventDispatcher.new\n          handler = AHK::HTTPKernel.new dispatcher, AHTTP::RequestStore.new, MockArgumentResolver.new, MockActionResolver.new\n\n          expect_raises AHK::Exception::NotFound, \"Unable to find the action for path '/test'.\" do\n            handler.handle new_request\n          end\n        end\n      end\n\n      describe \"view layer\" do\n        it \"should resolve the returned value into a response\" do\n          dispatcher = AED::Spec::TracableEventDispatcher.new\n          dispatcher.listener AHK::Events::View do |event|\n            event.response = AHTTP::Response.new \"TEST\".to_json, 201, ::HTTP::Headers{\"content-type\" => \"application/json\"}\n          end\n\n          action = new_action\n\n          handler = AHK::HTTPKernel.new dispatcher, AHTTP::RequestStore.new, MockArgumentResolver.new, MockActionResolver.new action\n\n          response = handler.handle request = new_request action: action\n\n          request.attributes.get(\"_action\").should eq action\n\n          response.status.should eq ::HTTP::Status::CREATED\n          response.content.should eq %(\"TEST\")\n          response.headers[\"content-type\"].should eq \"application/json\"\n\n          dispatcher.emitted_events.should eq [AHK::Events::Request, AHK::Events::Action, AHK::Events::View, AHK::Events::Response]\n        end\n\n        it \"should raise an exception if the value was not handled\" do\n          dispatcher = AED::Spec::TracableEventDispatcher.new\n          action = create_action(String?) do\n            nil\n          end\n\n          handler = AHK::HTTPKernel.new dispatcher, AHTTP::RequestStore.new, MockArgumentResolver.new, MockActionResolver.new action\n\n          expect_raises Exception, \"'TestController#test' must return an `AHTTP::Response` but it returned ''.\" do\n            handler.handle new_request\n          end\n\n          dispatcher.emitted_events.should eq [AHK::Events::Request, AHK::Events::Action, AHK::Events::View, AHK::Events::Exception]\n        end\n      end\n\n      describe \"that was handled via a request listener\" do\n        it \"should emit the proper events and set the proper response\" do\n          dispatcher = AED::Spec::TracableEventDispatcher.new\n          dispatcher.listener AHK::Events::Request do |event|\n            event.response = AHTTP::Response.new \"\", ::HTTP::Status::IM_A_TEAPOT, ::HTTP::Headers{\"FOO\" => \"BAR\"}\n          end\n\n          handler = AHK::HTTPKernel.new dispatcher, AHTTP::RequestStore.new, MockArgumentResolver.new, MockActionResolver.new\n\n          response = handler.handle new_request\n\n          response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n          response.content.should be_empty\n          response.headers[\"FOO\"].should eq \"BAR\"\n\n          dispatcher.emitted_events.should eq [AHK::Events::Request, AHK::Events::Response]\n        end\n      end\n    end\n\n    describe \"exception\" do\n      describe \"that is handled\" do\n        it \"should emit the proper events and set correct response\" do\n          dispatcher = AED::Spec::TracableEventDispatcher.new\n          dispatcher.listener AHK::Events::Exception do |event|\n            event.response = AHTTP::Response.new \"HANDLED\", ::HTTP::Status::BAD_REQUEST\n          end\n\n          handler = AHK::HTTPKernel.new dispatcher, AHTTP::RequestStore.new, MockArgumentResolver.new(AHK::Exception::BadRequest.new(\"TEST_EX\")), MockActionResolver.new new_action\n\n          response = handler.handle new_request\n\n          response.status.should eq ::HTTP::Status::BAD_REQUEST\n          response.content.should eq \"HANDLED\"\n\n          dispatcher.emitted_events.should eq [AHK::Events::Request, AHK::Events::Action, AHK::Events::Exception, AHK::Events::Response]\n        end\n      end\n\n      describe \"that is not_handled\" do\n        it \"should emit the proper events and set correct response\" do\n          dispatcher = AED::Spec::TracableEventDispatcher.new\n\n          handler = AHK::HTTPKernel.new dispatcher, AHTTP::RequestStore.new, MockArgumentResolver.new(AHK::Exception::BadRequest.new(\"TEST_EX\")), MockActionResolver.new new_action\n\n          expect_raises AHK::Exception::BadRequest, \"TEST_EX\" do\n            handler.handle new_request\n          end\n\n          dispatcher.emitted_events.should eq [AHK::Events::Request, AHK::Events::Action, AHK::Events::Exception]\n        end\n      end\n\n      describe \"when another exception is raised in the response listener\" do\n        it \"should return the previous response\" do\n          dispatcher = AED::Spec::TracableEventDispatcher.new\n          dispatcher.listener AHK::Events::Response do\n            raise AHK::Exception::NotFound.new \"NOT_FOUND\"\n          end\n\n          dispatcher.listener AHK::Events::Exception do |event|\n            event.response = AHTTP::Response.new \"HANDLED\", ::HTTP::Status::NOT_FOUND\n          end\n\n          action = create_action(AHTTP::Response) do\n            AHTTP::Response.new \"TEST\"\n          end\n\n          handler = AHK::HTTPKernel.new dispatcher, AHTTP::RequestStore.new, MockArgumentResolver.new, MockActionResolver.new action\n\n          response = handler.handle new_request action: action\n\n          response.status.should eq ::HTTP::Status::NOT_FOUND\n          response.content.should eq %(HANDLED)\n\n          dispatcher.emitted_events.should eq [AHK::Events::Request, AHK::Events::Action, AHK::Events::Response, AHK::Events::Exception, AHK::Events::Response]\n        end\n      end\n    end\n  end\n\n  describe \"#terminate\" do\n    it \"emits the terminate event\" do\n      dispatcher = AED::Spec::TracableEventDispatcher.new\n      handler = AHK::HTTPKernel.new dispatcher, AHTTP::RequestStore.new, MockArgumentResolver.new, MockActionResolver.new\n\n      handler.terminate new_request, AHTTP::Response.new\n\n      dispatcher.emitted_events.should eq [AHK::Events::Terminate]\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/listeners/error_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate struct MockErrorRenderer\n  include AHK::ErrorRendererInterface\n\n  def render(exception : ::Exception) : AHTTP::Response\n    AHTTP::Response.new \"ERR\", 418, ::HTTP::Headers{\"FOO\" => \"BAR\"}\n  end\nend\n\nprivate class MockException < AHK::Exception::BadRequest\nend\n\ndescribe AHK::Listeners::Error do\n  it \"converts an exception into a response and logs the exception as warning\" do\n    event = AHK::Events::Exception.new new_request, MockException.new \"Something went wrong\"\n\n    AHK::Listeners::Error.new(MockErrorRenderer.new).on_exception event\n\n    response = event.response.should_not be_nil\n    response.status.should eq ::HTTP::Status::IM_A_TEAPOT\n    response.headers[\"FOO\"].should eq \"BAR\"\n    response.content.should eq \"ERR\"\n  end\n\n  describe \"logging\" do\n    it \"logs non HTTPExceptions as error\" do\n      event = AHK::Events::Exception.new new_request, Exception.new \"err\"\n\n      Log.capture do |logs|\n        AHK::Listeners::Error.new(MockErrorRenderer.new).on_exception event\n\n        logs.check :error, /Exception:err/\n      end\n    end\n\n    it \"logs server HTTPExceptions as error\" do\n      event = AHK::Events::Exception.new new_request, AHK::Exception::NotImplemented.new \"nope\"\n\n      Log.capture do |logs|\n        AHK::Listeners::Error.new(MockErrorRenderer.new).on_exception event\n\n        logs.check :error, /Athena::HTTPKernel::Exception::NotImplemented:nope/\n      end\n    end\n\n    it \"logs validation errors as notice\" do\n      event = AHK::Events::Exception.new new_request, AHK::Exception::UnprocessableEntity.new \"Validation tests failed\"\n\n      Log.capture do |logs|\n        AHK::Listeners::Error.new(MockErrorRenderer.new).on_exception event\n\n        logs.check :notice, /Athena::HTTPKernel::Exception::UnprocessableEntity:Validation tests failed/\n      end\n    end\n\n    it \"logs HTTPExceptions as warning\" do\n      event = AHK::Events::Exception.new new_request, MockException.new \"Something went wrong\"\n\n      Log.capture do |logs|\n        AHK::Listeners::Error.new(MockErrorRenderer.new).on_exception event\n\n        logs.check :warn, /MockException:Something went wrong/\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"log/spec\"\nrequire \"athena-spec\"\nrequire \"../src/athena-http_kernel\"\nrequire \"athena-event_dispatcher/spec\"\n\nASPEC.run_all\n\nclass TestController\n  def get_test : String\n    \"TEST\"\n  end\nend\n\nmacro create_action(return_type = String, &)\n  AHK::Action.new(\n    Proc(typeof(Tuple.new), {{return_type}}).new { {{yield}} },\n    Tuple.new,\n    {{return_type}},\n  )\nend\n\ndef new_parameter : AHK::Controller::ParameterMetadata\n  AHK::Controller::ParameterMetadata(Int32).new \"id\"\nend\n\ndef new_action(\n  *,\n  arguments : Tuple = Tuple.new,\n) : AHK::ActionBase\n  AHK::Action.new(\n    Proc(typeof(Tuple.new), String).new { test_controller = TestController.new; test_controller.get_test },\n    arguments,\n    String,\n  )\nend\n\ndef new_request(\n  *,\n  path : String = \"/test\",\n  method : String = \"GET\",\n  action : AHK::ActionBase = new_action,\n  body : String | IO | Nil = nil,\n  query : String? = nil,\n  format : String = \"json\",\n  files : Hash(String, Array(AHTTP::UploadedFile)) = {} of String => Array(AHTTP::UploadedFile),\n  headers : ::HTTP::Headers = ::HTTP::Headers.new,\n) : AHTTP::Request\n  request = AHTTP::Request.new method, path, body: body\n  request.files.merge! files\n  request.attributes.set \"_controller\", \"TestController#test\", String\n  request.attributes.set \"_route\", \"test_controller_test\", String\n  request.attributes.set \"_action\", action\n  request.query = query\n  request.headers = ::HTTP::Headers{\n    \"content-type\" => AHTTP::Request::FORMATS[format].first,\n  }.merge! headers\n  request\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/action.cr",
    "content": "# Parent type of a controller action just used for typing.\n#\n# See `AHK::Action`.\nabstract class Athena::HTTPKernel::ActionBase\n  abstract def resolve_arguments(value_resolvers : Array, request : AHTTP::Request) : Array\n\n  abstract def execute(arguments : Array)\n\n  # :inherit:\n  def inspect(io : IO) : Nil\n    io << \"#<AHK::Action>\"\n  end\nend\n\n# Represents a controller action that will handle a request.\n#\n# Includes metadata about the endpoint, such as its action parameters and return type, and the action that should be executed.\nclass Athena::HTTPKernel::Action(ReturnType, ParameterTypeTuple, ParametersType) < Athena::HTTPKernel::ActionBase\n  # Returns a tuple of `AHK::Controller::ParameterMetadata` representing the parameters this action expects.\n  getter parameters : ParametersType\n\n  def initialize(\n    @action : Proc(ParameterTypeTuple, ReturnType),\n    @parameters : ParametersType,\n    # Don't bother making this an ivar since we just need it to set the generic type\n    _return_type : ReturnType.class,\n  ); end\n\n  # Returns the type that this action returns.\n  def return_type : ReturnType.class\n    ReturnType\n  end\n\n  # Executes this action with the provided *arguments* array.\n  def execute(arguments : Array) : ReturnType\n    @action.call {{ParameterTypeTuple.type_vars.empty? ? \"Tuple.new\".id : ParameterTypeTuple}}.from arguments\n  end\n\n  # Resolves the arguments for this action for the given *request*.\n  #\n  # This is defined in here as opposed to `AHK::Controller::ArgumentResolver` so that the free vars are resolved correctly.\n  # See https://forum.crystal-lang.org/t/incorrect-overload-selected-with-freevar-and-generic-inheritance/3625.\n  def resolve_arguments(value_resolvers : Array, request : AHTTP::Request) : Array\n    {% begin %}\n      {% if 0 == ParametersType.size %}\n        Tuple.new.to_a\n      {% else %}\n        {\n          {% for idx in (0...ParametersType.size) %}\n            begin\n              %parameter = @parameters[{{idx}}]\n\n              # Variadic parameters are not supported.\n              # `nil` represents both the value `nil` and that resolver was unable to resolve a value\n              # Each resolver can return at most one value.\n              # First resolver to resolve a non-nil value wins, otherwise `nil` itself is used as the value,\n              # assuming the parameter accepts nil, otherwise an error is raised.\n\n               value = value_resolvers.each do |resolver|\n                resolved_value = resolver.resolve request, %parameter\n                break resolved_value unless resolved_value.nil?\n              end\n\n              if value.nil? && !%parameter.nilable?\n                raise RuntimeError.new \"AHK::Action requires that you provide a value for the '#{%parameter.name}' parameter. Either the parameter is nilable and no nil value has been provided, or no default value has been provided.\"\n              end\n\n              value\n            end,\n          {% end %}\n        }.to_a\n      {% end %}\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/action_resolver.cr",
    "content": "# Default `AHK::ActionResolverInterface` implementation that looks for an `_action` key\n# within [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes).\nclass Athena::HTTPKernel::ActionResolver\n  include Athena::HTTPKernel::ActionResolverInterface\n\n  # :inherit:\n  def resolve(request : AHTTP::Request) : AHK::ActionBase?\n    request.attributes.get? \"_action\", AHK::ActionBase\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/action_resolver_interface.cr",
    "content": "# Resolves the `AHK::ActionBase` for a given request.\n#\n# The route matched via `AHK::Listeners::Routing` (or equivalent) needs to be resolved to the `AHK::ActionBase` instance that actually represents the action (controller) of the request.\nmodule Athena::HTTPKernel::ActionResolverInterface\n  # Resolves the `AHK::ActionBase` instance that should handle the provided *request*.\n  abstract def resolve(request : AHTTP::Request) : AHK::ActionBase?\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/athena-http_kernel.cr",
    "content": "require \"log\"\nrequire \"json\"\nrequire \"semantic_version\"\n\nrequire \"athena-event_dispatcher\"\nrequire \"athena-http\"\n\nrequire \"./action\"\nrequire \"./action_resolver_interface\"\nrequire \"./action_resolver\"\nrequire \"./error_renderer_interface\"\nrequire \"./error_renderer\"\nrequire \"./http_kernel\"\n\nrequire \"./controller/**\"\n\nrequire \"./events/request_aware\"\nrequire \"./events/settable_response\"\nrequire \"./events/*\"\n\nrequire \"./exception/http_exception\"\nrequire \"./exception/*\"\n\nrequire \"./listeners/error\"\n\nmacro finished\n  {% if @top_level.has_constant?(\"ART\") %}\n    require \"./listeners/routing\"\n  {% end %}\nend\n\n# Convenience alias to make referencing `Athena::HTTPKernel` types easier.\nalias AHK = Athena::HTTPKernel\n\nmodule Athena::HTTPKernel\n  VERSION = \"0.1.0\"\n  Log     = ::Log.for \"athena.http_kernel\"\n\n  # This type includes all of the built-in resolvers that the HTTPKernel uses to try and resolve an argument for a particular controller action parameter.\n  #\n  # Custom resolvers may also be defined.\n  # See `AHK::Controller::ValueResolvers::Interface` for more information.\n  module Controller::ValueResolvers; end\n\n  # The `ACTR::EventDispatcher::Event` that are emitted via `Athena::EventDispatcher` to handle a request during its life-cycle.\n  # Custom events can also be defined and dispatched within a controller, listener, or some other service.\n  module Events; end\n\n  # Exception handling in Athena is similar to exception handling in any Crystal program, with the addition of a new unique exception type, `AHK::Exception::HTTPException`.\n  #\n  # When an exception is raised, Athena emits the `AHK::Events::Exception` event to allow an opportunity for it to be handled.\n  # If the exception goes unhandled, i.e. no listener set an `AHTTP::Response` on the event, then the request is finished and the exception is re-raised.\n  # Otherwise, that response is returned, setting the status and merging the headers on the exceptions if it is an `AHK::Exception::HTTPException`.\n  # See `AHK::Listeners::Error` and `AHK::ErrorRendererInterface` for more information on how exceptions are handled by default.\n  #\n  # To provide the best response to the client, non `AHK::Exception::HTTPException` should be rescued and converted into a corresponding `AHK::Exception::HTTPException`.\n  # Custom HTTP errors can also be defined by inheriting from `AHK::Exception::HTTPException` or a child type.\n  # A use case for this could be allowing for additional data/context to be included within the exception that ultimately could be used in a `AHK::Events::Exception` listener.\n  module Exception; end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/controller/argument_resolver.cr",
    "content": "require \"./value_resolvers/interface\"\nrequire \"./argument_resolver_interface\"\n\n# The default implementation of `AHK::Controller::ArgumentResolverInterface`.\nstruct Athena::HTTPKernel::Controller::ArgumentResolver\n  include Athena::HTTPKernel::Controller::ArgumentResolverInterface\n\n  def initialize(@value_resolvers : Array(Athena::HTTPKernel::Controller::ValueResolvers::Interface)); end\n\n  # :inherit:\n  def get_arguments(request : AHTTP::Request, action : AHK::ActionBase) : Array\n    action.resolve_arguments @value_resolvers, request\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/controller/argument_resolver_interface.cr",
    "content": "# Responsible for resolving the arguments that will be passed to a controller action.\n#\n# See the [Getting Started](/getting_started/middleware#argument-resolution) docs for more information.\nmodule Athena::HTTPKernel::Controller::ArgumentResolverInterface\n  # Returns an array of arguments resolved from the provided *request* for the given *action*.\n  abstract def get_arguments(request : AHTTP::Request, action : AHK::ActionBase) : Array\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/controller/parameter_metadata.cr",
    "content": "# Represents a controller action parameter. Stores metadata associated with it, such as its name, type, and default value if any.\nstruct Athena::HTTPKernel::Controller::ParameterMetadata(T)\n  # :inherit:\n  def inspect(io : IO) : Nil\n    io << \"#<AHK::ParameterMetadata(\" << {{ T.stringify }} << \") name=\" << @name.inspect << \">\"\n  end\n\n  # Returns the name of the parameter.\n  getter name : String\n\n  # Returns `true` if this parameter has a default value set, otherwise `false`.\n  getter? has_default : Bool\n\n  # :nodoc:\n  def initialize(\n    @name : String,\n    @has_default : Bool = false,\n    @default_value : T? = nil,\n  ); end\n\n  # If `nil` is a valid value for the parameter.\n  def nilable? : Bool\n    {{T.nilable?}}\n  end\n\n  # Returns the default value for this parameter, raising an exception if it does not have one.\n  def default_value : T\n    raise AHK::Exception::Logic.new \"Argument '#{@name}' does not have a default value.\" unless self.has_default?\n\n    @default_value.not_nil!\n  end\n\n  # Returns the default value for this parameter, or `nil` if it does not have one.\n  def default_value? : T?\n    @default_value\n  end\n\n  # The type of the parameter, i.e. what its type restriction is.\n  def type : T.class\n    T\n  end\n\n  # Returns `true` if this parameter's `#type` includes the provided *klass*.\n  #\n  # ```\n  # AHK::Controller::ParameterMetadata(Int32).new(\"foo\").instance_of?(Int32)       # => true\n  # AHK::Controller::ParameterMetadata(Int32 | Bool).new(\"foo\").instance_of?(Bool) # => true\n  # AHK::Controller::ParameterMetadata(Int32).new(\"foo\").instance_of?(String)      # => false\n  # ```\n  def instance_of?(klass : Type.class) : Bool forall Type\n    {{ T.union? ? T.union_types.any? { |t| t <= Type } : T <= Type }}\n  end\n\n  # Returns the metaclass of the first matching type variable that is an `#instance_of?` the provided *klass*, or `nil` if none match.\n  # If this the `#type` is union, this would be the first viable type.\n  #\n  # ```\n  # AHK::Controller::ParameterMetadata(Int32).new(\"foo\").first_type_of(Int32)                            # => Int32.class\n  # AHK::Controller::ParameterMetadata(String | Int32 | Bool).new(\"foo\").first_type_of(Int32)            # => Int32.class\n  # AHK::Controller::ParameterMetadata(String | Int8 | Float64 | Int64).new(\"foo\").first_type_of(Number) # => Float64.class\n  # AHK::Controller::ParameterMetadata(String | Int32 | Bool).new(\"foo\").first_type_of(Float64)          # => nil\n  # ```\n  def first_type_of(klass : Type.class) forall Type\n    {% if T.union? %}\n      {% for t in T.union_types %}\n        {% if t <= Type %}\n          return {{t}}\n        {% end %}\n      {% end %}\n    {% elsif T <= Type %}\n      {{T}}\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/controller/value_resolvers/default_value.cr",
    "content": "# Resolves the default value of a controller action parameter if no other value was provided;\n# using `nil` if the parameter does not have a default value, but is nilable.\n#\n# ```\n# AHK::Controller::ParameterMetadata(Int32).new(\"id\", has_default: true, default_value: 123)\n# # resolve would return 123\n# ```\nstruct Athena::HTTPKernel::Controller::ValueResolvers::DefaultValue\n  include Athena::HTTPKernel::Controller::ValueResolvers::Interface\n\n  # :inherit:\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata)\n    return if !parameter.has_default? && !parameter.nilable?\n\n    parameter.default_value?\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/controller/value_resolvers/interface.cr",
    "content": "# Value resolvers handle resolving the argument(s) to pass to a controller action based on values stored within the `AHTTP::Request`, or some other source.\n#\n# Custom resolvers can be defined by creating a type that implements this interface.\n# The first resolver to return a value wins and no other resolvers will be executed for that particular parameter.\n# The resolver should return `nil` to denote no value could be resolved, such as if the parameter is of the wrong type, does not have a specific annotation applied, or anything else that can be deduced from either parameter.\n# If no resolver is able to resolve a value for a specific parameter, an error is thrown and processing of the request ceases.\nmodule Athena::HTTPKernel::Controller::ValueResolvers::Interface\n  # Returns a value resolved from the provided *request* and *parameter* if possible, otherwise returns `nil` if no parameter could be resolved.\n  abstract def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata)\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/controller/value_resolvers/request.cr",
    "content": "# Handles resolving a value for action parameters typed as `AHTTP::Request`.\n#\n# ```\n# @[ARTA::Get(\"/\")]\n# def get_request_path(request : AHTTP::Request) : String\n#   request.path\n# end\n# ```\nstruct Athena::HTTPKernel::Controller::ValueResolvers::Request\n  include Athena::HTTPKernel::Controller::ValueResolvers::Interface\n\n  # :inherit:\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata) : AHTTP::Request?\n    return unless parameter.instance_of? ::AHTTP::Request\n\n    request\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/controller/value_resolvers/request_attribute.cr",
    "content": "# Handles resolving a value that is stored in the request's [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes).\n# This includes any path/query parameters, custom values stored via an event listener, or extra `defaults` stored within the routing annotation.\n#\n# ```\n# @[ARTA::Get(\"/{id}\")]\n# def get_id(id : Int32) : Int32\n#   id\n# end\n# ```\nstruct Athena::HTTPKernel::Controller::ValueResolvers::RequestAttribute\n  include Athena::HTTPKernel::Controller::ValueResolvers::Interface\n\n  # :inherit:\n  def resolve(request : AHTTP::Request, parameter : AHK::Controller::ParameterMetadata)\n    return unless request.attributes.has? parameter.name\n\n    value = request.attributes.get parameter.name\n\n    parameter.type.from_parameter value\n  rescue ex : ArgumentError\n    # Catch type cast errors and bubble it up as a BadRequest\n    raise AHK::Exception::BadRequest.new \"Parameter '#{parameter.name}' with value '#{value}' could not be converted into a valid '#{parameter.type}'.\", cause: ex\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/error_renderer.cr",
    "content": "# The default `AHK::ErrorRendererInterface`, JSON serializes the exception.\nstruct Athena::HTTPKernel::ErrorRenderer\n  include Athena::HTTPKernel::ErrorRendererInterface\n\n  def initialize(@debug : Bool); end\n\n  # :inherit:\n  def render(exception : ::Exception) : AHTTP::Response\n    headers = ::HTTP::Headers.new\n    content = exception.to_json\n\n    if exception.is_a? AHK::Exception::HTTPException\n      status = exception.status\n      headers = exception.headers\n    elsif exception.is_a?(AHTTP::Exception::RequestExceptionInterface)\n      status = ::HTTP::Status::BAD_REQUEST\n      content = {code: 400, message: \"Bad Request\"}.to_json\n    else\n      status = ::HTTP::Status::INTERNAL_SERVER_ERROR\n    end\n\n    headers[\"content-type\"] = \"application/json; charset=utf-8\"\n\n    # TODO: Use a better API to get the file/line/column info.\n    if @debug && (backtrace = exception.backtrace?.try(&.first).to_s.presence)\n      if (match = backtrace.match(/(.*):(\\d+):(\\d+)/)) || (match = backtrace.match(/(.*):(\\d+)/))\n        headers[\"x-debug-exception-message\"] = URI.encode_path exception.message.to_s\n        headers[\"x-debug-exception-class\"] = exception.class.to_s\n        headers[\"x-debug-exception-code\"] = status.value.to_s\n\n        file = \"#{URI.encode_path(match[1])}:#{match[2]}\"\n\n        if m3 = match[3]?\n          file = \"#{file}:#{m3}\"\n        end\n\n        headers[\"x-debug-exception-file\"] = file\n      end\n    end\n\n    AHTTP::Response.new content, status, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/error_renderer_interface.cr",
    "content": "# An `AHK::ErrorRendererInterface` converts an `::Exception` into an `AHTTP::Response`.\n#\n# The default implementation JSON serialize exceptions.\n# However, it can be overridden to allow rendering errors differently, such as via HTML.\nmodule Athena::HTTPKernel::ErrorRendererInterface\n  # Renders the given *exception* into an `AHTTP::Response`.\n  abstract def render(exception : ::Exception) : AHTTP::Response\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/events/action_event.cr",
    "content": "# Emitted after `AHK::Events::Request` and the related `AHK::Action` has been resolved, but before it has been executed.\n#\n# See the [Getting Started](/getting_started/middleware#2-action-event) docs for more information.\nclass Athena::HTTPKernel::Events::Action < ACTR::EventDispatcher::Event\n  include Athena::HTTPKernel::Events::RequestAware\n\n  # The related `AHK::Action` that will be used to handle the current request.\n  getter action : AHK::ActionBase\n\n  def initialize(request : AHTTP::Request, @action : AHK::ActionBase)\n    super request\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/events/exception_event.cr",
    "content": "# Emitted when an exception occurs. See `AHK::Exception` for more information on how exception handling works in Athena.\n#\n# This event can be listened on to recover from errors or to modify the exception before it's rendered.\n#\n# See the [Getting Started](/getting_started/middleware#8-exception-handling) docs for more information.\nclass Athena::HTTPKernel::Events::Exception < ACTR::EventDispatcher::Event\n  include Athena::HTTPKernel::Events::SettableResponse\n  include Athena::HTTPKernel::Events::RequestAware\n\n  # The `::Exception` associated with `self`.\n  #\n  # Can be replaced by an `AHK::Listeners::Error`.\n  property exception : ::Exception\n\n  def initialize(request : AHTTP::Request, @exception : ::Exception)\n    super request\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/events/request_aware.cr",
    "content": "# Represents an event that has access to the current request object.\nmodule Athena::HTTPKernel::Events::RequestAware\n  # Returns the current request object.\n  getter request : AHTTP::Request\n\n  def initialize(@request : AHTTP::Request); end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/events/request_event.cr",
    "content": "# Emitted very early in the request's life-cycle; before the corresponding `AHK::Action` (if any) has been resolved.\n#\n# This event can be listened on in order to:\n#\n# * Add information to the request, via its [AHTTP::Request#attributes](/HTTP/Request/#Athena::HTTP::Request#attributes)\n# * Return a response immediately if there is enough information available\n#\n# NOTE: If your listener logic requires that the the corresponding `AHK::Action` has been resolved, use `AHK::Events::Action` instead.\n#\n# See the [Getting Started](/getting_started/middleware#1-request-event) docs for more information.\nclass Athena::HTTPKernel::Events::Request < ACTR::EventDispatcher::Event\n  include Athena::HTTPKernel::Events::SettableResponse\n  include Athena::HTTPKernel::Events::RequestAware\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/events/response_event.cr",
    "content": "# Emitted after the route's action has been executed, but before the response has been returned to the client.\n#\n# This event can be listened on to modify the response object further before it is returned;\n# such as adding headers/cookies, compressing the response, etc.\n#\n# See the [Getting Started](/getting_started/middleware#5-response-event) docs for more information.\nclass Athena::HTTPKernel::Events::Response < ACTR::EventDispatcher::Event\n  include Athena::HTTPKernel::Events::RequestAware\n\n  # The response object.\n  property response : AHTTP::Response\n\n  def initialize(request : AHTTP::Request, @response : AHTTP::Response)\n    super request\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/events/settable_response.cr",
    "content": "# Represents an event where an `AHTTP::Response` can be set on `self` to handle the original `AHTTP::Request`.\n#\n# WARNING: Once `#response=` is called, propagation stops; i.e. listeners with lower priority will not be executed.\nmodule Athena::HTTPKernel::Events::SettableResponse\n  # The response object, if any.\n  getter response : AHTTP::Response? = nil\n\n  # Sets the *response* that will be returned for the current `AHTTP::Request` being handled.\n  #\n  # Propagation of `self` will stop once `#response=` is called.\n  def response=(@response : AHTTP::Response) : Nil\n    self.stop_propagation\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/events/terminate_event.cr",
    "content": "# Emitted very late in the request's life-cycle, after the response has been sent.\n#\n# This event can be listened on to perform tasks that are not required to finish before the response is sent; such as sending emails, or other \"heavy\" tasks.\n#\n# See the [Getting Started](/getting_started/middleware#7-terminate-event) docs for more information.\nclass Athena::HTTPKernel::Events::Terminate < ACTR::EventDispatcher::Event\n  include Athena::HTTPKernel::Events::RequestAware\n\n  # The response object.\n  getter response : AHTTP::Response\n\n  def initialize(request : AHTTP::Request, @response : AHTTP::Response)\n    super request\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/events/view_event.cr",
    "content": "# Emitted after the route's action has been executed, but only if it does _NOT_ return an `AHTTP::Response`.\n#\n# This event can be listened on to handle converting a non `AHTTP::Response` into an `AHTTP::Response`.\n#\n# See the [Getting Started](/getting_started/middleware#4-view-event) docs for more information.\nclass Athena::HTTPKernel::Events::View < ACTR::EventDispatcher::Event\n  include Athena::HTTPKernel::Events::SettableResponse\n  include Athena::HTTPKernel::Events::RequestAware\n\n  private module ContainerBase; end\n\n  private record ResultContainer(T), data : T do\n    include ContainerBase\n\n    # :inherit:\n    def inspect(io : IO) : Nil\n      io << \"#<ViewResult(\" << {{ T.stringify }} << \")>\"\n    end\n  end\n\n  @result : ContainerBase\n\n  def initialize(request : AHTTP::Request, action_result : _)\n    super request\n\n    @result = ResultContainer.new action_result\n  end\n\n  # Returns the value returned from the related controller action.\n  def action_result\n    @result.data\n  end\n\n  # Overrides the return value of the related controller action.\n  #\n  # Can be used to mutate the controller action's returned value within a listener context;\n  # such as for pagination.\n  def action_result=(value : _) : Nil\n    @result = ResultContainer.new value\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/bad_gateway.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::BadGateway < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :bad_gateway, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/bad_request.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::BadRequest < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :bad_request, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/conflict.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::Conflict < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :conflict, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/forbidden.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::Forbidden < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :forbidden, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/gone.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::Gone < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :gone, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/http_exception.cr",
    "content": "# :nodoc:\nclass ::Exception\n  def to_json(builder : JSON::Builder) : Nil\n    builder.object do\n      builder.field \"code\", 500\n      builder.field \"message\", \"Internal Server Error\"\n    end\n  end\nend\n\n# Represents an HTTP error.\n#\n# Each child represents a specific HTTP error with the associated status code.\n# Also optionally allows adding headers to the resulting response.\n#\n# Can be used directly/inherited from to represent non-typical HTTP errors/codes.\nclass Athena::HTTPKernel::Exception::HTTPException < ::Exception\n  include Athena::HTTPKernel::Exception\n\n  # Helper method to return the proper exception subclass for the provided *status*.\n  # The *message*, *cause*, and *headers* are passed along as well if provided.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  def self.from_status(\n    status : Int32 | ::HTTP::Status,\n    message : String = \"\",\n    cause : ::Exception? = nil,\n    headers : ::HTTP::Headers = ::HTTP::Headers.new,\n  ) : self\n    status = status.is_a?(::HTTP::Status) ? status : ::HTTP::Status.new(status)\n\n    case status\n    when .bad_request?            then AHK::Exception::BadRequest.new(message, cause, headers)\n    when .forbidden?              then AHK::Exception::Forbidden.new(message, cause, headers)\n    when .not_found?              then AHK::Exception::NotFound.new(message, cause, headers)\n    when .not_acceptable?         then AHK::Exception::NotAcceptable.new(message, cause, headers)\n    when .conflict?               then AHK::Exception::Conflict.new(message, cause, headers)\n    when .gone?                   then AHK::Exception::Gone.new(message, cause, headers)\n    when .length_required?        then AHK::Exception::LengthRequired.new(message, cause, headers)\n    when .precondition_failed?    then AHK::Exception::PreconditionFailed.new(message, cause, headers)\n    when .unsupported_media_type? then AHK::Exception::UnsupportedMediaType.new(message, cause, headers)\n    when .unprocessable_entity?   then AHK::Exception::UnprocessableEntity.new(message, cause, headers)\n    when .too_many_requests?      then AHK::Exception::TooManyRequests.new(message, nil, cause, headers)\n    when .service_unavailable?    then AHK::Exception::ServiceUnavailable.new(message, nil, cause, headers)\n    else\n      new status, message, cause, headers\n    end\n  end\n\n  # The `::HTTP::Status` associated with `self`.\n  getter status : ::HTTP::Status\n\n  # Any HTTP response headers associated with `self`.\n  #\n  # Some HTTP errors use response headers to give additional information about `self`.\n  property headers : ::HTTP::Headers\n\n  # Instantiates `self` with the given *status* and *message*.\n  #\n  # Optionally includes *cause*, and *headers*.\n  def initialize(@status : ::HTTP::Status, message : String, cause : ::Exception? = nil, @headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super message, cause\n  end\n\n  # Instantiates `self` with the given *status_code* and *message*.\n  #\n  # Optionally includes *cause*, and *headers*.\n  def self.new(status_code : Int32, message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    new ::HTTP::Status.new(status_code), message, cause, headers\n  end\n\n  # Returns the HTTP status code of `#status`.\n  def status_code : Int32\n    @status.value\n  end\n\n  # Serializes `self` to JSON in the format of `{\"code\":400,\"message\":\"Exception message\"}`\n  def to_json(builder : JSON::Builder) : Nil\n    builder.object do\n      builder.field \"code\", self.status_code\n      builder.field \"message\", @message\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/length_required.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::LengthRequired < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :length_required, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/logic.cr",
    "content": "# Represents a code logic error that should lead directly to a fix in your code.\nclass Athena::HTTPKernel::Exception::Logic < ::Exception\n  include Athena::HTTPKernel::Exception\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/method_not_allowed.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::MethodNotAllowed < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(\n    allow : Array(String),\n    message : String,\n    cause : ::Exception? = nil,\n    headers : ::HTTP::Headers = ::HTTP::Headers.new,\n  )\n    headers[\"allow\"] = allow.join \", \", &.upcase\n\n    super :method_not_allowed, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/not_acceptable.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::NotAcceptable < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :not_acceptable, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/not_found.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::NotFound < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :not_found, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/not_implemented.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::NotImplemented < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :not_implemented, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/precondition_failed.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::PreconditionFailed < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :precondition_failed, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/service_unavailable.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::ServiceUnavailable < Athena::HTTPKernel::Exception::HTTPException\n  # See `Athena::HTTPKernel::Exception::HTTPException#new`.\n  #\n  # If *retry_after* is provided, adds a `retry-after` header that represents the number of seconds or HTTP-date after which the request may be retried.\n  def initialize(message : String, retry_after : Number | String | Nil = nil, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    headers[\"retry-after\"] = retry_after.to_s if retry_after\n\n    super :service_unavailable, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/stop_format_listener.cr",
    "content": "class Athena::HTTPKernel::Exception::StopFormatListener < ::Exception\n  include Athena::HTTPKernel::Exception\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/too_many_requests.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::TooManyRequests < Athena::HTTPKernel::Exception::HTTPException\n  # See `Athena::HTTPKernel::Exception::HTTPException#new`.\n  #\n  # If *retry_after* is provided, adds a `retry-after` header that represents the number of seconds or HTTP-date after which the request may be retried.\n  def initialize(message : String, retry_after : Number | String | Nil = nil, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    headers[\"retry-after\"] = retry_after.to_s if retry_after\n\n    super :too_many_requests, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/unauthorized.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::Unauthorized < Athena::HTTPKernel::Exception::HTTPException\n  # See `Athena::HTTPKernel::Exception::HTTPException#new`.\n  #\n  # Includes a `www-authenticate` header with the provided *challenge*.\n  def initialize(message : String, challenge : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    headers[\"www-authenticate\"] = challenge\n\n    super :unauthorized, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/unprocessable_entity.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::UnprocessableEntity < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :unprocessable_entity, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/exception/unsupported_media_type.cr",
    "content": "require \"./http_exception\"\n\nclass Athena::HTTPKernel::Exception::UnsupportedMediaType < Athena::HTTPKernel::Exception::HTTPException\n  def initialize(message : String, cause : ::Exception? = nil, headers : ::HTTP::Headers = ::HTTP::Headers.new)\n    super :unsupported_media_type, message, cause, headers\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/http_kernel.cr",
    "content": "# The entry-point into `Athena::HTTPKernel`.\n#\n# Emits events that handle a given request and returns the resulting `AHTTP::Response`.\nstruct Athena::HTTPKernel::HTTPKernel\n  def initialize(\n    @event_dispatcher : AED::EventDispatcherInterface,\n    @request_store : AHTTP::RequestStore,\n    @argument_resolver : AHK::Controller::ArgumentResolverInterface,\n    @action_resolver : AHK::ActionResolverInterface,\n  )\n  end\n\n  def handle(request : ::HTTP::Request) : AHTTP::Response\n    self.handle AHTTP::Request.new request\n  end\n\n  def handle(request : AHTTP::Request) : AHTTP::Response\n    handle_raw request\n  rescue ex : ::Exception\n    event = AHK::Events::Exception.new request, ex\n    @event_dispatcher.dispatch event\n\n    exception = event.exception\n\n    unless response = event.response\n      finish_request\n\n      raise exception\n    end\n\n    if exception.is_a? AHK::Exception::HTTPException\n      response.status = exception.status\n      response.headers.merge! exception.headers\n    end\n\n    begin\n      finish_response response, request\n    rescue\n      response\n    end\n  end\n\n  # Terminates a request/response lifecycle.\n  #\n  # Should be called after sending the response to the client.\n  def terminate(request : AHTTP::Request, response : AHTTP::Response) : Nil\n    @event_dispatcher.dispatch AHK::Events::Terminate.new request, response\n  end\n\n  private def handle_raw(request : AHTTP::Request) : AHTTP::Response\n    # Set the current request in the RequestStore.\n    @request_store.request = request\n\n    # Emit the request event.\n    request_event = AHK::Events::Request.new request\n    @event_dispatcher.dispatch request_event\n\n    # Return the event early if the request event handled the request.\n    if response = request_event.response\n      return finish_response response, request\n    end\n\n    unless action = @action_resolver.resolve request\n      raise AHK::Exception::NotFound.new \"Unable to find the action for path '#{request.path}'.\"\n    end\n\n    # Emit the action event.\n    @event_dispatcher.dispatch AHK::Events::Action.new request, action\n\n    # Resolve the arguments for this action from the request.\n    arguments = @argument_resolver.get_arguments request, action\n\n    # Call the action and get the response.\n    response = action.execute arguments\n\n    unless response.is_a? AHTTP::Response\n      view_event = AHK::Events::View.new request, response\n      @event_dispatcher.dispatch view_event\n\n      unless response = view_event.response\n        raise %('#{request.attributes.get? \"_controller\" || \"AHK::Action\"}' must return an `AHTTP::Response` but it returned '#{response}'.)\n      end\n    end\n\n    finish_response response, request\n  end\n\n  private def finish_response(response : AHTTP::Response, request : AHTTP::Request) : AHTTP::Response\n    # Emit the response event.\n    event = AHK::Events::Response.new request, response\n\n    @event_dispatcher.dispatch event\n\n    self.finish_request\n\n    event.response\n  end\n\n  private def finish_request : Nil\n    # Reset the request store.\n    @request_store.reset\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/listeners/error.cr",
    "content": "# Handles an exception by converting it into an `AHTTP::Response` via an `AHK::ErrorRendererInterface`.\n#\n# This listener defines a `log_exception` protected method that determines how the exception gets logged.\n# Non `AHK::Exception::HTTPException`s and server errors are logged as errors.\n# Validation errors (`AHK::Exception::UnprocessableEntity`) are logged as notice.\n# Everything else is logged as a warning.\n# The method can be redefined if different logic is desired.\n#\n# ```\n# struct AHK::Listeners::Error\n#   # :inherit:\n#   protected def log_exception(exception : ::Exception, & : -> String) : Nil\n#     # Don't log anything if an exception is some specific type.\n#     return if exception.is_a? MyException\n#\n#     # Exception types could also include modules to act as interfaces to determine their level, E.g. `include NoticeException`.\n#     if exception.is_a? NoticeException\n#       Log.notice(exception: exception) { yield }\n#       return\n#     end\n#\n#     # Otherwise fallback to the default implementation.\n#     previous_def\n#   end\n# end\n# ```\nstruct Athena::HTTPKernel::Listeners::Error\n  def initialize(@error_renderer : AHK::ErrorRendererInterface); end\n\n  @[AEDA::AsEventListener(priority: -50)]\n  def on_exception(event : AHK::Events::Exception) : Nil\n    exception = event.exception\n\n    log_exception(exception) { \"Uncaught exception #{exception.inspect} at #{exception.backtrace?.try &.first}\" }\n\n    event.response = @error_renderer.render event.exception\n  rescue ex : ::Exception\n    # Also log exceptions raised when handling an exception\n    log_exception(ex) { \"Exception raised when handling an exception #{ex.inspect} at #{ex.backtrace?.try &.first}\" }\n\n    raise ex\n  end\n\n  # Logs the provided *exception*, *yields* if the message will be logged.\n  #\n  # Applications can redefine this method to customize how exceptions are logged.\n  protected def log_exception(exception : ::Exception, & : -> String) : Nil\n    if !exception.is_a?(AHK::Exception::HTTPException) || exception.status.server_error?\n      # Log non HTTPExceptions and server errors as errors\n      Log.error(exception: exception) { yield }\n    elsif exception.is_a? AHK::Exception::UnprocessableEntity\n      # Log failed validations as notice\n      Log.notice(exception: exception) { yield }\n    else\n      # Log everything else as warnings\n      Log.warn(exception: exception) { yield }\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/http_kernel/src/listeners/routing.cr",
    "content": "# Sets route parameters on the current request via an `ART::RequestMatcherInterface`.\n#\n# This listener is only functional when the `athena-routing` component is available.\nstruct Athena::HTTPKernel::Listeners::Routing\n  @request_context : ART::RequestContext\n\n  def initialize(\n    @matcher : ART::Matcher::URLMatcherInterface | ART::Matcher::RequestMatcherInterface,\n    request_context : ART::RequestContext? = nil,\n  )\n    @request_context = request_context || @matcher.context\n  end\n\n  @[AEDA::AsEventListener(priority: 32)]\n  def on_request(event : AHK::Events::Request) : Nil\n    request = event.request\n\n    @request_context.apply request\n\n    begin\n      parameters = if @matcher.is_a? ART::Matcher::RequestMatcherInterface\n                     @matcher.match request\n                   else\n                     @matcher.match request.path\n                   end\n\n      Log.info &.emit %(Matched route '#{matched_route = parameters[\"_route\"]? || \"n/a\"}'),\n        route: matched_route,\n        route_parameters: parameters.to_h,\n        request_uri: request.resource,\n        method: request.method\n\n      parameters.each { |k, v| request.attributes.set k, v }\n\n      parameters.delete \"_route\"\n\n      request.attributes.set \"_route_params\", parameters, ART::Parameters\n    rescue ex : ART::Exception::ResourceNotFound\n      message = \"No route found for '#{request.method} #{request.resource}'\"\n\n      # This is misspelled on purpose, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer.\n      if referrer = request.headers[\"referer\"]? # spellchecker:disable-line\n        message += \" (from: '#{referrer}')\"\n      end\n\n      message += \".\"\n\n      raise AHK::Exception::NotFound.new message, ex\n    rescue ex : ART::Exception::MethodNotAllowed\n      raise AHK::Exception::MethodNotAllowed.new(\n        ex.allowed_methods,\n        %(No route found for '#{request.method} #{request.resource}': Method Not Allowed (Allow: #{ex.allowed_methods.join \", \"}).),\n        ex\n      )\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/image_size/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/image_size/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.1.4] - 2025-01-26\n\n_Administrative release, no functional changes_\n\n[0.1.4]: https://github.com/athena-framework/image-size/releases/tag/v0.1.4\n\n## [0.1.3] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/image-size/releases/tag/v0.1.3\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.1.2] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.1.2]: https://github.com/athena-framework/image-size/releases/tag/v0.1.2\n\n## [0.1.1] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect `description` key in `shard.yml` ([#171]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/image-size/releases/tag/v0.1.1\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#171]: https://github.com/athena-framework/athena/pull/171\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.0] - 2022-02-21\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/image-size/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/image_size/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/image_size/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2022 George Dietrich\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": "src/components/image_size/README.md",
    "content": "# ImageSize\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/image-size.svg)](https://github.com/athena-framework/image-size/releases)\n\nMeasures the size of various image formats.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/ImageSize).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/image_size/docs/README.md",
    "content": "The `Athena::ImageSize` component allows measuring the size of various [image formats](/ImageSize/Image/Format/).\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-image_size:\n    github: athena-framework/image-size\n    version: ~> 0.1.0\n```\n\n## Usage\n\nan [AIS::Image](/ImageSize/Image/) instance can be instantiated given a path to an image file, or via an [IO](https://crystal-lang.org/api/IO.html).\nFrom there, information about the image can be accessed off of the instance.\n\n```crystal\nAIS::Image.from_file_path \"spec/images/jpeg/436x429_8_3.jpeg\" # =>\n# Athena::ImageSize::Image(\n# @bits=8,\n# @channels=3,\n# @format=JPEG,\n# @height=429,\n# @width=436)\n```\n"
  },
  {
    "path": "src/components/image_size/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Image Size\nsite_url: https://athenaframework.org/ImageSize/\nrepo_url: https://github.com/athena-framework/image-size\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-image_size/src/athena-image_size.cr\n          source_locations:\n            lib/athena-image_size: https://github.com/athena-framework/image-size/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/image_size/shard.yml",
    "content": "name: athena-image_size\n\nversion: 0.1.4\n\ncrystal: ~> 1.4\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/image-size\n\ndocumentation: https://athenaframework.org/ImageSize\n\ndescription: |\n  Measures the size of various image formats.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n"
  },
  {
    "path": "src/components/image_size/spec/athena-image_size_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct ImageTest < ASPEC::TestCase\n  @[DataProvider(\"files\")]\n  def test_from_io(file_path : String)\n    File.open file_path do |file|\n      image = AIS::Image.from_io file\n\n      filename = File.basename file_path\n\n      # width, height, bits, channels, format\n      /(\\d+)x(\\d+)_(\\d+)_(\\d+)\\.(\\w+)$/.match(filename)\n\n      expected_width, expected_height, expected_bits, expected_channels, expected_format = $~[1..5]\n\n      expected_bits = \"0\" == expected_bits ? nil : expected_bits.to_i\n      expected_channels = \"0\" == expected_channels ? nil : expected_channels.to_i\n\n      image.width.should eq expected_width.to_i\n      image.height.should eq expected_height.to_i\n      image.size.should eq({expected_width.to_i, expected_height.to_i})\n      image.bits.should eq expected_bits\n      image.channels.should eq expected_channels\n      image.format.should eq AIS::Image::Format.parse(expected_format)\n    end\n  end\n\n  def test_from_io_unsupported_raises : Nil\n    tempfile = File.tempfile\n    tempfile.write Bytes.new 50, 0\n    tempfile.rewind\n\n    expect_raises Exception, \"Unsupported image format.\" do\n      AIS::Image.from_io tempfile\n    end\n\n    tempfile.delete\n  end\n\n  def test_from_io_unsupported_nil : Nil\n    tempfile = File.tempfile\n    tempfile.write Bytes.new 50, 0\n    tempfile.rewind\n\n    AIS::Image.from_io?(tempfile).should be_nil\n\n    tempfile.delete\n  end\n\n  def test_from_io_parse_failure_nil : Nil\n    tempfile = File.tempfile\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x01\n    tempfile.write_byte 0x00\n    tempfile.write_bytes 50\n    tempfile.write_bytes 10\n    tempfile.write_bytes 10\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x01 # This byte is required to be `0`\n    tempfile.rewind\n\n    AIS::Image.from_io?(tempfile).should be_nil\n\n    tempfile.delete\n  end\n\n  def test_from_io_parse_failure_raises : Nil\n    tempfile = File.tempfile\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x01\n    tempfile.write_byte 0x00\n    tempfile.write_bytes 50\n    tempfile.write_bytes 10\n    tempfile.write_bytes 10\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x01 # This byte is required to be `0`\n    tempfile.rewind\n\n    expect_raises Exception, \"Failed to parse image.\" do\n      AIS::Image.from_io tempfile\n    end\n\n    tempfile.delete\n  end\n\n  @[DataProvider(\"files\")]\n  def test_from_file_path(file_path : String)\n    image = AIS::Image.from_file_path file_path\n\n    filename = File.basename file_path\n\n    # width, height, bits, channels, format\n    /(\\d+)x(\\d+)_(\\d+)_(\\d+)\\.(\\w+)$/.match(filename)\n\n    expected_width, expected_height, expected_bits, expected_channels, expected_format = $~[1..5]\n\n    expected_bits = \"0\" == expected_bits ? nil : expected_bits.to_i\n    expected_channels = \"0\" == expected_channels ? nil : expected_channels.to_i\n\n    image.width.should eq expected_width.to_i\n    image.height.should eq expected_height.to_i\n    image.bits.should eq expected_bits\n    image.channels.should eq expected_channels\n    image.format.should eq AIS::Image::Format.parse(expected_format)\n  end\n\n  def test_from_file_path_unsupported_raises : Nil\n    tempfile = File.tempfile\n    tempfile.write Bytes.new 50, 0\n    tempfile.rewind\n\n    expect_raises Exception, \"Unsupported image format.\" do\n      AIS::Image.from_file_path tempfile.path\n    end\n\n    tempfile.delete\n  end\n\n  def test_from_file_path_unsupported_nil : Nil\n    tempfile = File.tempfile\n    tempfile.write Bytes.new 50, 0\n    tempfile.rewind\n\n    AIS::Image.from_file_path?(tempfile.path).should be_nil\n\n    tempfile.delete\n  end\n\n  def test_from_file_path_parse_failure_nil : Nil\n    tempfile = File.tempfile\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x01\n    tempfile.write_byte 0x00\n    tempfile.write_bytes 50\n    tempfile.write_bytes 10\n    tempfile.write_bytes 10\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x01 # This byte is required to be `0`\n    tempfile.rewind\n\n    AIS::Image.from_file_path?(tempfile.path).should be_nil\n\n    tempfile.delete\n  end\n\n  def test_from_file_path_parse_failure_raises : Nil\n    tempfile = File.tempfile\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x01\n    tempfile.write_byte 0x00\n    tempfile.write_bytes 50\n    tempfile.write_bytes 10\n    tempfile.write_bytes 10\n    tempfile.write_byte 0x00\n    tempfile.write_byte 0x01 # This byte is required to be `0`\n    tempfile.rewind\n\n    expect_raises Exception, \"Failed to parse image.\" do\n      AIS::Image.from_file_path tempfile.path\n    end\n\n    tempfile.delete\n  end\n\n  def files : Hash\n    Dir.glob(\"#{__DIR__}/images/*/*\").each_with_object(Hash(String, Tuple(String)).new) do |name, hash|\n      hash[name] = {name}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/spec/spec_helper.cr",
    "content": "require \"spec\"\n\nrequire \"athena-spec\"\nrequire \"../src/athena-image_size\"\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/image_size/src/athena-image_size.cr",
    "content": "require \"./image_format\"\nrequire \"./extractors/*\"\nrequire \"./image\"\n\n# Convenience alias to make referencing `Athena::ImageSize` types easier.\nalias AIS = Athena::ImageSize\n\n# Allows measuring the size of various [image formats][Athena::ImageSize::Image::Format].\nmodule Athena::ImageSize\n  VERSION = \"0.1.4\"\n\n  # Represents the [DPI (Dots Per Inch)](https://en.wikipedia.org/wiki/Dots_per_inch) used to calculate dimensions of `AIS::Image::Format::SVG` images, defaulting to `72.0`.\n  class_property dpi : Float64 = 72.0\n\n  # :nodoc:\n  module Extractors; end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/abstract_ico.cr",
    "content": "require \"./extractor\"\n\nabstract struct Athena::ImageSize::Extractors::AbstractICO < Athena::ImageSize::Extractors::Extractor\n  def self.extract(io : IO) : AIS::Image?\n    num_icons = io.read_bytes UInt16\n\n    width = 0\n    height = 0\n    bits = 0\n\n    return if num_icons < 1 || num_icons > 255\n\n    num_icons.times do\n      icon_width = io.read_bytes UInt8\n      icon_height = io.read_bytes UInt8\n\n      # Skip color count\n      io.skip 1\n\n      # This bit must be `0`\n      return unless io.read_bytes(UInt8).zero?\n\n      # Skip color planes\n      io.skip 2\n\n      if (icon_bits = io.read_bytes UInt16) >= bits\n        width, height, bits = icon_width, icon_height, icon_bits\n      end\n    end\n\n    Image.new width.zero? ? 256 : width, height.zero? ? 256 : height, self.format, bits\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/abstract_png.cr",
    "content": "require \"./extractor\"\n\nabstract struct Athena::ImageSize::Extractors::AbstractPNG < Athena::ImageSize::Extractors::Extractor\n  private SIGNATURE = Bytes[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, read_only: true]\n\n  # Based on https://github.com/php/php-src/blob/95da6e807a948039d3a42defbd849c4fed6cbe35/ext/standard/image.c#L299.\n  def self.extract(io : IO) : AIS::Image?\n    io.skip 4 # Skip data length and type\n\n    return if \"IHDR\" != io.read_string(4)\n\n    width = io.read_bytes UInt32, IO::ByteFormat::BigEndian\n    height = io.read_bytes UInt32, IO::ByteFormat::BigEndian\n    bits = io.read_bytes UInt8, IO::ByteFormat::BigEndian\n\n    io.skip 8 # Skip rest of chunk data, and CRC\n\n    format = Image::Format::PNG\n\n    # Determine if the PNG is an actual PNG or an APNG\n    loop do\n      data_chunk_length = io.read_bytes UInt32, IO::ByteFormat::BigEndian\n      chunk_type = io.read_string 4\n\n      break if chunk_type.in? \"IDAT\", \"IEND\", nil\n      if \"acTL\" == chunk_type\n        format = Image::Format::APNG\n        break\n      end\n\n      io.skip data_chunk_length + 4 # Skips data and CRC chunk\n    end\n\n    Image.new width, height, format, bits\n  end\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    return false unless bytes[0, 3] == SIGNATURE[0, 3]\n\n    eight_bytes = Bytes.new 8\n    io.pos -= 3\n    io.read_fully eight_bytes\n\n    eight_bytes == SIGNATURE\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/abstract_tiff.cr",
    "content": "abstract struct Athena::ImageSize::Extractors::AbstractTIFF < Athena::ImageSize::Extractors::Extractor\n  private enum Tag\n    ImageWidth      = 0x0100\n    ImageLength     = 0x0101\n    BitsPerSample   = 0x0102\n    SamplesPerPixel = 0x0115\n  end\n\n  private enum DataType : UInt16\n    BYTE      =  1 # 8-bit unsigned integer\n    STRING    =  2 # 8-bit, NULL-terminated string\n    USHORT    =  3 # 16-bit unsigned integer\n    ULONG     =  4 # 32-bit unsigned integer\n    URATIONAL =  5 # Two 32-bit unsigned integers\n    SBYTE     =  6 # 8-bit signed integer\n    UNDEFINED =  7 # 8-bit byte\n    SSHORT    =  8 # 16-bit signed integer\n    SLONG     =  9 # 32-bit signed integer\n    SRATIONAL = 10 # Two 32-bit signed integers\n    FLOAT     = 11 # 4-byte single-precision IEEE floating-point value\n    DOUBLE    = 12 # 8-byte double-precision IEEE floating-point value\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  def self.extract(io : IO) : AIS::Image?\n    offset = io.read_bytes UInt32, self.byte_format\n\n    io.skip offset - 8 # Account for already read bytes\n    num_dirent = io.read_bytes UInt16, self.byte_format\n    num_dirent = (io.pos + 2) + (num_dirent * 12)\n\n    width = height = bits = channels = nil\n    until width && height && bits && channels\n      return if io.pos > num_dirent\n\n      ifd = Bytes.new 12\n      io.read_fully ifd\n      ifd = IO::Memory.new ifd, false\n\n      unless tag = Tag.from_value? ifd.read_bytes UInt16, self.byte_format\n        next\n      end\n\n      data_type = DataType.new ifd.read_bytes UInt16, self.byte_format\n\n      ifd.skip 4\n\n      entry_value = case data_type\n                    when .byte?, .sbyte? then ifd.read_bytes(Int8, self.byte_format)\n                    when .ushort?        then ifd.read_bytes(UInt16, self.byte_format)\n                    when .sshort?        then ifd.read_bytes(Int16, self.byte_format)\n                    when .ulong?         then ifd.read_bytes(UInt32, self.byte_format)\n                    when .slong?         then ifd.read_bytes(Int32, self.byte_format)\n                    else\n                      next\n                    end\n\n      case tag\n      in .image_width?       then width = entry_value\n      in .image_length?      then height = entry_value\n      in .bits_per_sample?   then bits = entry_value\n      in .samples_per_pixel? then channels = entry_value\n      end\n    end\n\n    Image.new width, height, :tiff, bits, channels\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/apng.cr",
    "content": "struct Athena::ImageSize::Extractors::APNG < Athena::ImageSize::Extractors::AbstractPNG\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/bmp.cr",
    "content": "require \"./extractor\"\n\nstruct Athena::ImageSize::Extractors::BMP < Athena::ImageSize::Extractors::Extractor\n  private SIGNATURE = Bytes['B'.ord, 'M'.ord, read_only: true]\n\n  def self.extract(io : IO) : AIS::Image?\n    io.skip 11 # Skip rest of Header chunk\n\n    info_header_length = io.read_bytes UInt32\n\n    if 12 == info_header_length # BITMAPCOREHEADER\n      width = io.read_bytes Int16\n      height = io.read_bytes Int16\n\n      io.skip 3\n\n      bits = io.read_bytes UInt8\n    elsif 40 == info_header_length # BITMAPINFOHEADER\n      width = io.read_bytes Int32\n      height = io.read_bytes(Int32).abs\n\n      io.skip 2\n\n      bits = io.read_bytes UInt16\n    else\n      return\n    end\n\n    Image.new width, height, :bmp, bits.zero? ? nil : bits\n  end\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 2] == SIGNATURE\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/cur.cr",
    "content": "struct Athena::ImageSize::Extractors::CUR < Athena::ImageSize::Extractors::AbstractICO\n  private SIGNATURE = Bytes[0x00, 0x00, 0x02, 0x00, read_only: true]\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 4] == SIGNATURE\n  end\n\n  protected def self.format : AIS::Image::Format\n    Image::Format::CUR\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/extractor.cr",
    "content": "# :nodoc:\nabstract struct Athena::ImageSize::Extractors::Extractor\n  # ameba:disable Metrics/CyclomaticComplexity\n  def self.from_io(io : IO)\n    bytes = Bytes.new 3\n    io.read_fully bytes\n\n    return PNG if PNG.matches? io, bytes\n    return JPEG if JPEG.matches? io, bytes\n    return GIF if GIF.matches? io, bytes\n    return BMP if BMP.matches? io, bytes\n    return APNG if APNG.matches? io, bytes\n    return SWF if SWF.matches? io, bytes\n\n    # Read in an additionl bytes to determine the format.\n    bytes = Bytes.new 4\n    io.pos -= 3\n    io.read_fully bytes\n\n    return MMTIFF if MMTIFF.matches? io, bytes\n    return IITIFF if IITIFF.matches? io, bytes\n    return ICO if ICO.matches? io, bytes\n    return CUR if CUR.matches? io, bytes\n    return PSD if PSD.matches? io, bytes\n\n    # Read in an additionl bytes to determine the format.\n    bytes = Bytes.new 8\n    io.pos -= 4\n    io.read_fully bytes\n\n    return MNG if MNG.matches? io, bytes\n\n    # Read in an additionl bytes to determine the format.\n    bytes = Bytes.new 12\n    io.pos -= 8\n    io.read_fully bytes\n\n    return WebP if WebP.matches? io, bytes\n\n    # Read in an additionl bytes to determine the format.\n    # These are text based formats so will need to instantiate a string for the logic to work.\n    # Being sure to rewind the IO.\n    bytes = Bytes.new 4096\n    io.pos -= 12\n    io.read_fully? bytes\n\n    io.rewind\n\n    return SVG if SVG.matches? io, bytes\n\n    nil\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/gif.cr",
    "content": "struct Athena::ImageSize::Extractors::GIF < Athena::ImageSize::Extractors::Extractor\n  private SIGNATURE = Bytes['G'.ord, 'I'.ord, 'F'.ord, read_only: true]\n\n  # Based on https://github.com/php/php-src/blob/95da6e807a948039d3a42defbd849c4fed6cbe35/ext/standard/image.c#L100.\n  def self.extract(io : IO) : AIS::Image?\n    io.skip 3 # Skip the version string\n\n    width = io.read_bytes(UInt16)\n    height = io.read_bytes(UInt16)\n\n    packed_bit = io.read_byte.not_nil!\n\n    # Not 100% sure what this is doing, probably parsing something from the packed field.\n    bits = !(packed_bit & 0x80).zero? ? ((packed_bit & 0x07) + 1) : 0\n\n    Image.new width, height, :gif, bits, 3\n  end\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 3] == SIGNATURE\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/ico.cr",
    "content": "struct Athena::ImageSize::Extractors::ICO < Athena::ImageSize::Extractors::AbstractICO\n  private SIGNATURE = Bytes[0x00, 0x00, 0x01, 0x00, read_only: true]\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 4] == SIGNATURE\n  end\n\n  protected def self.format : AIS::Image::Format\n    Image::Format::ICO\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/ii_tiff.cr",
    "content": "struct Athena::ImageSize::Extractors::IITIFF < Athena::ImageSize::Extractors::AbstractTIFF\n  private SIGNATURE = Bytes['I'.ord, 'I'.ord, 0x2A, 0x00, read_only: true]\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 4] == SIGNATURE\n  end\n\n  protected def self.byte_format : IO::ByteFormat\n    IO::ByteFormat::LittleEndian\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/jpeg.cr",
    "content": "struct Athena::ImageSize::Extractors::JPEG < Athena::ImageSize::Extractors::Extractor\n  private SIGNATURE = Bytes[0xff, 0xd8, 0xff, read_only: true]\n\n  private enum Block : UInt8\n    M_SOF0  = 0xC0 # Start Of Frame N\n    M_SOF1  = 0xC1 # N indicates which compression process\n    M_SOF2  = 0xC2 # Only SOF0-SOF2 are now in common use\n    M_SOF3  = 0xC3\n    M_SOF5  = 0xC5 # NB: codes C4 and CC are NOT SOF markers\n    M_SOF6  = 0xC6\n    M_SOF7  = 0xC7\n    M_SOF9  = 0xC9\n    M_SOF10 = 0xCA\n    M_SOF11 = 0xCB\n    M_SOF13 = 0xCD\n    M_SOF14 = 0xCE\n    M_SOF15 = 0xCF\n    M_SOI   = 0xD8\n    M_EOI   = 0xD9 # End Of Image (end of datastream)\n    M_SOS   = 0xDA # Start Of Scan (begins compressed data)\n    M_APP0  = 0xe0\n    M_APP1  = 0xe1\n    M_APP2  = 0xe2\n    M_APP3  = 0xe3\n    M_APP4  = 0xe4\n    M_APP5  = 0xe5\n    M_APP6  = 0xe6\n    M_APP7  = 0xe7\n    M_APP8  = 0xe8\n    M_APP9  = 0xe9\n    M_APP10 = 0xea\n    M_APP11 = 0xeb\n    M_APP12 = 0xec\n    M_APP13 = 0xed\n    M_APP14 = 0xee\n    M_APP15 = 0xef\n    M_COM   = 0xFE # COMment\n  end\n\n  def self.extract(io : IO) : AIS::Image?\n    ff_read = true\n    image = nil\n\n    loop do\n      marker = self.next_marker io, ff_read\n      ff_read = false\n\n      case marker\n      when Block::M_SOF0, Block::M_SOF1, Block::M_SOF2, Block::M_SOF3, Block::M_SOF5, Block::M_SOF6, Block::M_SOF7,\n           Block::M_SOF9, Block::M_SOF10, Block::M_SOF11, Block::M_SOF13, Block::M_SOF14, Block::M_SOF15\n        if image.nil?\n          io.read_bytes UInt16, IO::ByteFormat::BigEndian\n\n          bits = io.read_byte.not_nil!\n          height = io.read_bytes UInt16, IO::ByteFormat::BigEndian\n          width = io.read_bytes UInt16, IO::ByteFormat::BigEndian\n          channels = io.read_byte.not_nil!\n\n          return Image.new width, height, :jpeg, bits, channels\n        elsif !self.skip_variable(io)\n          return image.unsafe_as(Image)\n        end\n\n        next\n      when Block::M_APP0, Block::M_APP1, Block::M_APP2, Block::M_APP3, Block::M_APP4, Block::M_APP5, Block::M_APP6, Block::M_APP7,\n           Block::M_APP8, Block::M_APP9, Block::M_APP10, Block::M_APP11, Block::M_APP12, Block::M_APP13, Block::M_APP14, Block::M_APP15\n        if !self.skip_variable(io)\n          return image.unsafe_as(Image)\n        end\n\n        next\n      when Block::M_SOS, Block::M_EOI then return image.unsafe_as(Image)\n      else\n        if !self.skip_variable(io)\n          return image.unsafe_as(Image)\n        end\n      end\n    end\n\n    image.unsafe_as(Image)\n  end\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 3] == SIGNATURE\n  end\n\n  private def self.next_marker(io : IO, ff_read : Bool) : Block\n    if !ff_read\n      extraneous = 0\n\n      while (marker = io.read_byte) != 0xff\n        return Block::M_EOI if marker.nil?\n        extraneous += 1\n      end\n    end\n\n    a = 1\n\n    marker = nil\n\n    loop do\n      marker = io.read_byte\n\n      return Block::M_EOI if marker.nil?\n\n      a += 1\n\n      break if marker != 0xff\n    end\n\n    if a < 2\n      return Block::M_EOI\n    end\n\n    Block.new marker.not_nil!\n  end\n\n  private def self.skip_variable(io : IO) : Bool\n    length = io.read_bytes UInt16, IO::ByteFormat::BigEndian\n\n    if length < 2\n      return false\n    end\n\n    length -= 2\n\n    io.pos += length\n\n    true\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/mm_tiff.cr",
    "content": "struct Athena::ImageSize::Extractors::MMTIFF < Athena::ImageSize::Extractors::AbstractTIFF\n  private SIGNATURE = Bytes['M'.ord, 'M'.ord, 0x00, 0x2A, read_only: true]\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 4] == SIGNATURE\n  end\n\n  protected def self.byte_format : IO::ByteFormat\n    IO::ByteFormat::BigEndian\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/mng.cr",
    "content": "struct Athena::ImageSize::Extractors::MNG < Athena::ImageSize::Extractors::Extractor\n  private SIGNATURE = Bytes[0x8a, 0x4d, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, read_only: true]\n\n  def self.extract(io : IO) : AIS::Image?\n    io.skip 4 # Skip the version string\n\n    return if \"MHDR\" != io.read_string(4)\n\n    width = io.read_bytes UInt32, IO::ByteFormat::BigEndian\n    height = io.read_bytes UInt32, IO::ByteFormat::BigEndian\n\n    Image.new width, height, :mng\n  end\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 8] == SIGNATURE\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/png.cr",
    "content": "struct Athena::ImageSize::Extractors::PNG < Athena::ImageSize::Extractors::AbstractPNG\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/psd.cr",
    "content": "struct Athena::ImageSize::Extractors::PSD < Athena::ImageSize::Extractors::Extractor\n  private SIGNATURE = Bytes['8'.ord, 'B'.ord, 'P'.ord, 'S'.ord, read_only: true]\n\n  def self.extract(io : IO) : AIS::Image?\n    io.skip 8 # Skip version and reversed section\n\n    channels = io.read_bytes UInt16, IO::ByteFormat::BigEndian\n    height = io.read_bytes UInt32, IO::ByteFormat::BigEndian\n    width = io.read_bytes UInt32, IO::ByteFormat::BigEndian\n    bits = io.read_bytes UInt16, IO::ByteFormat::BigEndian\n\n    Image.new width, height, :psd, bits, channels\n  end\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 4] == SIGNATURE\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/svg.cr",
    "content": "struct Athena::ImageSize::Extractors::SVG < Athena::ImageSize::Extractors::Extractor\n  private SVG_FORMAT = /<svg\\b([^>]*)>/\n  private XML_FORMAT = /<\\?xml|<!--/\n\n  # Based on https://github.com/php/php-src/blob/95da6e807a948039d3a42defbd849c4fed6cbe35/ext/standard/image.c#L100.\n  def self.extract(io : IO) : AIS::Image?\n    contents = Bytes.new 4096\n    io.read_fully? contents\n    contents = String.new contents\n\n    attributes = Hash(String, String?).new\n\n    svg_tag = contents[0, 1024][SVG_FORMAT, 1]? || contents[0, 4096][SVG_FORMAT, 1]\n\n    svg_tag.scan(/(\\S+)=(?:'([^']*)'|\"([^\"]*)\"|([^'\"\\s]*))/) do |match|\n      attributes[match[1]] = match[2]? || match[3]? || match[4]?\n    end\n\n    width = height = 0\n\n    if w = attributes[\"width\"]?\n      width = self.parse_length w\n    end\n\n    if h = attributes[\"height\"]?\n      height = self.parse_length h\n    end\n\n    Image.new width.not_nil!, height.not_nil!, :svg\n  end\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    contents = String.new bytes\n\n    SVG_FORMAT.matches?(contents) || (XML_FORMAT.matches?(contents) && SVG_FORMAT.matches?(contents))\n  end\n\n  private def self.parse_length(length : String) : Int32\n    pixels = case length.downcase.strip[/(?:em|ex|px|in|cm|mm|pt|pc|%)\\z/]?\n             when \"em\", \"ex\", \"%\" then nil\n             when \"in\"            then length.to_f(strict: false) * AIS.dpi\n             when \"cm\"            then length.to_f(strict: false) * AIS.dpi / 2.54\n             when \"mm\"            then length.to_f(strict: false) * AIS.dpi / 25.4\n             when \"pt\"            then length.to_f(strict: false) * AIS.dpi / 72\n             when \"pc\"            then length.to_f(strict: false) * AIS.dpi / 6\n             else                      length.to_f(strict: false)\n             end\n\n    if pixels\n      pixels.round.to_i\n    else\n      0\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/swf.cr",
    "content": "struct Athena::ImageSize::Extractors::SWF < Athena::ImageSize::Extractors::Extractor\n  private SIGNATURE = Bytes['F'.ord, 'W'.ord, 'S'.ord, read_only: true]\n\n  def self.extract(io : IO) : AIS::Image?\n    io.skip 5\n\n    buffer = Bytes.new 32\n    io.read_fully buffer\n    bits = self.get_bits buffer, 0, 5\n\n    width = (self.get_bits(buffer, 5 + 15, 15) - self.get_bits(buffer, 5, bits)) // 20\n    height = (self.get_bits(buffer, 5 + (3 * bits), bits) - self.get_bits(buffer, 5 + (2 * bits), bits)) // 20\n\n    Image.new width, height, :swf\n  end\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 3] == SIGNATURE\n  end\n\n  private def self.get_bits(buffer : Bytes, pos : Int32, count : Int32) : Int32\n    result = 0\n    l = pos\n\n    while l < (pos + count)\n      result += ((((buffer[l // 8].to_i) >> (7 - (l % 8))) & 0x01) << (count - (l - pos) - 1))\n\n      l += 1\n    end\n\n    result\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/extractors/webp.cr",
    "content": "struct Athena::ImageSize::Extractors::WebP < Athena::ImageSize::Extractors::Extractor\n  private RIFF_SIGNATURE = Bytes['R'.ord, 'I'.ord, 'F'.ord, 'F'.ord, read_only: true]\n  private WEBP_SIGNATURE = Bytes['W'.ord, 'E'.ord, 'B'.ord, 'P'.ord, read_only: true]\n  private SIGNATURE      = Bytes['V'.ord, 'P'.ord, '8'.ord, read_only: true]\n\n  private enum Format\n    Lossy    = 0x20\n    Lossless = 0x4c\n    Extended = 0x58\n  end\n\n  # Based on https://github.com/php/php-src/blob/95da6e807a948039d3a42defbd849c4fed6cbe35/ext/standard/image.c#L100.\n  def self.extract(io : IO) : AIS::Image?\n    buffer = Bytes.new 18\n    io.read_fully buffer\n\n    return unless buffer[0, 3] == SIGNATURE\n\n    return unless format = Format.from_value? buffer[3]\n\n    width, height = case format\n                    in .lossless?\n                      {\n                        (buffer[9] + ((buffer[10] & 0x3F) << 8) + 1),\n                        ((buffer[10] >> 6) + (buffer[11] << 2) + ((buffer[12] & 0xF) << 10) + 1),\n                      }\n                    in .lossy?\n                      {\n                        (buffer[14] + ((buffer[15] & 0x3F) << 8)),\n                        (buffer[16] + ((buffer[17] & 0x3F) << 8)),\n                      }\n                    in .extended?\n                      {\n                        (buffer[12] + (buffer[13] << 8) + (buffer[14] << 16) + 1),\n                        (buffer[15] + (buffer[16] << 8) + (buffer[17] << 16) + 1),\n                      }\n                    end\n\n    Image.new width, height, :webp, 8\n  end\n\n  def self.matches?(io : IO, bytes : Bytes) : Bool\n    bytes[0, 4] == RIFF_SIGNATURE && bytes[8, 4] == WEBP_SIGNATURE\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/image.cr",
    "content": "# Represents information related to a processed image.\n#\n# ```\n# pp AIS::Image.from_file_path \"spec/images/jpeg/436x429_8_3.jpeg\" # =>\n# # Athena::ImageSize::Image(\n# # @bits=8,\n# # @channels=3,\n# # @format=JPEG,\n# # @height=429,\n# # @width=436)\n# ```\nstruct Athena::ImageSize::Image\n  # Returns the width of this image in pixels.\n  getter width : Int32\n\n  # Returns the width of this image in pixels.\n  getter height : Int32\n\n  # Returns the number of [bits per pixel](https://en.wikipedia.org/wiki/Color_depth) within this image, if available.\n  getter bits : Int32?\n\n  # Returns the number of [channels](https://en.wikipedia.org/wiki/Channel_(digital_image)) within this image, if available.\n  getter channels : Int32?\n\n  # Returns the format of this image.\n  getter format : Athena::ImageSize::Image::Format\n\n  protected def initialize(width : Int, height : Int, @format : AIS::Image::Format, bits : Int? = nil, channels : Int? = nil)\n    @width = width.to_i\n    @height = height.to_i\n    @bits = bits.try &.to_i\n    @channels = channels.try &.to_i\n  end\n\n  # Attempts to process the image at the provided *path*,\n  # raising an exception if either the images fails to process or is an unsupported format.\n  def self.from_file_path(path : String | Path) : self\n    self.from_io File.open path\n  end\n\n  # Attempts to process the image at the provided *path*,\n  # returning `nil` if either the images fails to process or is an unsupported format.\n  def self.from_file_path?(path : String | Path) : self?\n    self.from_io? File.open path\n  end\n\n  # Attempts to process the image from the provided *io*,\n  # raising an exception if either the images fails to process or is an unsupported format.\n  def self.from_io(io : IO) : self\n    if extractor_type = AIS::Extractors::Extractor.from_io io\n      return extractor_type.extract(io) || raise \"Failed to parse image.\"\n    end\n\n    raise \"Unsupported image format.\"\n  end\n\n  # Attempts to process the image from the provided *io*,\n  # returning `nil` if either the images fails to process or is an unsupported format.\n  def self.from_io?(io : IO) : self?\n    if extractor_type = AIS::Extractors::Extractor.from_io io\n      return extractor_type.extract io\n    end\n  ensure\n    io.close\n  end\n\n  # Returns a tuple of this images size in the format of `{width, height}`.\n  def size : Tuple(Int32, Int32)\n    {@width, @height}\n  end\nend\n"
  },
  {
    "path": "src/components/image_size/src/image_format.cr",
    "content": "struct Athena::ImageSize::Image; end\n\n# Enumerates the supported image formats.\nenum Athena::ImageSize::Image::Format\n  APNG\n  BMP\n  CUR\n  GIF\n  ICO\n  JPEG\n  MNG\n  PNG\n  PSD\n  SVG\n  SWF\n  TIFF\n  WEBP\nend\n"
  },
  {
    "path": "src/components/mercure/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/mercure/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/mercure/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.1.0] - 2026-04-19\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/mercure/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/mercure/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/mercure/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2024 George Dietrich\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": "src/components/mercure/README.md",
    "content": "# Mercure\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/mercure.svg)](https://github.com/athena-framework/mercure/releases)\n\nAllows easily pushing updates to web browsers and other HTTP clients using the Mercure protocol.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Mercure).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/mercure/docs/README.md",
    "content": "The `Athena::Mercure` component allows easily pushing updates to web browsers and other HTTP clients using the [Mercure protocol](https://mercure.rocks/docs/mercure).\nBecause it is built on top of [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), Mercure is supported out of the box in modern browsers.\n\nMercure comes with an authorization mechanism, automatic reconnection in case of network issues with retrieving of lost updates, a presence API, \"connection-less\" push for smartphones and auto-discoverability (a supported client can automatically discover and subscribe to updates of a given resource thanks to a specific HTTP header).\n\nUnlike the Crystal stdlib's [HTP::WebSocketHandler](https://crystal-lang.org/api/HTTP/WebSocketHandler.html), Mercure relies on a centralized hub to manage the persistent SSE connections with the client(s) as opposed to connecting directly to the Crystal HTTP server.\n\n```mermaid\nflowchart LR\n\n  %% Publishers\n  subgraph Publishers\n    P1[\"Athena app\"]\n    P2[\"Other HTTP service\"]\n  end\n\n  %% Mercure Hub\n  H[\"Mercure Hub\"]\n\n  %% Subscribers\n  subgraph Subscribers\n    S1[\"Browser client JavaScript\"]\n    S2[\"Mobile app React Native\"]\n    S3[\"Other HTTP client\"]\n  end\n\n  %% Flows from publishers to hub\n  P1 -->|HTTP POST| H\n  P2 -->|HTTP POST| H\n\n  %% Flows from hub to subscribers\n  H -->|SSE| S1\n  H -->|SSE| S2\n  H -->|SSE| S3\n```\n\nUltimately this makes the interactions/usage of it simpler since the majority of the complex parts are abstracted away.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-mercure:\n    github: athena-framework/mercure\n    version: ~> 0.1.0\n```\n\n### Setup\n\nBecause the Mercure Hub is a separate process from the Athena HTTP server, it does mean you have to [install](https://mercure.rocks/docs/hub/install) a Mercure hub by yourself.\nFor production usages, an official and open source (AGPL) hub based on the Caddy web server can be downloaded as a static binary from [Mercure.rocks](https://mercure.rocks).\nA Docker image, a Helm chart for Kubernetes and a managed, High Availability Hub are also provided.\n\nLocally, it's easiest to run the Hub via [docker compose](https://docs.docker.com/compose).\nA minimal development compose file would look like:\n\n```yaml\nservices:\n  mercure:\n    image: dunglas/mercure\n    restart: unless-stopped\n    environment:\n      SERVER_NAME: ':80' # Disable HTTPS for local dev\n      MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'\n      MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'\n      MERCURE_EXTRA_DIRECTIVES: |\n        cors_origins http://localhost:3000 # Allow Athena Server\n    command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile # Enable dev mode\n    ports:\n      - '80:80'\n    volumes:\n      - mercure_data:/data\n      - mercure_config:/config\n\nvolumes:\n  mercure_data:\n  mercure_config:\n```\n\n## Usage\n\nNow that the Mercure hub is running, we can use it to publish updates, and subscribe to receive those updates on the client side.\n\n### Publishing\n\nIn order to publish an update, a [AMC::Hub](/Mercure/Hub/) instance is required.\nThis type expects to be provided a URL to the Mercure Hub that updates should be sent to, and an [AMC::TokenProvider::Interface](/Mercure/TokenProvider/Interface/) instance.\nThe token provider is responsible for returning a JWT token used to authenticate the request sent to the Mercure Hub.\nMost commonly this'll be generated using a static secret key via the [Crystal JWT](https://github.com/crystal-community/jwt) shard.\n\nAn [AMC::Update](/Mercure/Update/) instance should then be instantiated that represents the update to publish, and provided to the `#publish` method of the hub instance.\nA complete example of this flow is as follows:\n\n```crystal\ntoken_factory = AMC::TokenFactory::JWT.new ENV[\"MERCURE_JWT_SECRET\"]\n\n# Use `*` to give the created JWT access to all topics.\ntoken_provider = AMC::TokenProvider::Factory.new token_factory, publish: [\"*\"]\n\nhub = AMC::Hub.new ENV[\"MERCURE_URL\"], token_provider, token_factory\n\nupdate = AMC::Update.new(\n  \"https://example.com/my-topic\",\n  {message: \"Hello world @ #{Time.local}!\"}.to_json\n)\n\nhub.publish update # => urn:uuid:e1ee88e2-532a-4d6f-ba70-f0f8bd584022\n```\n\nMultiple hubs can be used and accessed by name via a [AMC::Hub::Registry](/Mercure/Hub/Registry/).\n\n### Subscribing\n\nUpdates can be subscribed to on any platform that supports [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).\nFor example via JS:\n\n```html\n<!doctype html>\n<html>\n  <body>\n    <script type=\"application/javascript\">\n      const url = new URL(\"http://localhost/.well-known/mercure\");\n      url.searchParams.append(\"topic\", \"https://example.com/my-topic\");\n\n      const eventSource = new EventSource(url);\n\n      console.log(\"listening...\");\n      eventSource.onmessage = (e) => console.log(e);\n    </script>\n\n    <h2>Mercure Testing</h2>\n  </body>\n</html>\n```\nThis code would log each received update to the console.\nBe sure to call `eventSource.close()` when no longer needed to avoid a resource leak.\n\n### Authorization\n\nMercure allows dispatching updates to only authorized clients.\nTo do so, mark an [AMC::Update](/Mercure/Update/) as `private` via the third constructor argument, the `private` named argument:\n\n```crystal\nAMC::Update.new(\n  \"https://example.com/books/1\",\n  {status: \"OutOfStock\"}.to_json,\n  private: true\n)\n```\n\nTo subscribe to private updates, subscribers must provide to the Hub a JWT containing a topic selector matching by the topic of the update.\nThe preferred way of providing the JWT in a browser context is via a cookie.\n\nWARNING: To use the cookies, the Athena app and the Mercure Hub must be served from the same domain (can be different sub-domains).\n\nThe Mercure component provides [AMC::Authorization](/Mercure/Authorization/) that can handle generating/setting the cookie given a request/response.\nCookies set by this helper class are automatically passed by the browser to the Mercure hub if the `withCredentials` attribute of `EventSource` is set to `true`:\n\n```js\nconst eventSource = new EventSource(url, { withCredentials: true });\n```\n\n### Discovery\n\nMercure comes with the ability to automatically discover the hub via a [Link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link) header.\n\n```mermaid\nsequenceDiagram\n\n  participant C as Client\n  participant A as Athena API\n  participant H as Mercure Hub\n\n  C->>A: GET resource\n  A-->>C: 200 OK resource\n  A-->>C: Link header rel mercure\n\n  Note over C: Discover hub URL\n  Note over C: Add topic parameter\n\n  C->>H: Open SSE connection\n  H-->>C: Updates for topic\n```\n\nThe header may be added using the [AMC::Discovery](/Mercure/Discovery/) type.\nThe client would then be able to extract the hub URL from the `Link` header to be able to subscribe to updates related to that resource:\n\n```js\n// Fetch the original resource served by the Athena web API\nfetch('/books/1') // Has Link: <https://hub.example.com/.well-known/mercure>; rel=\"mercure\"\n  .then(response => {\n    // Extract the hub URL from the Link header\n    const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\\s+rel=(?:mercure|\"[^\"]*mercure[^\"]*\")/)[1];\n\n    // Append the topic(s) to subscribe as query parameter\n    const hub = new URL(hubUrl, window.origin);\n    hub.searchParams.append('topic', 'https://example.com/books/{id}');\n\n    // Subscribe to updates\n    const eventSource = new EventSource(hub);\n    eventSource.onmessage = event => console.log(event.data);\n  });\n```\n\n### Testing\n\nThe Mercure component comes with some helper types for testing code that publishes updates, without actually sending the update.\nSee [AMC::Spec](/Mercure/Spec/) for more information.\n\n```crystal\nrequire \"athena-mercure/spec\"\n\nhub = AMC::Spec::MockHub.new(\"https://foo.com\", AMC::TokenProvider::Static.new(\"JWT\")) { \"id\" }\n\n# ...\n```\n"
  },
  {
    "path": "src/components/mercure/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Mercure\nsite_url: https://athenaframework.org/Mercure/\nrepo_url: https://github.com/athena-framework/mercure\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-mercure/src/athena-mercure.cr\n            - ./lib/athena-mercure/src/spec.cr\n          source_locations:\n            lib/athena-mercure: https://github.com/athena-framework/mercure/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/mercure/shard.yml",
    "content": "name: athena-mercure\n\nversion: 0.1.0\n\ncrystal: ~> 1.13\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/mercure\n\ndocumentation: https://athenaframework.org/Mercure\n\ndescription: |\n  Allows easily pushing updates to web browsers and other HTTP clients using the Mercure protocol.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\ndependencies:\n  jwt:\n    github: crystal-community/jwt\n    version: ~> 1.7\n"
  },
  {
    "path": "src/components/mercure/spec/authorization_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct AuthorizationTest < ASPEC::TestCase\n  def test_jwt_lifetime : Nil\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      \"https://example.com/.well-known/mercure\",\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: AMC::TokenFactory::JWT.new(\"looooooooooooongenoughtestsecret\", jwt_lifetime: 4000)\n    ) { \"ID\" })\n\n    authorization = AMC::Authorization.new registry\n    cookie = authorization.create_cookie ::HTTP::Request.new(\"GET\", \"https://example.com\", headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n\n    payload, _ = JWT.decode(cookie.value, verify: false, validate: false)\n    payload[\"exp\"].as_i?.should be_a Int32\n  end\n\n  def test_set_cookie_zero_expiration : Nil\n    token_factory = AMC::Spec::AssertingTokenFactory.new(\n      \"JWT\",\n      [\"foo\"],\n      [\"bar\"],\n      {\"x-foo\" => \"baz\"},\n    )\n\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      \"https://example.com/.well-known/mercure\",\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: token_factory\n    ) { \"ID\" })\n\n    request = ::HTTP::Request.new(\"GET\", \"https://example.com\", headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n    response = ::HTTP::Server::Response.new IO::Memory.new\n\n    authorization = AMC::Authorization.new registry, Time::Span.zero, :lax\n    authorization.set_cookie request, response, [\"foo\"], [\"bar\"], {\"x-foo\" => \"baz\"}\n    token_factory.called?.should be_true\n\n    cookie = response.cookies.first\n    cookie.max_age.should eq Time::Span.zero\n    cookie.value.should_not be_empty\n    cookie.samesite.try &.lax?.should be_true\n  end\n\n  def test_set_cookie_default_expiration : Nil\n    token_factory = AMC::Spec::AssertingTokenFactory.new(\n      \"JWT\",\n      [\"foo\"],\n      [\"bar\"],\n      {\"x-foo\" => \"baz\"},\n    )\n\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      \"https://example.com/.well-known/mercure\",\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: token_factory\n    ) { \"ID\" })\n\n    request = ::HTTP::Request.new(\"GET\", \"https://example.com\", headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n    response = ::HTTP::Server::Response.new IO::Memory.new\n\n    authorization = AMC::Authorization.new registry, cookie_samesite: :lax\n    authorization.set_cookie request, response, [\"foo\"], [\"bar\"], {\"x-foo\" => \"baz\"}\n    token_factory.called?.should be_true\n\n    cookie = response.cookies.first\n    cookie.max_age.should eq 1.hour\n    cookie.value.should_not be_nil\n    cookie.samesite.try &.lax?.should be_true\n  end\n\n  def test_clear_cookie : Nil\n    token_factory = AMC::Spec::AssertingTokenFactory.new(\"JWT\")\n\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      \"https://example.com/.well-known/mercure\",\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: token_factory\n    ) { \"ID\" })\n\n    request = ::HTTP::Request.new(\"GET\", \"https://example.com\", headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n    response = ::HTTP::Server::Response.new IO::Memory.new\n\n    authorization = AMC::Authorization.new registry\n    authorization.clear_cookie request, response\n\n    cookie = response.cookies.first\n    cookie.value.should be_empty\n    cookie.max_age.should eq 1.second\n  end\n\n  @[DataProvider(\"applicable_cookie_domains\")]\n  def test_applicable_cookie_domains(expected : String?, hub_url : String, request_url : String) : Nil\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      hub_url,\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: AMC::TokenFactory::JWT.new(\"looooooooooooongenoughtestsecret\", jwt_lifetime: 4000)\n    ) { \"ID\" })\n\n    uri = URI.parse request_url\n    request = ::HTTP::Request.new(\"GET\", uri.path, headers: ::HTTP::Headers{\"host\" => uri.hostname || \"\"})\n\n    authorization = AMC::Authorization.new registry\n\n    cookie = authorization.create_cookie request\n    cookie.domain.should eq expected\n  end\n\n  def applicable_cookie_domains : Tuple\n    {\n      {\".example.com\", \"https://foo.bar.baz.example.com\", \"https://foo.bar.baz.qux.example.com\"},\n      {\".foo.bar.baz.example.com\", \"https://mercure.foo.bar.baz.example.com\", \"https://app.foo.bar.baz.example.com\"},\n      {\"example.com\", \"https://demo.example.com\", \"https://example.com\"},\n      {\".example.com\", \"https://mercure.example.com\", \"https://app.example.com\"},\n      {\".example.com\", \"https://example.com/.well-known/mercure\", \"https://app.example.com\"},\n      {nil, \"https://example.com/.well-known/mercure\", \"https://example.com\"},\n    }\n  end\n\n  @[DataProvider(\"nonapplicable_cookie_domains\")]\n  def test_nonapplicable_cookie_domains(hub_url : String, request_url : String) : Nil\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      hub_url,\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: AMC::TokenFactory::JWT.new(\"looooooooooooongenoughtestsecret\", jwt_lifetime: 4000)\n    ) { \"ID\" })\n\n    uri = URI.parse request_url\n    request = ::HTTP::Request.new(\"GET\", uri.path, headers: ::HTTP::Headers{\"host\" => uri.hostname || \"\"})\n\n    authorization = AMC::Authorization.new registry\n\n    expect_raises AMC::Exception::InvalidArgument, \"Unable to create authorization cookie for a hub on the different second-level domain\" do\n      authorization.create_cookie request\n    end\n  end\n\n  def nonapplicable_cookie_domains : Tuple\n    {\n      {\"https://demo.mercure.com\", \"https://example.com\"},\n      {\"https://mercure.internal.com\", \"https://external.com\"},\n    }\n  end\n\n  def test_set_multiple_cookies : Nil\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      \"https://example.com/.well-known/mercure\",\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: AMC::TokenFactory::JWT.new(\"looooooooooooongenoughtestsecret\", jwt_lifetime: 4000)\n    ) { \"ID\" })\n\n    request = ::HTTP::Request.new(\"GET\", \"https://example.com\", headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n    response = ::HTTP::Server::Response.new IO::Memory.new\n\n    authorization = AMC::Authorization.new registry\n\n    expect_raises AMC::Exception::Runtime, \"The 'mercureAuthorization' cookie for the 'default hub' has already been set. You cannot set it two times during the same request.\" do\n      authorization.set_cookie request, response\n      authorization.clear_cookie request, response\n    end\n  end\n\n  def test_nil_cookie_topics : Nil\n    token_factory = AMC::Spec::AssertingTokenFactory.new(\n      \"JWT\",\n      nil,\n      nil,\n      {\"x-foo\" => \"baz\"},\n    )\n\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      \"https://example.com/.well-known/mercure\",\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: token_factory\n    ) { \"ID\" })\n\n    request = ::HTTP::Request.new(\"GET\", \"https://example.com\", headers: ::HTTP::Headers{\"host\" => \"example.com\"})\n    response = ::HTTP::Server::Response.new IO::Memory.new\n\n    authorization = AMC::Authorization.new registry\n    authorization.set_cookie request, response, nil, nil, {\"x-foo\" => \"baz\"}\n\n    cookie = response.cookies.first\n    cookie.value.should_not be_empty\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/spec/discovery_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe AMC::Discovery do\n  it \"preflight request\" do\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      \"https://example.com/.well-known/mercure\",\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: AMC::TokenFactory::JWT.new(\"looooooooooooongenoughtestsecret\", jwt_lifetime: 4000)\n    ) { \"ID\" })\n\n    request = ::HTTP::Request.new(\"OPTIONS\", \"/\", headers: ::HTTP::Headers{\"access-control-request-method\" => \"GET\"})\n    response = ::HTTP::Server::Response.new IO::Memory.new\n\n    discovery = AMC::Discovery.new registry\n    discovery.add_link request, response\n\n    response.headers[\"link\"]?.should be_nil\n  end\n\n  it \"non-preflight request\" do\n    registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(\n      \"https://example.com/.well-known/mercure\",\n      AMC::TokenProvider::Static.new(\"JWT\"),\n      token_factory: AMC::TokenFactory::JWT.new(\"looooooooooooongenoughtestsecret\", jwt_lifetime: 4000)\n    ) { \"ID\" })\n\n    request = ::HTTP::Request.new(\"POST\", \"/\")\n    response = ::HTTP::Server::Response.new IO::Memory.new\n\n    discovery = AMC::Discovery.new registry\n    discovery.add_link request, response\n\n    response.headers.get(\"link\").should eq [\"<https://example.com/.well-known/mercure>; rel=\\\"mercure\\\"\"]\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/spec/hub/hub_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate URL = \"https://demo.mercure.rocks/.well-known/mercure\"\n\nprivate class MockHTTPClient < ::HTTP::Client\n  setter exception : ::Exception? = nil\n\n  def post(path, headers : ::HTTP::Headers? = nil, *, form : String | IO) : ::HTTP::Client::Response\n    if ex = @exception\n      raise ex\n    end\n\n    path.should eq \"/.well-known/mercure\"\n    headers.should eq ::HTTP::Headers{\"authorization\" => \"Bearer FOO\"}\n    form.should eq \"topic=https%3A%2F%2Fdemo.mercure.rocks%2Fdemo%2Fbooks%2F1.jsonld&data=Hi+from+Athena%21&private=on&id=id&retry=3\"\n\n    ::HTTP::Client::Response.new :ok, \"ID\"\n  end\nend\n\ndescribe Athena::Mercure::Hub do\n  describe \"#publish\" do\n    it \"happy path\" do\n      provider = AMC::TokenProvider::Static.new \"FOO\"\n      hub = AMC::Hub.new URL, provider, http_client: MockHTTPClient.new URI.parse URL\n\n      hub.publish(AMC::Update.new(\n        \"https://demo.mercure.rocks/demo/books/1.jsonld\",\n        \"Hi from Athena!\",\n        true,\n        \"id\",\n        nil,\n        3\n      )).should eq \"ID\"\n    end\n\n    it \"network issue\" do\n      provider = AMC::TokenProvider::Static.new \"FOO\"\n      http_client = MockHTTPClient.new URI.parse URL\n      http_client.exception = ::Exception.new \"Oh noes\"\n\n      hub = AMC::Hub.new URL, provider, http_client: http_client\n\n      expect_raises AMC::Exception::Runtime, \"Failed to send an update.\" do\n        hub.publish(AMC::Update.new(\n          \"https://demo.mercure.rocks/demo/books/1.jsonld\",\n          \"Hi from Athena!\",\n          true,\n          \"id\",\n          nil,\n          3\n        ))\n      end\n    end\n  end\n\n  it \"#url\" do\n    AMC::Hub\n      .new(URL, AMC::TokenProvider::Static.new(\"FOO\"), http_client: MockHTTPClient.new URI.parse URL)\n      .url.should eq URL\n  end\n\n  it \"#publish\" do\n    foo_hub = AMC::Spec::MockHub.new(\"https://foo.com\", AMC::TokenProvider::Static.new(\"FOO\")) { \"foo\" }\n    foo_hub.publish(AMC::Update.new(\"https://demo.mercure.rocks/demo/books/1.jsonld\")).should eq \"foo\"\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/spec/hub/registry_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AMC::Hub::Interface do\n  describe \"#hub\" do\n    it \"explicit name\" do\n      foo_hub = AMC::Spec::MockHub.new(\"https://foo.com\", AMC::TokenProvider::Static.new(\"FOO\")) { \"foo\" }\n      bar_hub = AMC::Spec::MockHub.new(\"https://bar.com\", AMC::TokenProvider::Static.new(\"BAR\")) { \"bar\" }\n\n      registry = AMC::Hub::Registry.new foo_hub, {\"foo\" => foo_hub, \"bar\" => bar_hub} of String => AMC::Hub::Interface\n\n      registry.hub(\"bar\").should eq bar_hub\n    end\n\n    it \"default hub\" do\n      foo_hub = AMC::Spec::MockHub.new(\"https://foo.com\", AMC::TokenProvider::Static.new(\"FOO\")) { \"foo\" }\n      bar_hub = AMC::Spec::MockHub.new(\"https://bar.com\", AMC::TokenProvider::Static.new(\"BAR\")) { \"bar\" }\n\n      registry = AMC::Hub::Registry.new foo_hub, {\"foo\" => foo_hub, \"bar\" => bar_hub} of String => AMC::Hub::Interface\n\n      registry.hub.should eq foo_hub\n    end\n\n    it \"missing hub\" do\n      foo_hub = AMC::Spec::MockHub.new(\"https://foo.com\", AMC::TokenProvider::Static.new(\"FOO\")) { \"foo\" }\n\n      registry = AMC::Hub::Registry.new foo_hub, {\"foo\" => foo_hub} of String => AMC::Hub::Interface\n\n      expect_raises AMC::Exception::InvalidArgument, \"No hub named 'baz' is available.\" do\n        registry.hub \"baz\"\n      end\n    end\n  end\n\n  it \"#hubs\" do\n    foo_hub = AMC::Spec::MockHub.new(\"https://foo.com\", AMC::TokenProvider::Static.new(\"FOO\")) { \"foo\" }\n    bar_hub = AMC::Spec::MockHub.new(\"https://bar.com\", AMC::TokenProvider::Static.new(\"BAR\")) { \"bar\" }\n\n    registry = AMC::Hub::Registry.new foo_hub, hubs = {\"foo\" => foo_hub, \"bar\" => bar_hub} of String => AMC::Hub::Interface\n\n    registry.hubs.should eq hubs\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/spec/spec_helper.cr",
    "content": "require \"spec\"\n\nrequire \"athena-spec\"\n\nrequire \"../src/athena-mercure\"\nrequire \"../src/spec\"\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/mercure/spec/token_factory/jwt_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct JWTTest < ASPEC::TestCase\n  @[DataProvider(\"create_data\")]\n  def test_create(\n    secret : String,\n    algorithm : JWT::Algorithm,\n    subscribe : Array(String)?,\n    publish : Array(String)?,\n    additional_claims : Hash?,\n    expected_jwt : String,\n  ) : Nil\n    AMC::TokenFactory::JWT\n      .new(secret, algorithm, jwt_lifetime: nil)\n      .create(subscribe, publish, additional_claims).should eq expected_jwt\n  end\n\n  def create_data : Tuple\n    {\n      {\n        \"looooooooooooongenoughtestsecret\",\n        JWT::Algorithm::HS256,\n        nil,\n        nil,\n        nil,\n        \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7fX0.Nl9FuNHooqvulVq4efunVwwUBE_VUNr4JC0ivPoZvFM\",\n      },\n\n      {\n        \"looooooooooooongenoughtestsecret\",\n        JWT::Algorithm::HS256,\n        Array(String).new,\n        [\"*\"],\n        nil,\n        \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOltdfX0.ZTK3JhEKO1338LAgRMw6j0lkGRMoaZtU4EtGiAylAns\", # spellchecker:disable-line\n      },\n\n      {\n        \"looooooooooooooooooooooooooooongenoughtestsecret\",\n        JWT::Algorithm::HS384,\n        Array(String).new,\n        [\"*\"],\n        nil,\n        \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOltdfX0.ERwjuquA1VXjCx_Q05zHHIVWU40maCOLsu493IKD4osTk0l0bTs9t9S8_tgM32Ih\",\n      },\n\n      {\n        \"looooooooooooongenoughtestsecret\",\n        JWT::Algorithm::HS256,\n        Array(String).new,\n        [\"*\"],\n        {\n          \"mercure\" => {\n            \"publish\"   => [\"overridden\"],\n            \"subscribe\" => [\"overridden\"],\n            \"payload\"   => {\"foo\" => \"bar\"},\n          },\n        },\n        \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOltdfSwibWVyY3VyZSI6eyJwdWJsaXNoIjpbIm92ZXJyaWRkZW4iXSwic3Vic2NyaWJlIjpbIm92ZXJyaWRkZW4iXSwicGF5bG9hZCI6eyJmb28iOiJiYXIifX19.X9IUAOq-12pRpO5oNnwnQsdZPAQQfan83DpJI32IxlI\",\n      },\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/spec/token_provider/callable_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AMC::TokenProvider::Callable do\n  it \"block overload\" do\n    provider = AMC::TokenProvider::Callable.new do\n      \"FOO\"\n    end\n\n    provider.jwt.should eq \"FOO\"\n  end\n\n  it \"proc overload\" do\n    AMC::TokenProvider::Callable.new(-> { \"BAR\" }).jwt.should eq \"BAR\"\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/spec/token_provider/factory_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AMC::TokenProvider::Factory do\n  it \"returns the token\" do\n    AMC::TokenProvider::Factory\n      .new(\n        AMC::TokenFactory::JWT.new(\"looooooooooooongenoughtestsecret\", jwt_lifetime: nil),\n        [] of String,\n        [\"*\"]\n      )\n      .jwt.should eq \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOltdfX0.ZTK3JhEKO1338LAgRMw6j0lkGRMoaZtU4EtGiAylAns\" # spellchecker:disable-line\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/spec/token_provider/static_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AMC::TokenProvider::Static do\n  it \"returns the token\" do\n    AMC::TokenProvider::Static.new(\"FOO\").jwt.should eq \"FOO\"\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/athena-mercure.cr",
    "content": "require \"jwt\"\n\nrequire \"http/client\"\nrequire \"http/headers\"\n\nrequire \"./authorization\"\nrequire \"./discovery\"\nrequire \"./update\"\n\nrequire \"./exception/*\"\nrequire \"./hub/*\"\nrequire \"./token_provider/*\"\nrequire \"./token_factory/*\"\n\n# Convenience alias to make referencing `Athena::Mercure` types easier.\nalias AMC = Athena::Mercure\n\n# The `Athena::Mercure` component allows easily pushing updates to web browsers and other HTTP clients using the [Mercure protocol](https://mercure.rocks/docs/mercure).\n# Because it is built on top of [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), Mercure is supported out of the box in modern browsers.\n#\n# Mercure comes with an authorization mechanism, automatic reconnection in case of network issues with retrieving of lost updates, a presence API, \"connection-less\" push for smartphones and auto-discoverability (a supported client can automatically discover and subscribe to updates of a given resource thanks to a specific HTTP header).\nmodule Athena::Mercure\n  VERSION = \"0.1.0\"\n\n  # See `AMC::TokenFactory::Interface`\n  module TokenFactory; end\n\n  # See `AMC::TokenProvider::Interface`\n  module TokenProvider; end\n\n  # Both acts as a namespace for exceptions related to the `Athena::Mercure` component, as well as a way to check for exceptions from the component.\n  module Exception; end\nend\n"
  },
  {
    "path": "src/components/mercure/src/authorization.cr",
    "content": "# Helper class for adding the Mercure authorization cookie to an HTTP response in order to enable private updates.\n# See [Authorization](/Mercure/#authorization) for more information.\nclass Athena::Mercure::Authorization\n  private COOKIE_NAME = \"mercureAuthorization\"\n\n  def initialize(\n    @hub_registry : AMC::Hub::Registry,\n    @cookie_lifetime : Time::Span = 1.hour,\n    @cookie_samesite : ::HTTP::Cookie::SameSite = :strict,\n  ); end\n\n  # Sets the `mercureAuthorization` cookie on the provided *response* given the provided *request*, optionally for the provided *hub_name*.\n  # The JWT cookie value by default does not have access to publish or subscribe to any topic.\n  # Be sure to set the *subscribe* and *publish* arrays to the topics you want it to be able to interact with, or `[\"*\"]` to handle all topics.\n  # *additional_claims* may also be used to define additional claims to the JWT if needed.\n  def set_cookie(\n    request : ::HTTP::Request,\n    response : ::HTTP::Server::Response,\n    subscribe : Array(String)? = [] of String,\n    publish : Array(String)? = [] of String,\n    additional_claims : Hash? = nil,\n    hub_name : String? = nil,\n  ) : Nil\n    self.update_cookies request, response, hub_name, self.create_cookie(request, subscribe, publish, additional_claims, hub_name)\n  end\n\n  # Clears the Mercure cookie from the provided *response*, optionally for the provided *hub_name*.\n  def clear_cookie(\n    request : ::HTTP::Request,\n    response : ::HTTP::Server::Response,\n    hub_name : String? = nil,\n  ) : Nil\n    self.update_cookies request, response, hub_name, self.create_clear_cookie(request, hub_name)\n  end\n\n  # Returns a Mercure auth cookie given the provided *request* and optionally for the provided *hub_name*.\n  #\n  # The JWT cookie value by default does not have access to publish or subscribe to any topic.\n  # Be sure to set the *subscribe* and *publish* arrays to the topics you want it to be able to interact with, or `[\"*\"]` to handle all topics.\n  # *additional_claims* may also be used to define additional claims to the JWT if needed.\n  def create_cookie(\n    request : ::HTTP::Request,\n    subscribe : Array(String)? = [] of String,\n    publish : Array(String)? = [] of String,\n    additional_claims : Hash? = nil,\n    hub_name : String? = nil,\n  ) : ::HTTP::Cookie\n    hub = @hub_registry.hub hub_name\n    unless token_factory = hub.token_factory\n      raise AMC::Exception::InvalidArgument.new \"The hub '#{hub_name}' does not contain a token factory.\"\n    end\n\n    cookie_lifetime = @cookie_lifetime\n\n    if additional_claims && (cl = additional_claims[\"exp\"]?)\n      cookie_lifetime = case cl\n                        when String then cl.to_i.seconds\n                        when Number then cl.seconds\n                        else\n                          @cookie_lifetime\n                        end\n    end\n\n    token = token_factory.create subscribe, publish, additional_claims\n    uri = URI.parse hub.public_url\n\n    ::HTTP::Cookie.new(\n      COOKIE_NAME,\n      token,\n      uri.path || \"/\",\n      domain: self.cookie_domain(request, uri),\n      secure: true,\n      http_only: true,\n      samesite: @cookie_samesite,\n      max_age: cookie_lifetime\n    )\n  end\n\n  private def create_clear_cookie(request : ::HTTP::Request, hub_name : String? = nil) : ::HTTP::Cookie\n    hub = @hub_registry.hub hub_name\n    uri = URI.parse hub.public_url\n\n    ::HTTP::Cookie.new(\n      COOKIE_NAME,\n      \"\",\n      uri.path || \"/\",\n      domain: self.cookie_domain(request, uri),\n      secure: true,\n      http_only: true,\n      samesite: @cookie_samesite,\n      max_age: 1.second\n    )\n  end\n\n  private def cookie_domain(request : ::HTTP::Request, uri : URI) : String?\n    return unless uri_host = uri.host\n\n    cookie_domain = uri_host.downcase\n    host = request.hostname || \"\"\n\n    return if cookie_domain == host\n\n    if cookie_domain.ends_with? \".#{host}\"\n      return host\n    end\n\n    host_segments = host.split '.'\n\n    host_segments[0..-2].each_with_index do |_, idx|\n      current_domain = host_segments[idx..].join '.'\n\n      target = \".#{current_domain}\"\n\n      if current_domain == cookie_domain || cookie_domain.ends_with? target\n        return target\n      end\n    end\n\n    raise AMC::Exception::InvalidArgument.new \"Unable to create authorization cookie for a hub on the different second-level domain '#{cookie_domain}'.\"\n  end\n\n  private def update_cookies(\n    request : ::HTTP::Request,\n    response : ::HTTP::Server::Response,\n    hub_name : String?,\n    cookie : ::HTTP::Cookie,\n  ) : Nil\n    unless response.cookies[COOKIE_NAME]?.nil?\n      raise AMC::Exception::Runtime.new \"The 'mercureAuthorization' cookie for the '#{hub_name ? \"#{hub_name} hub\" : \"default hub\"}' has already been set. You cannot set it two times during the same request.\"\n    end\n\n    response.cookies << cookie\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/discovery.cr",
    "content": "# Allows for automatically discovering the Mercure hub via a [Link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link) header.\n# E.g. can be included with the response for a resource to allow clients to then extract the URL from the rel `mercure` header to subscribe to future updates for that resource.\n#\n# See [Discovery](/Mercure/#discovery) for more information.\nclass Athena::Mercure::Discovery\n  def initialize(\n    @hub_registry : AMC::Hub::Registry,\n  ); end\n\n  # Adds the mercure relation `link` header to the provided *response*, optionally for the provided *hub_name*.\n  def add_link(request : ::HTTP::Request, response : ::HTTP::Server::Response, hub_name : String? = nil) : Nil\n    return if self.preflight_request? request\n\n    hub = @hub_registry.hub hub_name\n\n    # TODO: Create WebLink component?\n    response.headers.add \"link\", self.generate_link(hub.public_url)\n  end\n\n  private def generate_link(url : String) : String\n    %(<#{url}>; rel=\"mercure\")\n  end\n\n  private def preflight_request?(request : ::HTTP::Request) : Bool\n    \"options\" == request.method.downcase && request.headers.has_key? \"access-control-request-method\"\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/exception/invalid_argument.cr",
    "content": "class Athena::Mercure::Exception::InvalidArgument < ArgumentError\n  include Athena::Mercure::Exception\nend\n"
  },
  {
    "path": "src/components/mercure/src/exception/runtime.cr",
    "content": "class Athena::Mercure::Exception::Runtime < RuntimeError\n  include Athena::Mercure::Exception\nend\n"
  },
  {
    "path": "src/components/mercure/src/hub/hub.cr",
    "content": "require \"./interface\"\n\n# Default implementation of `AMC::Hub::Interface`.\nclass Athena::Mercure::Hub\n  include Athena::Mercure::Hub::Interface\n\n  # :inherit:\n  getter token_provider : AMC::TokenProvider::Interface\n\n  # :inherit:\n  getter token_factory : AMC::TokenFactory::Interface?\n\n  @uri : URI\n  @public_url : String?\n  @http_client : ::HTTP::Client\n\n  def initialize(\n    url : String,\n    @token_provider : AMC::TokenProvider::Interface,\n    @token_factory : AMC::TokenFactory::Interface? = nil,\n    @public_url : String? = nil,\n    http_client : ::HTTP::Client? = nil,\n  )\n    @uri = URI.parse url\n    @http_client = http_client || ::HTTP::Client.new @uri\n  end\n\n  # :inherit:\n  def url : String\n    @uri.to_s\n  end\n\n  # :inherit:\n  def public_url : String\n    @public_url || @uri.to_s\n  end\n\n  # :inherit:\n  def publish(update : AMC::Update) : String\n    @http_client.post(\n      @uri.path,\n      headers: ::HTTP::Headers{\"authorization\" => \"Bearer #{@token_provider.jwt}\"},\n      form: URI::Params.build { |form| self.encode form, update }\n    ).body\n  rescue ex : ::Exception\n    raise AMC::Exception::Runtime.new \"Failed to send an update.\", cause: ex\n  end\n\n  private def encode(form : URI::Params::Builder, update : AMC::Update) : Nil\n    form.add \"topic\", update.topics\n    form.add \"data\", update.data\n\n    if update.private?\n      form.add \"private\", \"on\"\n    end\n\n    if id = update.id\n      form.add \"id\", id\n    end\n\n    if type = update.type\n      form.add \"type\", type\n    end\n\n    if retry = update.retry\n      form.add \"retry\", retry.to_s\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/hub/interface.cr",
    "content": "class Athena::Mercure::Hub; end\n\n# Represents the API that a Mercure hub instance must implement.\nmodule Athena::Mercure::Hub::Interface\n  # Returns the internal URL of this hub used to publish updates.\n  abstract def url : String\n\n  # Returns the public URL of this hub used to subscribe.\n  abstract def public_url : String\n\n  # Returns the `AMC::TokenProvider::Interface` associated with this hub.\n  abstract def token_provider : AMC::TokenProvider::Interface?\n\n  # Returns the `AMC::TokenFactory::Interface` associated with this hub.\n  abstract def token_factory : AMC::TokenFactory::Interface?\n\n  # Publishes the provided *update* to this hub.\n  abstract def publish(update : AMC::Update) : String\nend\n"
  },
  {
    "path": "src/components/mercure/src/hub/registry.cr",
    "content": "# The `AMC::Hub::Registry` can be used to store multiple `AMC::Hub` instances, accessing them by unique names.\n#\n# ```\n# foo_hub = hub = AMC::Hub.new ENV[\"FOO_HUB_MERCURE_URL\"], foo_token_provider, foo_token_factory\n# bar_hub = hub = AMC::Hub.new ENV[\"BAR_HUB_MERCURE_URL\"], bar_token_provider, bar_token_factory\n#\n# registry = AMC::Hub::Registry.new(\n#   foo_hub,\n#   {\n#     \"foo\" => foo_hub,\n#     \"bar\" => bar_hub,\n#   } of String => AMC::Hub::Interface\n# )\n#\n# registry.hub       # => (default foo_hub)\n# registry.hub \"bar\" # => (bar_hub)\n# ```\nclass Athena::Mercure::Hub::Registry\n  # Returns the mapping of hub names to their related instance.\n  getter hubs : Hash(String, AMC::Hub::Interface)\n\n  def initialize(\n    @default_hub : AMC::Hub::Interface,\n    @hubs : Hash(String, AMC::Hub::Interface) = {} of String => AMC::Hub::Interface,\n  ); end\n\n  # Returns the hub with the provided *name*, or the default one if no name was provided.\n  def hub(name : String? = nil) : AMC::Hub::Interface\n    return @default_hub if name.nil?\n\n    raise AMC::Exception::InvalidArgument.new \"No hub named '#{name}' is available.\" unless @hubs.has_key? name\n\n    @hubs[name]\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/spec.cr",
    "content": "# Provides helper types for testing `Athena::Mercure` related logic.\nmodule Athena::Mercure::Spec\n  # Similar to `AMC::Hub` but does not make any requests to a real Mercure hub.\n  # Instead, it accepts a block that can be used to make assertions against the related `AMC::Update`, and is expected to return the id of the related update.\n  struct MockHub\n    include Athena::Mercure::Hub::Interface\n\n    # :inherit:\n    getter url : String\n\n    # :inherit:\n    getter token_provider : AMC::TokenProvider::Interface\n\n    # :inherit:\n    getter token_factory : AMC::TokenFactory::Interface?\n\n    @publisher : Proc(AMC::Update, String)\n\n    def initialize(\n      @url : String,\n      @token_provider : AMC::TokenProvider::Interface,\n      @public_url : String? = nil,\n      @token_factory : AMC::TokenFactory::Interface? = nil,\n      &@publisher : AMC::Update -> String\n    )\n    end\n\n    # :inherit:\n    def public_url : String\n      @public_url || @url\n    end\n\n    # :inherit:\n    def publish(update : AMC::Update) : String\n      @publisher.call update\n    end\n  end\n\n  # An `AMC::TokenFactory::Interface` implementation that will assert it was called with the expected arguments to `#create`.\n  class AssertingTokenFactory\n    include AMC::TokenFactory::Interface\n\n    getter? called : Bool = false\n\n    def initialize(\n      @token : String,\n      @subscribe : Array(String)? = [] of String,\n      @publish : Array(String)? = [] of String,\n      @additional_claims : Hash(String, String) = {} of String => String,\n    )\n    end\n\n    def create(subscribe : Array(String) | ::Nil = [] of String, publish : Array(String) | ::Nil = [] of String, additional_claims : Hash | ::Nil = nil) : String\n      subscribe.should eq @subscribe\n      publish.should eq @publish\n      additional_claims.should eq @additional_claims\n\n      @token\n    ensure\n      @called = true\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/token_factory/interface.cr",
    "content": "# A token factory is responsible for creating the token used to authenticate requests to the Mercure hub.\nmodule Athena::Mercure::TokenFactory::Interface\n  # Returns a JWT token that has access to *subscribe* and *publish* to the provided topics.\n  # Optionally, *additional_claims* may be added to the JWT.\n  abstract def create(\n    subscribe : Array(String)? = [] of String,\n    publish : Array(String)? = [] of String,\n    additional_claims : Hash? = nil,\n  ) : String\nend\n"
  },
  {
    "path": "src/components/mercure/src/token_factory/jwt.cr",
    "content": "# A token factory implementation based on the [Crystal JWT](https://github.com/crystal-community/jwt) shard.\nstruct Athena::Mercure::TokenFactory::JWT\n  include Athena::Mercure::TokenFactory::Interface\n\n  # These make it easier to build the JSON in a type safe way vs dealing with merging hashes and such :shrug:\n  private record MercurePayload, subscribe : Array(String)?, publish : Array(String)? do\n    def to_json(builder : JSON::Builder) : Nil\n      builder.object do\n        if publish = @publish\n          builder.field \"publish\", publish\n        end\n\n        if subscribe = @subscribe\n          builder.field \"subscribe\", subscribe\n        end\n      end\n    end\n  end\n\n  private record Payload(T), mercure : MercurePayload, jwt_lifetime : Time::Span?, additional_claims : T do\n    def to_json(builder : JSON::Builder) : Nil\n      builder.object do\n        builder.field \"mercure\", @mercure\n\n        if (lifetime = @jwt_lifetime) && !@additional_claims.try &.has_key? \"exp\"\n          builder.field \"exp\", (Time.utc + lifetime).to_unix\n        end\n\n        additional_claims.try &.each do |k, v|\n          builder.field k, v\n        end\n      end\n    end\n  end\n\n  @jwt_lifetime : Time::Span?\n\n  def initialize(\n    @jwt_secret : String,\n    @algorithm : ::JWT::Algorithm = :hs256,\n    jwt_lifetime : Int32 | Time::Span | Nil = 3600,\n    @passphrase : String = \"\",\n  )\n    @jwt_lifetime = jwt_lifetime.is_a?(Int32) ? jwt_lifetime.seconds : jwt_lifetime\n  end\n\n  # :inherit:\n  def create(\n    subscribe : Array(String)? = [] of String,\n    publish : Array(String)? = [] of String,\n    additional_claims : Hash? = nil,\n  ) : String\n    ::JWT.encode(\n      Payload.new(MercurePayload.new(subscribe, publish), @jwt_lifetime, additional_claims),\n      @jwt_secret,\n      @algorithm\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/token_provider/callable.cr",
    "content": "require \"./interface\"\n\n# A token provider implementation that provides the JWT via the return value of a callback block.\nstruct Athena::Mercure::TokenProvider::Callable\n  include Athena::Mercure::TokenProvider::Interface\n\n  def self.new(&block : -> String) : self\n    new block\n  end\n\n  def initialize(@callback : Proc(String)); end\n\n  # :inherit:\n  def jwt : String\n    @callback.call\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/token_provider/factory.cr",
    "content": "require \"./interface\"\n\n# A token provider implementation that provides the JWT via an `AMC::TokenFactory::Interface` instance.\nstruct Athena::Mercure::TokenProvider::Factory\n  include Athena::Mercure::TokenProvider::Interface\n\n  def initialize(\n    @factory : AMC::TokenFactory::Interface,\n    @subscribe : Array(String) = [] of String,\n    @publish : Array(String) = [] of String,\n  ); end\n\n  # :inherit:\n  def jwt : String\n    @factory.create @subscribe, @publish\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/token_provider/interface.cr",
    "content": "# A token provider is responsible for providing the token used to authenticate requests to the Mercure hub.\nmodule Athena::Mercure::TokenProvider::Interface\n  # Returns the JWT token used to authenticate requests to the Mercure hub.\n  abstract def jwt : String\nend\n"
  },
  {
    "path": "src/components/mercure/src/token_provider/static.cr",
    "content": "# A token provider implementation that provides the JWT as a static value from the constructor.\nstruct Athena::Mercure::TokenProvider::Static\n  include Athena::Mercure::TokenProvider::Interface\n\n  def initialize(@token : String); end\n\n  # :inherit:\n  def jwt : String\n    @token\n  end\nend\n"
  },
  {
    "path": "src/components/mercure/src/update.cr",
    "content": "# Represents an update to publish.\n#\n# ```\n# AMC::Update.new(\n#   \"https://example.com/books/1\",\n#   {status: \"OutOfStock\"}.to_json\n# )\n# ```\n#\n# The topic may be any string, but is recommended it be an [IRI (Internationalized Resource Identifier)](https://datatracker.ietf.org/doc/html/rfc3987) that uniquely identifies the resource the update related to.\n# The data may also be any string, but will most commonly be JSON.\n#\n# ### Private Updates\n#\n# By default, an update would be sent to all subscribers listening on that topic.\n# However, if `private: true` is defined on the update, then it'll only be sent to subscribers who are authorized to receive it.\n# See [Authorization](/Mercure/#authorization) for more information.\nstruct Athena::Mercure::Update\n  # Returns the identifiers this update is associated with.\n  getter topics : Array(String)\n\n  # Returns the string content of the update.\n  getter data : String\n\n  # If `true`, the update will not be sent to subscribers who are not authorized to receive it.\n  getter? private : Bool\n\n  # Maps to the SSE's [id](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#id) property.\n  getter id : String?\n\n  # Maps to the SSE's [event](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event) property\n  getter type : String?\n\n  # Maps to the SSE's [retry](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry) property\n  getter retry : Int32?\n\n  def initialize(\n    topics : String | Array(String),\n    @data : String = \"\",\n    @private : Bool = false,\n    @id : String? = nil,\n    @type : String? = nil,\n    @retry : Int32? = nil,\n  )\n    @topics = topics.is_a?(String) ? [topics] : topics\n  end\nend\n"
  },
  {
    "path": "src/components/mercure-bundle/README.md",
    "content": "The source code for the Mercure Bundle is located in [src/bundles/mercure](../../bundles/mercure).\nThe documentation is located here due to limitations with the [Projects](https://squidfunk.github.io/mkdocs-material/plugins/projects/) MkDocs Material plugin.\nThis inconsistency will hopefully be resolved via Zensical's [Subprojects](https://zensical.org/about/roadmap/#subprojects) feature at some point in the future.\n"
  },
  {
    "path": "src/components/mercure-bundle/docs/README.md",
    "content": "The `Athena::MercureBundle` integrates the `Athena::Mercure` component into the Athena framework; abstracting away the setup down to just a couple configuration values.\n\n## Installation\n\nFirst, install the bundle by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-mercure_bundle:\n    github: athena-framework/mercure-bundle\n    version: ~> 0.1.0\n```\n\nThen, require it:\n```crystal\nrequire \"athena-mercure_bundle\"\n```\nThis automatically registers the bundle with the framework.\n\nFinally, configure it with at least one hub and required configuration:\n```crystal\nADI.configure({\n  mercure: {\n    hubs: {\n      default: {\n        url:        ENV[\"MERCURE_URL\"],\n        jwt: {\n          secret:    ENV[\"MERCURE_JWT_SECRET\"],\n          publish:   [\"*\"],\n          subscribe: [\"*\"],\n        },\n      },\n    },\n  },\n})\n```\nSee the bundle [Schema](/MercureBundle/Schema) for the full set of possible configuration options.\n\nIf multiple hubs are configured, they may be injected using the hub name as a constructor parameter typed to [AMC::Hub::Interface](/Mercure/Hub/Interface/).\nFor example `some_hub : AMC::Hub::Interface` assuming the key used in the `hubs` named tuple was `some_hub`.\n\n## Usage\n\nThe Mercure bundle brings [AMC::Hub](/Mercure/Hub/), [AMC::Authorization](/Mercure/Authorization/), and [AMC::Discovery](/Mercure/Discovery/) into the framework as injectable services, with event listeners that handle response headers and cookies automatically.\n\n### Publishing\n\nInject [AMC::Hub::Interface](/Mercure/Hub/Interface/) to publish updates from a controller:\n\n```crystal\n@[ARTA::Route(path: \"/broadcast\")]\nclass BroadcastController < ATH::Controller\n  def initialize(@hub : AMC::Hub::Interface); end\n\n  @[ARTA::Post(\"/\")]\n  def broadcast : Nil\n    @hub.publish AMC::Update.new(\n      \"https://example.com/books/1\",\n      {status: \"OutOfStock\"}.to_json\n    )\n  end\nend\n```\n\n### Authorization\n\nInject [ABM::Authorization](/MercureBundle/Authorization/) to set the `mercureAuthorization` cookie for [private updates](/Mercure/#authorization).\nThe [SetCookie](/MercureBundle/Listeners/SetCookie/) listener automatically adds the cookie to the response — there is no need to modify the response directly:\n\n```crystal\n@[ARTA::Route(path: \"/auth\")]\nclass AuthController < ATH::Controller\n  def initialize(@authorization : ABM::Authorization); end\n\n  @[ARTA::Get(\"/subscribe\")]\n  def subscribe(request : AHTTP::Request) : Nil\n    @authorization.set_cookie(\n      request,\n      subscribe: [\"https://example.com/books/{id}\"],\n    )\n  end\nend\n```\n\nSee [Authorization](/Mercure/#authorization) in the Mercure component docs for more on private updates and cookie-based auth.\n\n### Discovery\n\nInject [ABM::Discovery](/MercureBundle/Discovery/) to add the Mercure hub `Link` header to a response.\nThe [AddLinkHeader](/MercureBundle/Listeners/AddLinkHeader/) listener handles adding the header — there is no need to modify the response directly:\n\n```crystal\n@[ARTA::Route(path: \"/books\")]\nclass BookController < ATH::Controller\n  def initialize(@discovery : ABM::Discovery); end\n\n  @[ARTA::Get(\"/{id}\")]\n  def show(request : AHTTP::Request, id : Int32) : {id: Int32, title : String}\n    @discovery.add_link request\n\n    {id: id, title: \"Hello World!\"}\n  end\nend\n```\n\nThe response will include a `Link: <https://hub.example.com/.well-known/mercure>; rel=\"mercure\"` header.\nA client can then extract the hub URL to subscribe to updates for this resource:\n\n```js\nfetch('/books/1')\n  .then(response => {\n    const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\\s+rel=(?:mercure|\"[^\"]*mercure[^\"]*\")/)[1];\n\n    const hub = new URL(hubUrl, window.origin);\n    hub.searchParams.append('topic', 'https://example.com/books/{id}');\n\n    const eventSource = new EventSource(hub);\n    eventSource.onmessage = event => console.log(event.data);\n  });\n```\n\nSee [Discovery](/Mercure/#discovery) in the Mercure component docs for more on the discovery protocol.\n"
  },
  {
    "path": "src/components/mercure-bundle/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Mercure Bundle\nsite_url: https://athenaframework.org/MercureBundle/\nrepo_url: https://github.com/athena-framework/mercure-bundle\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-dependency_injection/src/athena-dependency_injection.cr\n            - ./lib/athena-http/src/athena-http.cr\n            - ./lib/athena-contracts/src/athena-contracts.cr\n            - ./lib/athena-event_dispatcher/src/athena-event_dispatcher.cr\n            - ./lib/athena-http_kernel/src/athena-http_kernel.cr\n            - ./lib/athena-mercure/src/athena-mercure.cr\n            - ./lib/athena-mercure_bundle/src/athena-mercure_bundle.cr\n          source_locations:\n            lib/athena-mercure_bundle: https://github.com/athena-framework/mercure-bundle/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/mime/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/mime/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/mime/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.2.1] - 2025-09-04\n\n### Added\n\n- Add fallback MIME types guesser based on stdlib `MIME` module ([#546]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.2.1]: https://github.com/athena-framework/mime/releases/tag/v0.2.1\n[#546]: https://github.com/athena-framework/athena/pull/546\n\n## [0.2.0] - 2025-05-14\n\n### Added\n\n- **Breaking:** Add `AMIME::Types` to more robustly handles MIME type/file extension/guessing ([#534]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/mime/releases/tag/v0.2.0\n[#534]: https://github.com/athena-framework/athena/pull/534\n\n## [0.1.0] - 2025-01-26\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/mime/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/mime/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/mime/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2024 George Dietrich\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": "src/components/mime/README.md",
    "content": "# MIME\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/mime.svg)](https://github.com/athena-framework/mime/releases)\n\nAllows manipulating MIME messages.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/MIME).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/mime/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.2.0\n\n### New system dependency\n\nIf using the component on a Unix or MSYS2 system, you will need to ensure you have the `libmagic` development package installed.\nRefer to your system's package manager for the exact package name/installation instructions.\n"
  },
  {
    "path": "src/components/mime/docs/README.md",
    "content": "The `Athena::MIME` component allows manipulating the MIME messages used to send emails and provides utilities related to MIME types.\nAdditionally it also exposes MIME guessing and MIME Type <=> file extension translations via the [AMIME::Types](/MIME/Types/) type.\n\n[MIME](https://en.wikipedia.org/wiki/MIME) (Multipurpose Internet Mail Extensions) is an Internet standard that extends the original basic format of emails to support features like:\n\n* Headers and text contents using non-ASCII characters;\n* Message bodies with multiple parts (e.g. HTML and plain text contents);\n* Non-text attachments: audio, video, images, PDF, etc.\n\nThe entire MIME standard is complex and huge, but this component abstracts all that complexity to provide two ways of creating MIME messages:\n\n* A high-level API based on the [AMIME::Email](/MIME/Email/) class to quickly create email messages with all the common features\n* A low-level API based on the [AMIME::Message](/MIME/Message/) class to have absolute control over every single part of the email message\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-mime:\n    github: athena-framework/mime\n    version: ~> 0.2.0\n```\n\n## Usage\n\nThe [AMIME::Email](/MIME/Email/) class provides fluent setters to allow constructing an email with the desired information:\n\n```crystal\nemail = AMIME::Email\n  .new\n  .from(\"me@example.com\")\n  .to(\"you@example.com\")\n  .cc(\"them@example.com\")\n  .bcc(\"other@example.com\")\n  .reply_to(\"me@example.com\")\n  .priority(:high)\n  .subject(\"Important Notification\")\n  .text(\"Lorem ipsum...\")\n  .html(\"<h1>Lorem ipsum</h1> <p>...</p>\")\n  .attach_from_path(\"/path/to/file.pdf\", \"my-attachment.pdf\")\n  .embed_from_path(\"/path/to/logo.png\")\n```\n\nSee the API docs for that type for more information.\nThis component only handles creating the email messages. From here you would need to pass it along to another shard/component to actually send it.\n\n### Creating Raw Email Messages\n\nFor most use cases, the `AMIME::Email` type would work just fine.\nHowever some applications may require total control over every part of the email.\n\nConsider a message that includes some HTMl and textual content, a single PNG embedded image, and a PDF file attachment.\nThe MIME standard allows constructing this message in different ways, but most commonly would be like:\n\n```txt\nmultipart/mixed\n├── multipart/related\n│   ├── multipart/alternative\n│   │   ├── text/plain\n│   │   └── text/html\n│   └── image/png\n└── application/pdf\n```\n\nThis is the purpose of each MIME message part:\n\n* `multipart/alternative`: used when two or more parts are alternatives of the same (or very similar) content. The preferred format must be added last.\n* `multipart/mixed`: used to send different content types in the same message, such as when attaching files.\n* `multipart/related`: used to indicate that each message part is a component of an aggregate whole. The most common usage is to display images embedded in the message contents.\n\nYou must keep all of the above in mind when using the low-level `AMIME::Message` class to construct an email.\n\n```crystal\nheaders = AMIME::Header::Collection\n  .new\n  .add_mailbox_list_header(\"from\", {\"me@example.com\"})\n  .add_mailbox_list_header(\"to\", {\"you@example.com\"})\n  .add_text_header(\"subject\", \"Important Notification\")\n\ntext_content = AMIME::Part::Text.new \"text content\"\nhtml_content = AMIME::Part::Text.new \"html content\", sub_type: \"html\"\nbody = AMIME::Part::Multipart::Alternative.new text_content, html_content\n\nemail = AMIME::Message.new headers, body\n```\n\nEmbedding images and attaching files is possible by creating the appropriate email multi parts:\n\n```crystal\nheaders = AMIME::Header::Collection\n  .new\n  .add_mailbox_list_header(\"from\", {\"me@example.com\"})\n  .add_mailbox_list_header(\"to\", {\"you@example.com\"})\n  .add_text_header(\"subject\", \"Important Notification\")\n\nembedded_image = AMIME::Part::Data.from_path \"#{__DIR__}/../spec/fixtures/mimetypes/test.gif\", content_type: \"image/png\"\nimage_cid = embedded_image.content_id\n\nattached_file = AMIME::Part::Data.from_path \"#{__DIR__}/../spec/fixtures/mimetypes/abc.csv\", content_type: \"image/png\"\n\ntext_content = AMIME::Part::Text.new \"text content\"\nhtml_content = AMIME::Part::Text.new %(<img src=\"cid:#{image_cid}\"/> <h1>Lorem ipsum</h1> <p>...</p>), nil, \"html\"\n\nbody_content = AMIME::Part::Multipart::Alternative.new text_content, html_content\nbody = AMIME::Part::Multipart::Related.new body_content, {embedded_image}\n\nmessage_parts = AMIME::Part::Multipart::Mixed.new body, attached_file\n\nemail = AMIME::Message.new headers, message_parts\n```\n"
  },
  {
    "path": "src/components/mime/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: MIME\nsite_url: https://athenaframework.org/MIME/\nrepo_url: https://github.com/athena-framework/mime\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-mime/src/athena-mime.cr\n          source_locations:\n            lib/athena-mime: https://github.com/athena-framework/mime/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/mime/shard.yml",
    "content": "name: athena-mime\n\nversion: 0.2.1\n\ncrystal: ~> 1.4\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/mime\n\ndocumentation: https://athenaframework.org/MIME\n\ndescription: |\n  Allows manipulating MIME messages.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\nlibraries:\n  libmagic: '*'\n"
  },
  {
    "path": "src/components/mime/spec/abstract_types_guesser_test_case.cr",
    "content": "require \"./spec_helper\"\n\nabstract struct AbstractTypesGuesserTestCase < ASPEC::TestCase\n  protected abstract def guesser : AMIME::TypesGuesserInterface\n\n  def test_guess_directory : Nil\n    expect_raises AMIME::Exception::InvalidArgument, \"The file '#{__DIR__}/fixtures/mimetypes/directory' does not exist or is not readable.\" do\n      self.guesser.guess_mime_type \"#{__DIR__}/fixtures/mimetypes/directory\"\n    end\n  end\n\n  def test_guess_incorrect_path : Nil\n    expect_raises AMIME::Exception::InvalidArgument, \"The file '#{__DIR__}/fixtures/mimetypes/not_here' does not exist or is not readable.\" do\n      self.guesser.guess_mime_type \"#{__DIR__}/fixtures/mimetypes/not_here\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/address_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct AddressTest < ASPEC::TestCase\n  def test_address_only : Nil\n    a = AMIME::Address.new \"contact@athenï.org\"\n    a.address.should eq \"contact@athenï.org\"\n    a.to_s.should eq \"contact@xn--athen-gta.org\"\n    a.encoded_address.should eq \"contact@xn--athen-gta.org\"\n  end\n\n  def test_address_and_name : Nil\n    a = AMIME::Address.new \"contact@athenï.org\", \"George\"\n    a.address.should eq \"contact@athenï.org\"\n    a.name.should eq \"George\"\n    a.to_s.should eq %(\"George\" <contact@xn--athen-gta.org>)\n    a.encoded_address.should eq \"contact@xn--athen-gta.org\"\n  end\n\n  def test_create : Nil\n    a = AMIME::Address.new \"contact@athenaframework.org\"\n    b = AMIME::Address.new \"george@athenaframework.org\", \"George\"\n\n    AMIME::Address.create(a).should eq a\n    AMIME::Address.create(b).should eq b\n  end\n\n  def test_create_invalid : Nil\n    expect_raises AMIME::Exception::InvalidArgument, \"Could not parse '<george@athenaframework' to a 'Athena::MIME::Address' instance.\" do\n      AMIME::Address.create \"<george@athenaframework\"\n    end\n  end\n\n  def test_create_multiple : Nil\n    foo = AMIME::Address.new \"foo@example.com\"\n    bar = AMIME::Address.new \"bar@example.com\"\n    baz = AMIME::Address.new \"baz@example.com\"\n\n    AMIME::Address.create_multiple(\"foo@example.com\", \"bar@example.com\", baz).should eq [foo, bar, baz]\n  end\n\n  def test_unicode_local_part : Nil\n    # dømi means example and is reserved by the `.fo` registry # spellchecker:disable-line\n    AMIME::Address.new(\"info@dømi.fo\").has_unicode_local_part?.should be_false # spellchecker:disable-line\n    AMIME::Address.new(\"dømi@dømi.fo\").has_unicode_local_part?.should be_true  # spellchecker:disable-line\n  end\n\n  @[TestWith(\n    {\"example@example.com\", \"\", \"example@example.com\"},\n    {\"<example@example.com>\", \"\", \"example@example.com\"},\n    {\"Jane Doe <example@example.com>\", \"Jane Doe\", \"example@example.com\"},\n    {\"Jane Doe<example@example.com>\", \"Jane Doe\", \"example@example.com\"},\n    {\"'Jane Doe' <example@example.com>\", \"Jane Doe\", \"example@example.com\"},\n    {\"\\\"Jane Doe\\\" <example@example.com>\", \"Jane Doe\", \"example@example.com\"},\n    {\"Jane Doe <\\\"ex<ample\\\"@example.com>\", \"Jane Doe\", \"\\\"ex<ample\\\"@example.com\"},\n    {\"Jane Doe <\\\"ex<amp>le\\\"@example.com>\", \"Jane Doe\", \"\\\"ex<amp>le\\\"@example.com\"},\n    {\"Jane Doe > <\\\"ex<am  p>le\\\"@example.com>\", \"Jane Doe >\", \"\\\"ex<am  p>le\\\"@example.com\"},\n    {\"Jane Doe <example@example.com>discarded\", \"Jane Doe\", \"example@example.com\"},\n  )]\n  def test_create_from_string(string : String, display_name : String, addr_spec : String) : Nil\n    address = AMIME::Address.create string\n    address.address.should eq addr_spec\n    address.name.should eq display_name\n\n    from_string_address = AMIME::Address.create address.to_s\n    from_string_address.address.should eq addr_spec\n    from_string_address.name.should eq display_name\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\" \"},\n    {\" \\r\\n \"},\n  )]\n  def test_empty_name(name : String) : Nil\n    mail = \"mail@example.com\"\n\n    AMIME::Address.new(mail, name).to_s.should eq mail\n  end\n\n  def test_encode_name_if_contains_commas : Nil\n    AMIME::Address.new(\"foo@example.com\", \"Foo, \\\"Bar\").to_s.should eq %(\"Foo, \"Bar\" <foo@example.com>)\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/draft_email_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct DraftEmailTest < ASPEC::TestCase\n  def test_can_have_just_body : Nil\n    email = AMIME::DraftEmail.new.text(\"text content\").to_s\n\n    email.should contain \"text content\"\n    email.should contain \"mime-version: 1.0\"\n    email.should contain \"x-unsent: 1\"\n  end\n\n  def test_removes_bcc : Nil\n    email = AMIME::DraftEmail.new.text(\"text content\").bcc(\"foo@example.com\").to_s\n\n    email.should_not contain \"foo@example.com\"\n  end\n\n  def test_must_have_body : Nil\n    expect_raises AMIME::Exception::Logic, \"A message must have a text or an HTML part or attachments.\" do\n      AMIME::DraftEmail.new.to_s\n    end\n  end\n\n  def test_ensure_validity_always_fails : Nil\n    expect_raises AMIME::Exception::Logic, \"Cannot send messages marked as 'draft'.\" do\n      AMIME::DraftEmail.new.text(\"text content\").to(\"you@example.com\").from(\"me@example.com\").ensure_validity!\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/email_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct EmailTest < ASPEC::TestCase\n  def test_subject : Nil\n    e = AMIME::Email.new\n    e.subject \"Subject\"\n    e.subject.should eq \"Subject\"\n  end\n\n  def test_date : Nil\n    e = AMIME::Email.new\n    e.date now = Time.utc\n    e.date.should eq now\n  end\n\n  def test_return_path : Nil\n    e = AMIME::Email.new\n    e.return_path \"foo@example.com\"\n    e.return_path.should eq AMIME::Address.new(\"foo@example.com\")\n  end\n\n  def test_sender : Nil\n    e = AMIME::Email.new\n    e.sender \"foo@example.com\"\n    e.sender.should eq AMIME::Address.new \"foo@example.com\"\n\n    e.sender s = AMIME::Address.new(\"bar@example.com\")\n    e.sender.should eq s\n  end\n\n  def test_from : Nil\n    e = AMIME::Email.new\n    helene = AMIME::Address.new \"helene@example.com\"\n    thomas = AMIME::Address.new \"thomas@example.com\"\n    caramel = AMIME::Address.new \"caramel@example.com\"\n\n    e.from.should be_empty\n\n    e.from \"fred@example.com\", helene, thomas\n\n    v = e.from\n    v.size.should eq 3\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n\n    e.add_from \"lucas@example.com\", caramel\n\n    v = e.from\n    v.size.should eq 5\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n    v[3].should eq AMIME::Address.new \"lucas@example.com\"\n    v[4].should eq caramel\n\n    e = AMIME::Email.new\n    e.add_from \"lucas@example.com\", caramel\n\n    v = e.from\n    v.size.should eq 2\n    v[0].should eq AMIME::Address.new \"lucas@example.com\"\n    v[1].should eq caramel\n\n    e = AMIME::Email.new\n    e.from \"lucas@example.com\"\n    e.from caramel\n\n    v = e.from\n    v.size.should eq 1\n    v[0].should eq caramel\n  end\n\n  def test_reply_to : Nil\n    e = AMIME::Email.new\n    helene = AMIME::Address.new \"helene@example.com\"\n    thomas = AMIME::Address.new \"thomas@example.com\"\n    caramel = AMIME::Address.new \"caramel@example.com\"\n\n    e.reply_to.should be_empty\n\n    e.reply_to \"fred@example.com\", helene, thomas\n\n    v = e.reply_to\n    v.size.should eq 3\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n\n    e.add_reply_to \"lucas@example.com\", caramel\n\n    v = e.reply_to\n    v.size.should eq 5\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n    v[3].should eq AMIME::Address.new \"lucas@example.com\"\n    v[4].should eq caramel\n\n    e = AMIME::Email.new\n    e.add_reply_to \"lucas@example.com\", caramel\n\n    v = e.reply_to\n    v.size.should eq 2\n    v[0].should eq AMIME::Address.new \"lucas@example.com\"\n    v[1].should eq caramel\n\n    e = AMIME::Email.new\n    e.reply_to \"lucas@example.com\"\n    e.reply_to caramel\n\n    v = e.reply_to\n    v.size.should eq 1\n    v[0].should eq caramel\n  end\n\n  def test_to : Nil\n    e = AMIME::Email.new\n    helene = AMIME::Address.new \"helene@example.com\"\n    thomas = AMIME::Address.new \"thomas@example.com\"\n    caramel = AMIME::Address.new \"caramel@example.com\"\n\n    e.to.should be_empty\n\n    e.to \"fred@example.com\", helene, thomas\n\n    v = e.to\n    v.size.should eq 3\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n\n    e.add_to \"lucas@example.com\", caramel\n\n    v = e.to\n    v.size.should eq 5\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n    v[3].should eq AMIME::Address.new \"lucas@example.com\"\n    v[4].should eq caramel\n\n    e = AMIME::Email.new\n    e.add_to \"lucas@example.com\", caramel\n\n    v = e.to\n    v.size.should eq 2\n    v[0].should eq AMIME::Address.new \"lucas@example.com\"\n    v[1].should eq caramel\n\n    e = AMIME::Email.new\n    e.to \"lucas@example.com\"\n    e.to caramel\n\n    v = e.to\n    v.size.should eq 1\n    v[0].should eq caramel\n  end\n\n  def test_cc : Nil\n    e = AMIME::Email.new\n    helene = AMIME::Address.new \"helene@example.com\"\n    thomas = AMIME::Address.new \"thomas@example.com\"\n    caramel = AMIME::Address.new \"caramel@example.com\"\n\n    e.cc.should be_empty\n\n    e.cc \"fred@example.com\", helene, thomas\n\n    v = e.cc\n    v.size.should eq 3\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n\n    e.add_cc \"lucas@example.com\", caramel\n\n    v = e.cc\n    v.size.should eq 5\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n    v[3].should eq AMIME::Address.new \"lucas@example.com\"\n    v[4].should eq caramel\n\n    e = AMIME::Email.new\n    e.add_cc \"lucas@example.com\", caramel\n\n    v = e.cc\n    v.size.should eq 2\n    v[0].should eq AMIME::Address.new \"lucas@example.com\"\n    v[1].should eq caramel\n\n    e = AMIME::Email.new\n    e.cc \"lucas@example.com\"\n    e.cc caramel\n\n    v = e.cc\n    v.size.should eq 1\n    v[0].should eq caramel\n  end\n\n  def test_bcc : Nil\n    e = AMIME::Email.new\n    helene = AMIME::Address.new \"helene@example.com\"\n    thomas = AMIME::Address.new \"thomas@example.com\"\n    caramel = AMIME::Address.new \"caramel@example.com\"\n\n    e.bcc.should be_empty\n\n    e.bcc \"fred@example.com\", helene, thomas\n\n    v = e.bcc\n    v.size.should eq 3\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n\n    e.add_bcc \"lucas@example.com\", caramel\n\n    v = e.bcc\n    v.size.should eq 5\n    v[0].should eq AMIME::Address.new \"fred@example.com\"\n    v[1].should eq helene\n    v[2].should eq thomas\n    v[3].should eq AMIME::Address.new \"lucas@example.com\"\n    v[4].should eq caramel\n\n    e = AMIME::Email.new\n    e.add_bcc \"lucas@example.com\", caramel\n\n    v = e.bcc\n    v.size.should eq 2\n    v[0].should eq AMIME::Address.new \"lucas@example.com\"\n    v[1].should eq caramel\n\n    e = AMIME::Email.new\n    e.bcc \"lucas@example.com\"\n    e.bcc caramel\n\n    v = e.bcc\n    v.size.should eq 1\n    v[0].should eq caramel\n  end\n\n  def test_priority : Nil\n    e = AMIME::Email.new\n    e.priority.should eq AMIME::Email::Priority::NORMAL\n\n    e.priority :high\n    e.priority.should eq AMIME::Email::Priority::HIGH\n\n    e.priority AMIME::Email::Priority.new(123)\n    e.priority.should eq AMIME::Email::Priority::NORMAL\n  end\n\n  def test_raises_when_body_is_empty : Nil\n    expect_raises AMIME::Exception::Logic, \"A message must have a text or an HTML part or attachments.\" do\n      AMIME::Email.new.body\n    end\n  end\n\n  def test_body : Nil\n    e = AMIME::Email.new\n    e.body = text = AMIME::Part::Text.new \"content\"\n    e.body.should eq text\n  end\n\n  def test_generate_body_with_text_only : Nil\n    text = AMIME::Part::Text.new \"text content\"\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.text \"text content\"\n    e.body.should eq text\n    e.text_body.should eq \"text content\"\n  end\n\n  def test_generate_body_with_html_only : Nil\n    text = AMIME::Part::Text.new \"html content\", sub_type: \"html\"\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.html \"html content\"\n    e.body.should eq text\n    e.html_body.should eq \"html content\"\n  end\n\n  def test_generate_body_with_text_and_html : Nil\n    text = AMIME::Part::Text.new \"text content\"\n    html = AMIME::Part::Text.new \"html content\", sub_type: \"html\"\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.text \"text content\"\n    e.html \"html content\"\n    e.body.should eq AMIME::Part::Multipart::Alternative.new(text, html)\n  end\n\n  def test_generate_body_with_text_and_html_non_utf8 : Nil\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.text \"text content\", \"iso-8859-1\"\n    e.html \"html content\", \"iso-8859-1\"\n\n    e.text_charset.should eq \"iso-8859-1\"\n    e.html_charset.should eq \"iso-8859-1\"\n\n    e.body.should eq AMIME::Part::Multipart::Alternative.new(\n      AMIME::Part::Text.new(\"text content\", \"iso-8859-1\"),\n      AMIME::Part::Text.new(\"html content\", \"iso-8859-1\", \"html\"),\n    )\n  end\n\n  def test_generate_body_with_text_content_and_attachment : Nil\n    text, _, file_part, file, _, _ = self.generate_some_parts\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.add_part AMIME::Part::Data.new(file)\n    e.text \"text content\"\n\n    e.body.should eq AMIME::Part::Multipart::Mixed.new text, file_part\n  end\n\n  def test_generate_body_with_html_content_and_attachment : Nil\n    _, html, file_part, file, _, _ = self.generate_some_parts\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.add_part AMIME::Part::Data.new(file)\n    e.html \"html content\"\n\n    e.body.should eq AMIME::Part::Multipart::Mixed.new html, file_part\n  end\n\n  def test_generate_body_with_html_content_and_inlined_image_not_reference : Nil\n    _, html, _, _, _, _ = self.generate_some_parts\n    image_part = AMIME::Part::Data.new image = ::File.open(\"#{__DIR__}/fixtures/mimetypes/test.gif\", \"r\")\n    image_part.as_inline\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.add_part AMIME::Part::Data.new(image).as_inline\n    e.html \"html content\"\n\n    e.body.should eq AMIME::Part::Multipart::Mixed.new(html, image_part)\n  end\n\n  def test_generate_body_attached_file_only : Nil\n    _, _, file_part, file, _, _ = self.generate_some_parts\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.add_part AMIME::Part::Data.new file\n\n    e.body.should eq AMIME::Part::Multipart::Mixed.new file_part\n  end\n\n  def test_generate_body_inline_image_only : Nil\n    image_part = AMIME::Part::Data.new image = ::File.open(\"#{__DIR__}/fixtures/mimetypes/test.gif\", \"r\")\n    image_part.as_inline\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.add_part AMIME::Part::Data.new(image).as_inline\n\n    e.body.should eq AMIME::Part::Multipart::Mixed.new image_part\n  end\n\n  def test_generate_body_with_text_and_html_content_and_attachment : Nil\n    text, html, file_part, file, _, _ = self.generate_some_parts\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.text \"text content\"\n    e.html \"html content\"\n    e.add_part AMIME::Part::Data.new file\n\n    e.body.should eq AMIME::Part::Multipart::Mixed.new(AMIME::Part::Multipart::Alternative.new(text, html), file_part)\n  end\n\n  def test_generate_body_with_text_and_html_content_and_attachment_and_attached_image_not_referenced : Nil\n    text, html, file_part, file, image_part, image = self.generate_some_parts\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.text \"text content\"\n    e.html \"html content\"\n    e.add_part AMIME::Part::Data.new(file)\n    e.add_part AMIME::Part::Data.new(image, \"test.gif\")\n\n    e.body.should eq AMIME::Part::Multipart::Mixed.new(AMIME::Part::Multipart::Alternative.new(text, html), file_part, image_part)\n  end\n\n  def test_generate_body_with_text_and_attached_file_and_attached_image_not_referenced : Nil\n    text, _, file_part, file, image_part, image = self.generate_some_parts\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.text \"text content\"\n    e.add_part AMIME::Part::Data.new(file)\n    e.add_part AMIME::Part::Data.new(image, \"test.gif\")\n\n    e.body.should eq AMIME::Part::Multipart::Mixed.new(text, file_part, image_part)\n  end\n\n  def test_generate_body_with_text_and_html_and_attached_file_and_attached_image_not_referenced_via_cid : Nil\n    text, _, file_part, file, image_part, image = self.generate_some_parts\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.html content = %(html content <img src=\"test.gif\">)\n    e.text \"text content\"\n    e.add_part AMIME::Part::Data.new(file)\n    e.add_part AMIME::Part::Data.new(image, \"test.gif\")\n    full_html = AMIME::Part::Text.new content, sub_type: \"html\"\n\n    e.body.should eq AMIME::Part::Multipart::Mixed.new(AMIME::Part::Multipart::Alternative.new(text, full_html), file_part, image_part)\n  end\n\n  def test_generate_body_with_text_and_html_and_attached_file_and_attached_image_referenced_via_cid : Nil\n    _, _, file_part, file, _, image = self.generate_some_parts\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.html %(html content <img src=\"cid:test.gif\">)\n    e.text \"text content\"\n    e.add_part AMIME::Part::Data.new(file)\n    e.add_part AMIME::Part::Data.new(image, \"test.gif\")\n\n    body = e.body.should be_a AMIME::Part::Multipart::Mixed\n    (related = body.parts).size.should eq 2\n\n    related_part = related[0].should be_a AMIME::Part::Multipart::Related\n    related[1].should eq file_part\n\n    (parts = related_part.parts).size.should eq 2\n\n    alt_part = parts[0].should be_a AMIME::Part::Multipart::Alternative\n    generated_html = alt_part.parts[1].should be_a AMIME::Part::Text\n    data_part = parts[1].should be_a AMIME::Part::Data\n\n    generated_html.body.should contain \"cid:#{data_part.content_id}\"\n  end\n\n  def test_generate_body_with_text_and_html_and_attached_file_and_attached_image_referenced_via_cid_and_content_id : Nil\n    _, _, file_part, file, _, image = self.generate_some_parts\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.text \"text content\"\n    e.add_part AMIME::Part::Data.new file\n    img = AMIME::Part::Data.new image, \"test.gif\"\n    e.add_part img\n    e.html %(html content <img src=\"cid:#{img.content_id}\">)\n\n    body = e.body.should be_a AMIME::Part::Multipart::Mixed\n    (related_parts = body.parts).size.should eq 2\n\n    related_part = related_parts[0].should be_a AMIME::Part::Multipart::Related\n    related_parts[1].should eq file_part\n\n    (parts = related_part.parts).size.should eq 2\n    parts[0].should be_a AMIME::Part::Multipart::Alternative\n  end\n\n  def test_generate_body_with_html_and_inlined_image_twice_referenced_via_cid : Nil\n    # Inline image (twice) referenced in the HTML content\n    content = IO::Memory.new %(html content <img src=\"cid:test.gif\">)\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.html content\n\n    # Embedding the same image twice results in one image only in the email\n    image = ::File.open \"#{__DIR__}/fixtures/mimetypes/test.gif\", \"r\"\n    e.add_part AMIME::Part::Data.new(image, \"test.gif\").as_inline\n    e.add_part AMIME::Part::Data.new(image, \"test.gif\").as_inline\n\n    body = e.body.should be_a AMIME::Part::Multipart::Related\n\n    # 2 parts only, not 3 (text + 1 embedded image)\n    (parts = body.parts).size.should eq 2\n    parts[0].body_to_s.should match /html content <img src=3D\"cid:\\w+@athena\">/\n\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.html %(<div background=\"cid:test.gif\"></div>)\n    e.add_part AMIME::Part::Data.new(image, \"test.gif\").as_inline\n\n    body = e.body.should be_a AMIME::Part::Multipart::Related\n    (parts = body.parts).size.should eq 2\n    parts[0].body_to_s.should match /<div background=3D\"cid:\\w+@athena\"><\\/div>/\n  end\n\n  def test_attachments : Nil\n    # Inline part\n    contents = ::File.read path = \"#{__DIR__}/fixtures/mimetypes/test\"\n    data_part = AMIME::Part::Data.new file = ::File.open(path), \"test\"\n    inline = AMIME::Part::Data.new(contents, \"test\").as_inline\n\n    e = AMIME::Email.new\n    e.add_part AMIME::Part::Data.new file, \"test\"\n    e.add_part AMIME::Part::Data.new(contents, \"test\").as_inline\n    e.attachments.should eq [data_part, inline]\n\n    # Inline part from path\n    data_part = AMIME::Part::Data.from_path path, \"test\"\n    inline = AMIME::Part::Data.from_path(path, \"test\").as_inline\n    e = AMIME::Email.new\n    e.add_part AMIME::Part::Data.new AMIME::Part::File.new(path)\n    e.add_part AMIME::Part::Data.new(AMIME::Part::File.new(path)).as_inline\n\n    e.attachments.map(&.body_to_s).should eq [data_part.body_to_s, inline.body_to_s]\n    e.attachments.map(&.prepared_headers).should eq [data_part.prepared_headers, inline.prepared_headers]\n  end\n\n  def test_attachments_attach_helper_methods : Nil\n    # Inline part\n    contents = ::File.read path = \"#{__DIR__}/fixtures/mimetypes/test\"\n    data_part = AMIME::Part::Data.new file = ::File.open(path), \"test\"\n    inline = AMIME::Part::Data.new(contents, \"test\").as_inline\n\n    e = AMIME::Email.new\n    e.attach file, \"test\"\n    e.embed contents, \"test\"\n    e.attachments.should eq [data_part, inline]\n\n    # Inline part from path\n    data_part = AMIME::Part::Data.from_path path, \"test\"\n    inline = AMIME::Part::Data.from_path(path, \"test\").as_inline\n    e = AMIME::Email.new\n    e.attach_from_path path, \"test\"\n    e.embed_from_path path, \"test\"\n\n    e.attachments.map(&.body_to_s).should eq [data_part.body_to_s, inline.body_to_s]\n    e.attachments.map(&.prepared_headers).should eq [data_part.prepared_headers, inline.prepared_headers]\n  end\n\n  def test_body_cache_same : Nil\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.text \"text content\"\n\n    body1 = e.body\n    body2 = e.body\n\n    # Must be the same instance so that DKIM sig is the same\n    body1.should be body2\n  end\n\n  def test_body_cache_different : Nil\n    e = AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n    e.text \"text content\"\n    body1 = e.body\n    e.html \"<b>bar</b>\"\n    body2 = e.body\n\n    # Must not be the same due to the content changing\n    body1.should_not be body2\n  end\n\n  def test_ensure_validity : Nil\n    AMIME::Email.new\n      .from(\"me@example.com\")\n      .to(\"you@example.com\")\n      .text(\"content\")\n      .ensure_validity!\n  end\n\n  private def generate_some_parts : {AMIME::Part::Text, AMIME::Part::Text, AMIME::Part::Data, ::File, AMIME::Part::Data, ::File}\n    text = AMIME::Part::Text.new \"text content\"\n    html = AMIME::Part::Text.new \"html content\", sub_type: \"html\"\n    file_part = AMIME::Part::Data.new file = ::File.open \"#{__DIR__}/fixtures/mimetypes/test\", \"r\"\n    image_part = AMIME::Part::Data.new (image = ::File.open(\"#{__DIR__}/fixtures/mimetypes/test.gif\", \"r\")), \"test.gif\"\n\n    {text, html, file_part, file, image_part, image}\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/encoder/base64_content_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct Base64ContentEncoderTest < ASPEC::TestCase\n  def test_name : Nil\n    AMIME::Encoder::Base64Content.new.name.should eq \"base64\"\n  end\n\n  def test_encodes_string : Nil\n    AMIME::Encoder::Base64Content.new.encode(\"123\").should eq \"MTIz\\n\"               # spellchecker:disable-line\n    AMIME::Encoder::Base64Content.new.encode(\"123456\").should eq \"MTIzNDU2\\n\"        # spellchecker:disable-line\n    AMIME::Encoder::Base64Content.new.encode(\"123456789\").should eq \"MTIzNDU2Nzg5\\n\" # spellchecker:disable-line\n  end\n\n  def test_encodes_io : Nil\n    AMIME::Encoder::Base64Content.new.encode(IO::Memory.new \"123\").should eq \"MTIz\\n\"               # spellchecker:disable-line\n    AMIME::Encoder::Base64Content.new.encode(IO::Memory.new \"123456\").should eq \"MTIzNDU2\\n\"        # spellchecker:disable-line\n    AMIME::Encoder::Base64Content.new.encode(IO::Memory.new \"123456789\").should eq \"MTIzNDU2Nzg5\\n\" # spellchecker:disable-line\n  end\n\n  def test_pad_length : Nil\n    encoder = AMIME::Encoder::Base64Content.new\n\n    30.times do\n      input = String.build do |io|\n        io.write_byte rand 255_u8\n      end\n\n      # Two bytes of padding for a single byte\n      encoder.encode(input).should match /^[a-zA-Z0-9\\/+]{2}==$/\n    end\n\n    30.times do\n      input = String.build do |io|\n        io.write_byte rand 255_u8\n        io.write_byte rand 255_u8\n      end\n\n      # Two bytes has 1 byte of padding\n      encoder.encode(input).should match /^[a-zA-Z0-9\\/+]{3}=$/\n    end\n\n    30.times do\n      input = String.build do |io|\n        io.write_byte rand 255_u8\n        io.write_byte rand 255_u8\n        io.write_byte rand 255_u8\n      end\n\n      # Three bytes has no padding\n      encoder.encode(input).should match /^[a-zA-Z0-9\\/+]{4}$/\n    end\n  end\n\n  def test_max_line_length : Nil\n    input = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    AMIME::Encoder::Base64Content\n      .new\n      .encode(input)\n      .lines(chomp: false) # Use lines here to allow ignoring the typos\n      .should eq([\n        \"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJT\\n\",\n        \"VFVWV1hZWjEyMzQ1Njc4OTBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC\\n\", # spellchecker:disable-line\n        \"Q0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMTIzNDU2Nzg5MEFCQ0RFRkdISUpL\\n\", # spellchecker:disable-line\n        \"TE1OT1BRUlNUVVZXWFla\\n\",                                         # spellchecker:disable-line\n      ])\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/encoder/eight_bit_content_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct EightBitContentEncoderTest < ASPEC::TestCase\n  def test_name : Nil\n    AMIME::Encoder::EightBitContent.new.name.should eq \"8bit\"\n  end\n\n  def test_encodes_string : Nil\n    AMIME::Encoder::EightBitContent.new.encode(\"123\").should eq \"123\"\n    AMIME::Encoder::EightBitContent.new.encode(\"123456\").should eq \"123456\"\n    AMIME::Encoder::EightBitContent.new.encode(\"123456789\").should eq \"123456789\"\n  end\n\n  def test_encodes_io : Nil\n    AMIME::Encoder::EightBitContent.new.encode(IO::Memory.new \"123\").should eq \"123\"\n    AMIME::Encoder::EightBitContent.new.encode(IO::Memory.new \"123456\").should eq \"123456\"\n    AMIME::Encoder::EightBitContent.new.encode(IO::Memory.new \"123456789\").should eq \"123456789\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/encoder/idn_address_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct IDNAddressEncoderTest < ASPEC::TestCase\n  def test_encodes_string : Nil\n    AMIME::Encoder::IDNAddress.new.encode(\"test@fußball.test\").should eq \"test@xn--fuball-cta.test\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/encoder/quoted_printable_content_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct QuotedPrintableEncoderTest < ASPEC::TestCase\n  def test_quoted_printable_encode : Nil\n    AMIME::Encoder::QuotedPrintableContent.quoted_printable_encode(\"test\").should eq \"test\"\n    AMIME::Encoder::QuotedPrintableContent.quoted_printable_encode(\"this is a foo\").should eq \"this is a foo\"\n\n    AMIME::Encoder::QuotedPrintableContent.quoted_printable_encode(\"This is a sample string with special characters: ä, ö, ü, and ß.\").should eq <<-TXT\n      This is a sample string with special characters: =C3=A4, =C3=B6, =C3=BC, an=\\r\n      d =C3=9F.\n      TXT\n\n    AMIME::Encoder::QuotedPrintableContent.quoted_printable_encode(\"Iñtërnâtiônàlizætiøn☃💩\").should eq <<-TXT\n      I=C3=B1t=C3=ABrn=C3=A2ti=C3=B4n=C3=A0liz=C3=A6ti=C3=B8n=E2=98=83=\\r\n      =F0=9F=92=A9\n      TXT\n  end\n\n  def test_quoted_printable_encode_encodes_nul_values : Nil\n    AMIME::Encoder::QuotedPrintableContent\n      .quoted_printable_encode(\"\\0\" * 200).should eq <<-TXT\n        =00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=\\r\n        =00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=\\r\n        =00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=\\r\n        =00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=\\r\n        =00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=\\r\n        =00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=\\r\n        =00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=\\r\n        =00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00=00\n        TXT\n  end\n\n  def test_quoted_printable_encode_encodes_non_ascii\n    AMIME::Encoder::QuotedPrintableContent\n      .quoted_printable_encode(\"строка в юникоде\" * 50).should eq <<-TXT\n        =D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=\\r\n        =BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=\\r\n        =D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=\\r\n        =B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=\\r\n        =D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=\\r\n        =82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=\\r\n        =D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=\\r\n        =BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=\\r\n        =D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =\\r\n        =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=\\r\n        =BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=\\r\n        =D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=\\r\n        =B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=\\r\n        =D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=\\r\n        =8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=\\r\n        =B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=\\r\n        =D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=\\r\n        =81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=\\r\n        =D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=\\r\n        =B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =\\r\n        =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=\\r\n        =D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=\\r\n        =80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=\\r\n        =D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=\\r\n        =BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=\\r\n        =D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=\\r\n        =B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=\\r\n        =D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=\\r\n        =82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=\\r\n        =D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=\\r\n        =BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=\\r\n        =D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =\\r\n        =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=\\r\n        =BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=\\r\n        =D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=\\r\n        =B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=\\r\n        =D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=\\r\n        =8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=\\r\n        =B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=\\r\n        =D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=\\r\n        =81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=\\r\n        =D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=\\r\n        =B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =\\r\n        =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=\\r\n        =D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=\\r\n        =80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=\\r\n        =D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=\\r\n        =BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=\\r\n        =D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=\\r\n        =B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=\\r\n        =D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=\\r\n        =82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=\\r\n        =D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=\\r\n        =BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=\\r\n        =D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =\\r\n        =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=\\r\n        =BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5=D1=81=\\r\n        =D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=\\r\n        =B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=8E=D0=BD=D0=B8=\\r\n        =D0=BA=D0=BE=D0=B4=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0 =D0=B2 =D1=\\r\n        =8E=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5\n        TXT\n  end\n\n  def test_quoted_printable_encode_does_not_split_multibyte_chars_by_soft_break : Nil\n    AMIME::Encoder::QuotedPrintableContent\n      .quoted_printable_encode(\"\\xc4\\x85\" * 77).should eq <<-TXT\n        =C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=\\r\n        =C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=\\r\n        =C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=\\r\n        =C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=\\r\n        =C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=\\r\n        =C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=C4=85=\\r\n        =C4=85=C4=85=C4=85=C4=85=C4=85\n        TXT\n  end\n\n  def test_quoted_printable_encode_permitted_characters_are_not_encoded : Nil\n    ((33..60).to_a + (62..126).to_a).each do |ord|\n      char = ord.chr.to_s\n      AMIME::Encoder::QuotedPrintableContent.quoted_printable_encode(char).should eq char\n    end\n  end\n\n  def test_quoted_printable_encode_crlf_is_left_alone : Nil\n    string = \"a\\r\\nb\\r\\nc\\r\\n\"\n    AMIME::Encoder::QuotedPrintableContent.quoted_printable_encode(string).should eq string\n  end\n\n  def test_quoted_printable_encode_always_encodes_tabs : Nil\n    AMIME::Encoder::QuotedPrintableContent\n      .quoted_printable_encode(\"a\\t\\t\\r\\nb\")\n      .should eq \"a=09=09\\r\\nb\"\n  end\n\n  def test_quoted_printable_encode_encodes_space_before_newline : Nil\n    AMIME::Encoder::QuotedPrintableContent\n      .quoted_printable_encode(\"a  \\r\\nb\")\n      .should eq \"a =20\\r\\nb\"\n  end\n\n  def test_quoted_printable_encode_lines_longer_than_76_characters_are_soft_broken : Nil\n    AMIME::Encoder::QuotedPrintableContent.quoted_printable_encode(\"a\" * 140).should eq <<-TXT\n      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=\\r\\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n      TXT\n  end\n\n  def test_quoted_printable_encode_bytes_below_permitted_range_are_encoded : Nil\n    (0..31).each do |byte|\n      char = byte.chr.to_s\n\n      AMIME::Encoder::QuotedPrintableContent\n        .quoted_printable_encode(char)\n        .should eq sprintf(\"=%02X\", byte)\n    end\n\n    # Allows spaces\n    AMIME::Encoder::QuotedPrintableContent.quoted_printable_encode(\" \").should eq \" \"\n  end\n\n  def test_name : Nil\n    AMIME::Encoder::QuotedPrintableContent.new.name.should eq \"quoted-printable\"\n  end\n\n  def test_encode : Nil\n    AMIME::Encoder::QuotedPrintableContent.new.encode(\"test\").should eq \"test\"\n    AMIME::Encoder::QuotedPrintableContent.new.encode(\"this is a foo\").should eq \"this is a foo\"\n\n    AMIME::Encoder::QuotedPrintableContent.new.encode(\"This is a sample string with special characters: ä, ö, ü, and ß.\").should eq <<-TXT\n      This is a sample string with special characters: =C3=A4, =C3=B6, =C3=BC, an=\\r\n      d =C3=9F.\n      TXT\n\n    AMIME::Encoder::QuotedPrintableContent.new.encode(\"Iñtërnâtiônàlizætiøn☃💩\").should eq <<-TXT\n      I=C3=B1t=C3=ABrn=C3=A2ti=C3=B4n=C3=A0liz=C3=A6ti=C3=B8n=E2=98=83=\\r\n      =F0=9F=92=A9\n      TXT\n  end\n\n  def test_quoted_printable_encode_io : Nil\n    AMIME::Encoder::QuotedPrintableContent.new.encode(IO::Memory.new \"test\").should eq \"test\"\n    AMIME::Encoder::QuotedPrintableContent.new.encode(IO::Memory.new \"this is a foo\").should eq \"this is a foo\"\n\n    AMIME::Encoder::QuotedPrintableContent.new.encode(IO::Memory.new \"This is a sample string with special characters: ä, ö, ü, and ß.\").should eq <<-TXT\n      This is a sample string with special characters: =C3=A4, =C3=B6, =C3=BC, an=\\r\n      d =C3=9F.\n      TXT\n\n    AMIME::Encoder::QuotedPrintableContent.new.encode(\"Iñtërnâtiônàlizætiøn☃💩\").should eq <<-TXT\n      I=C3=B1t=C3=ABrn=C3=A2ti=C3=B4n=C3=A0liz=C3=A6ti=C3=B8n=E2=98=83=\\r\n      =F0=9F=92=A9\n      TXT\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/encoder/quoted_printable_mime_header_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct QuotedPrintableMIMEHeaderTest < ASPEC::TestCase\n  def test_name_is_q : Nil\n    AMIME::Encoder::QuotedPrintableMIMEHeader.new.name.should eq \"Q\"\n  end\n\n  def test_space_and_tab_never_appear : Nil\n    AMIME::Encoder::QuotedPrintableMIMEHeader\n      .new\n      .encode(\"a \\t b\")\n      .should_not match /[ \\t]/\n  end\n\n  def test_space_is_represented_by_underscore : Nil\n    AMIME::Encoder::QuotedPrintableMIMEHeader\n      .new\n      .encode(\"a b\")\n      .should eq \"a_b\"\n  end\n\n  def test_equals_and_question_underscore_are_encoded : Nil\n    AMIME::Encoder::QuotedPrintableMIMEHeader\n      .new\n      .encode(\"=?_\")\n      .should eq \"=3D=3F=5F\"\n  end\n\n  def test_parans_and_quotes_are_encoded : Nil\n    AMIME::Encoder::QuotedPrintableMIMEHeader\n      .new\n      .encode(\"(\\\")\")\n      .should eq \"=28=22=29\"\n  end\n\n  def test_only_chars_allowed_in_phrases_are_used : Nil\n    encoder = AMIME::Encoder::QuotedPrintableMIMEHeader.new\n\n    allowed_bytes = [] of Int32\n    allowed_bytes.concat ('a'..'z').map(&.ord)\n    allowed_bytes.concat ('A'..'Z').map(&.ord)\n    allowed_bytes.concat ('0'..'9').map(&.ord)\n    allowed_bytes.concat ['!'.ord, '*'.ord, '+'.ord, '-'.ord, '/'.ord]\n\n    (0x00_u8..0xFF_u8).each do |byte|\n      io = IO::Memory.new\n      io.write_byte byte\n      input = io.to_s\n\n      encoded = encoder.encode input\n\n      if allowed_bytes.includes? byte\n        encoded.should eq input\n      elsif ' '.ord == byte\n        # Special case\n        encoded.should eq \"_\"\n      else\n        encoded.should eq \"=#{byte.to_s base: 16, upcase: true, precision: 2}\"\n      end\n    end\n  end\n\n  def test_equals_never_appears_at_end_of_line : Nil\n    AMIME::Encoder::QuotedPrintableMIMEHeader\n      .new\n      .encode(\"a\" * 140)\n      .should eq <<-TXT\n        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\r\n        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n        TXT\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/encoder/rfc2231_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct RFC2231EncoderTest < ASPEC::TestCase\n  private RFC2245_TOKEN = Regex.new \"^[\\x21\\x23-\\x27\\x2A\\x2B\\x2D\\x2E\\x30-\\x39\\x41-\\x5A\\x5E-\\x7E]+$\", options: :dollar_endonly\n\n  def test_encoding_ascii_characters_produces_valid_token : Nil\n    string = String.build do |io|\n      (0x00_u8..0x7F_u8).each do |byte|\n        io.write_byte byte\n      end\n    end\n\n    encoded = AMIME::Encoder::RFC2231\n      .new\n      .encode(string)\n\n    encoded.split(\"\\r\\n\").each do |line|\n      line.should match RFC2245_TOKEN\n    end\n  end\n\n  def test_encoding_non_ascii_characters_produces_valid_token : Nil\n    string = String.build do |io|\n      (0x80_u8..0xFF_u8).each do |byte|\n        io.write_byte byte\n      end\n    end\n\n    encoded = AMIME::Encoder::RFC2231\n      .new\n      .encode(string)\n\n    encoded.split(\"\\r\\n\").each do |line|\n      line.should match RFC2245_TOKEN\n    end\n  end\n\n  def test_max_line_length_can_be_set : Nil\n    AMIME::Encoder::RFC2231\n      .new\n      .encode(\"a\" * 200, max_line_length: 75)\n      .should eq <<-TXT\n        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\r\n        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\r\n        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n        TXT\n  end\n\n  def test_first_line_can_have_shorter_length : Nil\n    AMIME::Encoder::RFC2231\n      .new\n      .encode(\"a\" * 200, first_line_offset: 24, max_line_length: 72)\n      .should eq <<-TXT\n        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\r\n        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\r\n        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\r\n        aaaaaaaa\n        TXT\n  end\n\n  @[TestWith(\n    {\"iso-2022-jp\", \"one.txt\"},\n    {\"iso-8859-1\", \"one.txt\"},\n    {\"utf-8\", \"one.txt\"},\n    {\"utf-8\", \"two.txt\"},\n    {\"utf-8\", \"three.txt\"},\n  )]\n  def test_encoding_and_decoding_samples(encoding : String, file : String) : Nil\n    encoder = AMIME::Encoder::RFC2231.new\n\n    text = File.read \"#{__DIR__}/../fixtures/samples/charsets/#{encoding}/#{file}\"\n    encoded_text = encoder.encode text, encoding\n\n    # Encoded string should decode back to original string\n    URI.decode(encoded_text.split(\"\\r\\n\").join).should eq text\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/fixtures/content.txt",
    "content": "content"
  },
  {
    "path": "src/components/mime/spec/fixtures/mimetypes/.unknownextension",
    "content": "f"
  },
  {
    "path": "src/components/mime/spec/fixtures/mimetypes/abc.csv",
    "content": "a,b,c\nd,e,f\ng,h,i\n"
  },
  {
    "path": "src/components/mime/spec/fixtures/mimetypes/directory/.empty",
    "content": ""
  },
  {
    "path": "src/components/mime/spec/fixtures/mimetypes/other-file.example",
    "content": ""
  },
  {
    "path": "src/components/mime/spec/fixtures/samples/charsets/iso-2022-jp/one.txt",
    "content": "ISO-2022-JPは、インターネット上(特に電子メール)などで使われる日本の文字用の文字符号化方式。ISO/IEC 2022のエスケープシーケンスを利用して文字集合を切り替える7ビットのコードであることを特徴とする (アナウンス機能のエスケープシーケンスは省略される)。俗に「JISコード」と呼ばれることもある。\n\n概要\n日本語表記への利用が想定されている文字コードであり、日本語の利用されるネットワークにおいて、日本の規格を応用したものである。また文字集合としては、日本語で用いられる漢字、ひらがな、カタカナはもちろん、ラテン文字、ギリシア文字、キリル文字なども含んでおり、学術や産業の分野での利用も考慮たものとなっている。規格名に、ISOの日本語の言語コードであるjaではなく、国・地域名コードのJPが示されているゆえんである。\n文字集合としてJIS X 0201のC0集合（制御文字）、JIS X 0201のラテン文字集合、ISO 646の国際基準版図形文字、JIS X 0208の1978年版（JIS C 6226-1978）と1983年および1990年版が利用できる。JIS X 0201の片仮名文字集合は利用できない。1986年以降、日本の電子メールで用いられてきたJUNETコードを、村井純・Mark Crispin・Erik van der Poelが1993年にRFC化したもの(RFC 1468)。後にJIS X 0208:1997の附属書2としてJISに規定された。MIMEにおける文字符号化方式の識別用の名前として IANA に登録されている。\nなお、符号化の仕様についてはISO/IEC 2022#ISO-2022-JPも参照。\n\nISO-2022-JPと非標準的拡張使用\n「JISコード」（または「ISO-2022-JP」）というコード名の規定下では、その仕様通りの使用が求められる。しかし、Windows OS上では、実際にはCP932コード (MicrosoftによるShift JISを拡張した亜種。ISO-2022-JP規定外文字が追加されている。）による独自拡張（の文字）を断りなく使うアプリケーションが多い。この例としてInternet ExplorerやOutlook Expressがある。また、EmEditor、秀丸エディタやThunderbirdのようなMicrosoft社以外のWindowsアプリケーションでも同様の場合がある。この場合、ISO-2022-JPの範囲外の文字を使ってしまうと、異なる製品間では未定義不明文字として認識されるか、もしくは文字化けを起こす原因となる。そのため、Windows用の電子メールクライアントであっても独自拡張の文字を使用すると警告を出したり、あえて使えないように制限しているものも存在する。さらにはISO-2022-JPの範囲内であってもCP932は非標準文字（FULLWIDTH TILDE等）を持つので文字化けの原因になり得る。\nまた、符号化方式名をISO-2022-JPとしているのに、文字集合としてはJIS X 0212 (いわゆる補助漢字) やJIS X 0201の片仮名文字集合 (いわゆる半角カナ) をも符号化している例があるが、ISO-2022-JPではこれらの文字を許容していない。これらの符号化は独自拡張の実装であり、中にはISO/IEC 2022の仕様に準拠すらしていないものもある[2]。従って受信側の電子メールクライアントがこれらの独自拡張に対応していない場合、その文字あるいはその文字を含む行、時にはテキスト全体が文字化けすることがある。\n\n"
  },
  {
    "path": "src/components/mime/spec/fixtures/samples/charsets/iso-8859-1/one.txt",
    "content": "Op mat eraus hinnen beschte, rou zënne schaddreg ké. Ké sin Eisen Kaffi prächteg, den haut esou Fielse wa, Well zielen d'Welt am dir. Aus grousse rëschten d'Stroos do, as dat Kléder gewëss d'Kàchen. Schied gehéiert d'Vioule net hu, rou ke zënter Säiten d'Hierz. Ze eise Fletschen mat, gei as gréng d'Lëtzebuerger. Wäit räich no mat.\n\nSäiten d'Liewen aus en. Un gëtt bléit lossen wee, da wéi alle weisen Kolrettchen. Et deser d'Pan d'Kirmes vun, en wuel Benn rëschten méi. En get drem ménger beschte, da wär Stad welle. Nun Dach d'Pied do, mä gét ruffen gehéiert. Ze onser ugedon fir, d'Liewen Plett'len ech no, si Räis wielen bereet wat. Iwer spilt fir jo.\n\nAn hin däischter Margréitchen, eng ke Frot brommt, vu den Räis néierens. Da hir Hunn Frot nozegon, rout Fläiß Himmel zum si, net gutt Kaffi Gesträich fu. Vill lait Gaart sou wa, Land Mamm Schuebersonndeg rei do. Gei geet Minutt en, gei d'Leit beschte Kolrettchen et, Mamm fergiess un hun.\n\nEt gutt Heck kommen oft, Lann rëscht rei um, Hunn rëscht schéinste ke der. En lait zielen schnéiwäiss hir, fu rou botze éiweg Minutt, rem fest gudden schaddreg en. Noper bereet Margréitchen mat op, dem denkt d'Leit d'Vioule no, oft ké Himmel Hämmel. En denkt blénken Fréijor net, Gart Schiet d'Natur no wou. No hin Ierd Frot d'Kirmes. Hire aremt un rou, ké den éiweg wielen Milliounen.\n\nMir si Hunn Blénkeg. Ké get ston derfir d'Kàchen. Haut d'Pan fu ons, dé frou löschteg d'Meereische rei. Sou op wuel Léift. Stret schlon grousse gin hu. Mä denkt d'Leit hinnen net, ké gét haut fort rëscht.\n\nKoum d'Pan hannendrun ass ké, ké den brét Kaffi geplot. Schéi Hären d'Pied fu gét, do d'Mier néierens bei. Rëm päift Hämmel am, wee Engel beschéngt mä. Brommt klinzecht der ke, wa rout jeitzt dén. Get Zalot d'Vioule däischter da, jo fir Bänk päift duerch, bei d'Beem schéinen Plett'len jo. Den haut Faarwen ze, eng en Biereg Kirmesdag, um sin alles Faarwen d'Vioule.\n\nEng Hunn Schied et, wat wa Frot fest gebotzt. Bei jo bleiwe ruffen Klarinett. Un Feld klinzecht gét, rifft Margréitchen rem ke. Mir dé Noper duurch gewëss, ston sech kille sin en. Gei Stret d'Wise um, Haus Gart wee as. Monn ménger an blo, wat da Gart gefällt Hämmelsbrot.\n\nBrommt geplot och ze, dat wa Räis Well Kaffi. Do get spilt prächteg, as wär kille bleiwe gewalteg. Onser frësch Margréitchen rem ke, blo en huet ugedon. Onser Hemecht wär de, hu eraus d'Sonn dat, eise deser hannendrun da och.\n\nAs durch Himmel hun, no fest iw'rem schéinste mir, Hunn séngt Hierz ke zum. Séngt iw'rem d'Natur zum an. Ke wär gutt Grénge. Kënnt gudden prächteg mä rei. Dé dir Blénkeg Klarinett Kolrettchen, da fort muerges d'Kanner wou, main Feld ruffen vu wéi. Da gin esou Zalot gewalteg, gét vill Hemecht blénken dé.\n\nHaut gréng nun et, nei vu Bass gréng d'Gaassen. Fest d'Beem uechter si gin. Oft vu sinn wellen kréien. Et ass lait Zalot schéinen."
  },
  {
    "path": "src/components/mime/spec/fixtures/samples/charsets/utf-8/one.txt",
    "content": "Код одно гринспана руководишь на. Его вы знания движение. Ты две начать\nодиночку, сказать основатель удовольствием но миф. Бы какие система тем.\nПолностью использует три мы, человек клоунов те нас, бы давать творческую\nэзотерическая шеф.\n\nМог не помнить никакого сэкономленного, две либо какие пишите бы. Должен\nкомпанию кто те, этот заключалась проектировщик не ты. Глупые периоды ты\nдля. Вам который хороший он. Те любых кремния концентрируются мог,\nсобирать принадлежите без вы.\n\nДжоэла меньше хорошего вы миф, за тем году разработки. Даже управляющим\nруководители был не. Три коде выпускать заботиться ну. То его система\nудовольствием безостановочно, или ты главной процессорах. Мы без джоэл\nзнания получат, статьи остальные мы ещё.\n\nНих русском касается поскольку по, образование должником\nсистематизированный ну мои. Прийти кандидата университет но нас, для бы\nдолжны никакого, биг многие причин интервьюирования за.\n\nТем до плиту почему. Вот учёт такие одного бы, об биг разным внешних\nпромежуток. Вас до какому возможностей безответственный, были погодите бы\nего, по них глупые долгий количества.\n"
  },
  {
    "path": "src/components/mime/spec/fixtures/samples/charsets/utf-8/three.txt",
    "content": "Αν ήδη διάβασε γλιτώσει μεταγλωτίσει, αυτήν θυμάμαι μου μα. Την κατάσταση χρησιμοποίησέ να! Τα διαφορά φαινόμενο διολισθήσεις πες, υψηλότερη προκαλείς περισσότερες όχι κι. Με ελέγχου γίνεται σας, μικρής δημιουργούν τη του. Τις τα γράψει εικόνες απαράδεκτη?\n\nΝα ότι πρώτοι απαραίτητο. Άμεση πετάνε κακόκεφος τον ώς, να χώρου πιθανότητες του. Το μέχρι ορίστε λιγότερους σας. Πω ναί φυσικά εικόνες.\n\nΜου οι κώδικα αποκλειστικούς, λες το μάλλον συνεχώς. Νέου σημεία απίστευτα σας μα. Χρόνου μεταγλωτιστής σε νέα, τη τις πιάνει μπορούσες προγραμματιστές. Των κάνε βγαίνει εντυπωσιακό τα? Κρατάει τεσσαρών δυστυχώς της κι, ήδη υψηλότερη εξακολουθεί τα?\n\nΏρα πετάνε μπορούσε λιγότερους αν, τα απαράδεκτη συγχωνευτεί ροή. Τη έγραψες συνηθίζουν σαν. Όλα με υλικό στήλες χειρότερα. Ανώδυνη δουλέψει επί ως, αν διαδίκτυο εσωτερικών παράγοντες από. Κεντρικό επιτυχία πες το.\n\nΠω ναι λέει τελειώσει, έξι ως έργων τελειώσει. Με αρχεία βουτήξουν ανταγωνιστής ώρα, πολύ γραφικά σελίδων τα στη. Όρο οέλεγχος δημιουργούν δε, ας θέλεις ελέγχου συντακτικό όρο! Της θυμάμαι επιδιόρθωση τα. Για μπορούσε περισσότερο αν, μέγιστη σημαίνει αποφάσισε τα του, άτομο αποτελέσει τι στα.\n\nΤι στην αφήσεις διοίκηση στη. Τα εσφαλμένη δημιουργια επιχείριση έξι! Βήμα μαγικά εκτελέσει ανά τη. Όλη αφήσεις συνεχώς εμπορικά αν, το λες κόλπα επιτυχία. Ότι οι ζώνη κειμένων. Όρο κι ρωτάει γραμμής πελάτες, τελειώσει διολισθήσεις καθυστερούσε αν εγώ? Τι πετούν διοίκηση προβλήματα ήδη.\n\nΤη γλιτώσει αποθηκευτικού μια. Πω έξι δημιουργια πιθανότητες, ως πέντε ελέγχους εκτελείται λες. Πως ερωτήσεις διοικητικό συγκεντρωμένοι οι, ας συνεχώς διοικητικό αποστηθίσει σαν. Δε πρώτες συνεχώς διολισθήσεις έχω, από τι κανένας βουτήξουν, γειτονιάς προσεκτικά ανταγωνιστής κι σαν.\n\nΔημιουργια συνηθίζουν κλπ τι? Όχι ποσοστό διακοπής κι. Κλπ φακέλους δεδομένη εξοργιστικά θα? Υποψήφιο καθορίζουν με όλη, στα πήρε προσοχή εταιρείες πω, ώς τον συνάδελφος διοικητικό δημιουργήσεις! Δούλευε επιτίθενται σας θα, με ένας παραγωγικής ένα, να ναι σημεία μέγιστη απαράδεκτη?\n\nΣας τεσσαρών συνεντεύξης τη, αρπάζεις σίγουρος μη για', επί τοπικές εντολές ακούσει θα? Ως δυστυχής μεταγλωτιστής όλη, να την είχαν σφάλμα απαραίτητο! Μην ώς άτομο διορθώσει χρησιμοποιούνταν. Δεν τα κόλπα πετάξαμε, μη που άγχος υόρκη άμεση, αφού δυστυχώς διακόψουμε όρο αν! Όλη μαγικά πετάνε επιδιορθώσεις δε, ροή φυσικά αποτελέσει πω.\n\nΆπειρα παραπάνω φαινόμενο πω ώρα, σαν πόρτες κρατήσουν συνηθίζουν ως. Κι ώρα τρέξει είχαμε εφαρμογή. Απλό σχεδιαστής μεταγλωτιστής ας επί, τις τα όταν έγραψες γραμμής? Όλα κάνεις συνάδελφος εργαζόμενοι θα, χαρτιού χαμηλός τα ροή. Ως ναι όροφο έρθει, μην πελάτες αποφάσισε μεταφραστής με, να βιαστικά εκδόσεις αναζήτησης λες. Των φταίει εκθέσεις προσπαθήσεις οι, σπίτι αποστηθίσει ας λες?\n\nΏς που υπηρεσία απαραίτητο δημιουργείς. Μη άρα χαρά καθώς νύχτας, πω ματ μπουν είχαν. Άμεση δημιουργείς ώς ροή, γράψει γραμμής σίγουρος στα τι! Αν αφού πρώτοι εργαζόμενων ναί.\n\nΆμεση διορθώσεις με δύο? Έχουν παράδειγμα των θα, μου έρθει θυμάμαι περισσότερο το. Ότι θα αφού χρειάζονται περισσότερες. Σαν συνεχώς περίπου οι.\n\nΏς πρώτης πετάξαμε λες, όρο κι πρώτες ζητήσεις δυστυχής. Ανά χρόνου διακοπή επιχειρηματίες ας, ώς μόλις άτομο χειρότερα όρο, κρατάει σχεδιαστής προσπαθήσεις νέο το. Πουλάς προσθέσει όλη πω, τύπου χαρακτηριστικό εγώ σε, πω πιο δούλευε αναζήτησης? Αναφορά δίνοντας σαν μη, μάθε δεδομένη εσωτερικών με ναι, αναφέρονται περιβάλλοντος ώρα αν. Και λέει απόλαυσε τα, που το όροφο προσπαθούν?\n\nΠάντα χρόνου χρήματα ναι το, σαν σωστά θυμάμαι σκεφτείς τα. Μα αποτελέσει ανεπιθύμητη την, πιο το τέτοιο ατόμου, τη των τρόπο εργαλείων επιδιόρθωσης. Περιβάλλον παραγωγικής σου κι, κλπ οι τύπου κακόκεφους αποστηθίσει, δε των πλέον τρόποι. Πιθανότητες χαρακτηριστικών σας κι, γραφικά δημιουργήσεις μια οι, πω πολλοί εξαρτάται προσεκτικά εδώ. Σταματάς παράγοντες για' ώς, στις ρωτάει το ναι! Καρέκλα ζητήσεις συνδυασμούς τη ήδη!\n\nΓια μαγικά συνεχώς ακούσει το. Σταματάς προϊόντα βουτήξουν ώς ροή. Είχαν πρώτες οι ναι, μα λες αποστηθίσει ανακαλύπτεις. Όροφο άλγεβρα παραπάνω εδώ τη, πρόσληψη λαμβάνουν καταλάθος ήδη ας? Ως και εισαγωγή κρατήσουν, ένας κακόκεφους κι μας, όχι κώδικάς παίξουν πω. Πω νέα κρατάει εκφράσουν, τότε τελικών τη όχι, ας της τρέξει αλλάζοντας αποκλειστικούς.\n\nΈνας βιβλίο σε άρα, ναι ως γράψει ταξινομεί διορθώσεις! Εδώ να γεγονός συγγραφείς, ώς ήδη διακόψουμε επιχειρηματίες? Ότι πακέτων εσφαλμένη κι, θα όρο κόλπα παραγωγικής? Αν έχω κεντρικό υψηλότερη, κι δεν ίδιο πετάνε παρατηρούμενη! Που λοιπόν σημαντικό μα, προκαλείς χειροκροτήματα ως όλα, μα επί κόλπα άγχος γραμμές! Δε σου κάνεις βουτήξουν, μη έργων επενδυτής χρησιμοποίησέ στα, ως του πρώτες διάσημα σημαντικό.\n\nΒιβλίο τεράστιο προκύπτουν σαν το, σαν τρόπο επιδιόρθωση ας. Είχαν προσοχή προσπάθεια κι ματ, εδώ ως έτσι σελίδων συζήτηση. Και στην βγαίνει εσφαλμένη με, δυστυχής παράδειγμα δε μας, από σε υόρκη επιδιόρθωσης. Νέα πω νέου πιθανό, στήλες συγγραφείς μπαίνοντας μα για', το ρωτήσει κακόκεφους της? Μου σε αρέσει συγγραφής συγχωνευτεί, μη μου υόρκη ξέχασε διακοπής! Ώς επί αποφάσισε αποκλειστικούς χρησιμοποιώντας, χρήματα σελίδων ταξινομεί ναι με.\n\nΜη ανά γραμμή απόλαυσε, πω ναι μάτσο διασφαλίζεται. Τη έξι μόλις εργάστηκε δημιουργούν, έκδοση αναφορά δυσκολότερο οι νέο. Σας ως μπορούσε παράδειγμα, αν ότι δούλευε μπορούσε αποκλειστικούς, πιο λέει βουτήξουν διορθώσει ως. Έχω τελευταία κακόκεφους ας, όσο εργαζόμενων δημιουργήσεις τα.\n\nΤου αν δουλέψει μπορούσε, πετούν χαμηλός εδώ ας? Κύκλο τύπους με που, δεν σε έχουν συνεχώς χειρότερα, τις τι απαράδεκτη συνηθίζουν? Θα μην τους αυτήν, τη ένα πήρε πακέτων, κι προκύπτουν περιβάλλον πως. Μα για δουλέψει απόλαυσε εφαμοργής, ώς εδώ σημαίνει μπορούσες, άμεση ακούσει προσοχή τη εδώ?\n\nΣτα δώσε αθόρυβες λιγότερους οι, δε αναγκάζονται αποκλειστικούς όλα! Ας μπουν διοικητικό μια, πάντα ελέγχου διορθώσεις ώς τον. Ότι πήρε κανόνα μα. Που άτομα κάνεις δημιουργίες τα, οι μας αφού κόλπα προγραμματιστής, αφού ωραίο προκύπτουν στα ως. Θέμα χρησιμοποιήσει αν όλα, του τα άλγεβρα σελίδων. Τα ότι ανώδυνη δυστυχώς συνδυασμούς, μας οι πάντα γνωρίζουμε ανταγωνιστής, όχι τα δοκιμάσεις σχεδιαστής! Στην συνεντεύξης επιδιόρθωση πιο τα, μα από πουλάς περιβάλλον παραγωγικής.\n\nΈχουν μεταγλωτίσει σε σας, σε πάντα πρώτης μειώσει των, γράψει ρουτίνα δυσκολότερο ήδη μα? Ταξινομεί διορθώσεις να μας. Θα της προσπαθούν περιεχόμενα, δε έχω τοπικές στέλνοντάς. Ανά δε αλφα άμεση, κάποιο ρωτάει γνωρίζουμε πω στη, φράση μαγικά συνέχεια δε δύο! Αν είχαμε μειώσει ροή, μας μετράει καθυστερούσε επιδιορθώσεις μη. Χάος υόρκη κεντρικό έχω σε, ανά περίπου αναγκάζονται πω.\n\nΌσο επιστρέφουν χρονοδιαγράμματα μη. Πως ωραίο κακόκεφος διαχειριστής ως, τις να διακοπής αναζήτησης. Κάποιο ποσοστό ταξινομεί επί τη? Μάθε άμεση αλλάζοντας δύο με, μου νέου πάντα να.\n\nΠω του δυστυχώς πιθανότητες. Κι ρωτάει υψηλότερη δημιουργια ότι, πω εισαγωγή τελευταία απομόνωση ναι. Των ζητήσεις γνωρίζουμε ώς? Για' μη παραδοτέου αναφέρονται! Ύψος παραγωγικά ροή ως, φυσικά διάβασε εικόνες όσο σε? Δεν υόρκη διορθώσεις επεξεργασία θα, ως μέση σύστημα χρησιμοποιήσει τις."
  },
  {
    "path": "src/components/mime/spec/fixtures/samples/charsets/utf-8/two.txt",
    "content": "रखति आवश्यकत प्रेरना मुख्यतह हिंदी किएलोग असक्षम कार्यलय करते विवरण किके मानसिक दिनांक पुर्व संसाध एवम् कुशलता अमितकुमार प्रोत्साहित जनित देखने उदेशीत विकसित बलवान ब्रौशर किएलोग विश्लेषण लोगो कैसे जागरुक प्रव्रुति प्रोत्साहित सदस्य आवश्यकत प्रसारन उपलब्धता अथवा हिंदी जनित दर्शाता यन्त्रालय बलवान अतित सहयोग शुरुआत सभीकुछ माहितीवानीज्य लिये खरिदे है।अभी एकत्रित सम्पर्क रिती मुश्किल प्राथमिक भेदनक्षमता विश्व उन्हे गटको द्वारा तकरीबन\n\nविश्व द्वारा व्याख्या सके। आजपर वातावरण व्याख्यान पहोच। हमारी कीसे प्राथमिक विचारशिलता पुर्व करती कम्प्युटर भेदनक्षमता लिये बलवान और्४५० यायेका वार्तालाप सुचना भारत शुरुआत लाभान्वित पढाए संस्था वर्णित मार्गदर्शन चुनने"
  },
  {
    "path": "src/components/mime/spec/header/collection_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct HeaderCollectionTest < ASPEC::TestCase\n  def test_line_length : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_date_header \"date\", Time.utc\n\n    headers.line_length.should eq 76\n    headers[\"date\"].max_line_length.should eq 76\n\n    headers.line_length = 50\n\n    headers.line_length.should eq 50\n    headers[\"date\"].max_line_length.should eq 50\n  end\n\n  def test_add_mailbox_list_header : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_mailbox_list_header \"from\", [\"me@example.com\"]\n    headers[\"from\"].should_not be_nil\n  end\n\n  def test_add_date_header : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_date_header \"date\", Time.utc\n    headers[\"date\"].should_not be_nil\n  end\n\n  def test_add_text_header : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_text_header \"subject\", \"The Subject\"\n    headers[\"subject\"].should_not be_nil\n  end\n\n  def test_add_parameterized_header : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_parameterized_header \"content-type\", \"text/plain\", {\"charset\" => \"UTF-8\"}\n    headers[\"content-type\"].should_not be_nil\n  end\n\n  def test_add_id_header : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_id_header \"message-id\", \"some@id\"\n    headers[\"message-id\"].should_not be_nil\n  end\n\n  def test_add_path_header : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_path_header \"return-path\", \"me@example.com\"\n    headers[\"return-path\"].should_not be_nil\n  end\n\n  def test_has_key : Nil\n    headers = AMIME::Header::Collection.new\n    headers.has_key?(\"date\").should be_false\n    headers.add_date_header \"date\", Time.utc\n    headers.has_key?(\"date\").should be_true\n  end\n\n  def test_is_unique_header : Nil\n    AMIME::Header::Collection.unique_header?(\"date\").should be_true\n    AMIME::Header::Collection.unique_header?(\"foo\").should be_false\n  end\n\n  @[TestWith(\n    {AMIME::Header::Date.new(\"date\", Time.utc)},\n    {AMIME::Header::MailboxList.new(\"from\", [AMIME::Address.new \"me@example.com\"])},\n    {AMIME::Header::MailboxList.new(\"to\", [AMIME::Address.new \"me@example.com\"])},\n    {AMIME::Header::MailboxList.new(\"cc\", [AMIME::Address.new \"me@example.com\"])},\n    {AMIME::Header::MailboxList.new(\"bcc\", [AMIME::Address.new \"me@example.com\"])},\n    {AMIME::Header::MailboxList.new(\"reply-to\", [AMIME::Address.new \"me@example.com\"])},\n    {AMIME::Header::Path.new(\"return-path\", AMIME::Address.new \"me@example.com\")},\n    {AMIME::Header::Mailbox.new(\"sender\", AMIME::Address.new \"me@example.com\")},\n    {AMIME::Header::Identification.new(\"message-id\", \"some@id\")},\n    {AMIME::Header::Identification.new(\"in-reply-to\", \"some@id\")},\n    {AMIME::Header::Identification.new(\"references\", \"some@id\")},\n    {AMIME::Header::Unstructured.new(\"in-reply-to\", \"some@id\")},\n    {AMIME::Header::Unstructured.new(\"references\", \"some@id\")},\n    {AMIME::Header::Unstructured.new(\"x-foo\", \"bar\")}, # Handles custom headers\n  )]\n  def test_check_header_class_valid(header : AMIME::Header::Interface) : Nil\n    AMIME::Header::Collection.check_header_class header\n  end\n\n  def test_check_header_class_invalid : Nil\n    expect_raises AMIME::Exception::Logic, \"The 'date' header must be an instance of 'Athena::MIME::Header::Date' (got 'Athena::MIME::Header::Unstructured').\" do\n      AMIME::Header::Collection.check_header_class AMIME::Header::Unstructured.new \"date\", \"blah\"\n    end\n  end\n\n  def test_to_a : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_text_header \"foo\", \"bar\"\n    headers.add_text_header \"\", \"\"\n\n    headers.to_a.should eq [\"foo: bar\"]\n  end\n\n  def test_names : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_text_header \"foo\", \"bar\"\n    headers.add_text_header \"biz\", \"baz\"\n\n    headers.names.should eq [\"foo\", \"biz\"]\n  end\n\n  def test_all_no_args : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_text_header \"foo\", \"bar\"\n    headers.add_text_header \"biz\", \"baz\"\n\n    names = [] of String\n\n    headers.all do |header|\n      names << header.name\n    end\n\n    names.should eq [\"foo\", \"biz\"]\n  end\n\n  def test_all_specific_name : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_text_header \"text\", \"bar\"\n    headers.add_text_header \"text\", \"baz\"\n\n    values = [] of String\n\n    headers.all \"text\" do |header|\n      values << header.body.to_s\n    end\n\n    values.should eq [\"bar\", \"baz\"]\n  end\n\n  def test_untyped : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_date_header \"date\", Time.utc\n    headers[\"DATE\"].should_not be_nil\n  end\n\n  def test_untyped_multiple : Nil\n    headers = AMIME::Header::Collection.new\n    text1 = AMIME::Header::Unstructured.new \"text\", \"1\"\n    text2 = AMIME::Header::Unstructured.new \"text\", \"2\"\n\n    headers << text1\n    headers << text2\n\n    headers[\"text\"].should be text1\n  end\n\n  def test_untyped_missing_name : Nil\n    headers = AMIME::Header::Collection.new\n\n    expect_raises AMIME::Exception::HeaderNotFound, \"No headers with the name 'foo' exist.\" do\n      headers[\"foo\"]\n    end\n  end\n\n  def test_typed_missing_name : Nil\n    headers = AMIME::Header::Collection.new\n\n    expect_raises AMIME::Exception::HeaderNotFound, \"No headers with the name 'foo' exist.\" do\n      headers[\"foo\", AMIME::Header::Date]\n    end\n  end\n\n  def test_nilable_untyped : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_date_header \"date\", Time.utc\n    headers[\"DATE\"]?.should_not be_nil\n  end\n\n  def test_nilable_untyped_multiple : Nil\n    headers = AMIME::Header::Collection.new\n    text1 = AMIME::Header::Unstructured.new \"text\", \"1\"\n    text2 = AMIME::Header::Unstructured.new \"text\", \"2\"\n\n    headers << text1\n    headers << text2\n\n    headers[\"text\"]?.should be text1\n  end\n\n  def test_nilable_untyped_missing_name : Nil\n    headers = AMIME::Header::Collection.new\n    headers[\"foo\"]?.should be_nil\n  end\n\n  def test_nilable_typed_missing_name : Nil\n    headers = AMIME::Header::Collection.new\n    headers[\"foo\", AMIME::Header::Date]?.should be_nil\n  end\n\n  def test_set_unique_header : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_date_header \"date\", Time.utc\n\n    expect_raises AMIME::Exception::Logic, \"Cannot set header 'date' as it is already defined and must be unique.\" do\n      headers.add_date_header \"date\", Time.utc\n    end\n  end\n\n  def test_header_parameter : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_parameterized_header \"content-type\", \"text/plain\", {\"charset\" => \"UTF-8\"}\n\n    headers.header_parameter(\"content-type\", \"charset\").should eq \"UTF-8\"\n  end\n\n  def test_header_parameter_non_parameterized_header : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_text_header \"foo\", \"bar\"\n\n    expect_raises AMIME::Exception::Logic, \"Unable to get parameter 'param' on header 'foo' as the header is not of class 'Athena::MIME::Header::Parameterized'.\" do\n      headers.header_parameter \"foo\", \"param\"\n    end\n  end\n\n  def test_set_header_parameter_non_parameterized_header : Nil\n    headers = AMIME::Header::Collection.new\n    headers.add_text_header \"foo\", \"bar\"\n\n    expect_raises AMIME::Exception::Logic, \"Unable to set parameter 'param' on header 'foo' as the header is not of class 'Athena::MIME::Header::Parameterized'.\" do\n      headers.header_parameter \"foo\", \"param\", \"value\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/header/date_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct DateHeaderTest < ASPEC::TestCase\n  def test_happy_path : Nil\n    header = AMIME::Header::Date.new \"date\", now = Time.utc\n    header.body.should eq now\n\n    later = Time.utc + 1.week\n    header.body = later\n    header.body.should eq later\n  end\n\n  def test_body_to_s : Nil\n    AMIME::Header::Date\n      .new(\"date\", Time.utc 2025, 1, 3, 0, 16, 15)\n      .to_s.should eq \"date: Fri, 3 Jan 2025 00:16:15 +0000\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/header/identification_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct IdentificationHeaderTest < ASPEC::TestCase\n  def test_happy_path : Nil\n    AMIME::Header::Identification\n      .new(\"message-id\", \"id-left@id-right\")\n      .body_to_s\n      .should eq \"<id-left@id-right>\"\n  end\n\n  def test_can_be_retrieved_verbatim : Nil\n    AMIME::Header::Identification\n      .new(\"message-id\", \"id-left@id-right\")\n      .id\n      .should eq \"id-left@id-right\"\n  end\n\n  def test_can_have_multiple_ids : Nil\n    header = AMIME::Header::Identification.new(\"references\", \"c@d\")\n    header.ids = [\"a@b\", \"x@y\"]\n    header.ids.should eq [\"a@b\", \"x@y\"]\n  end\n\n  def test_multiple_ids_produces_list_value : Nil\n    header = AMIME::Header::Identification.new(\"references\", [\"a@b\", \"x@y\"])\n    header.body_to_s.should eq \"<a@b> <x@y>\"\n  end\n\n  def test_left_id_can_be_quoted : Nil\n    header = AMIME::Header::Identification.new(\"references\", %(\"ab\"@c))\n    header.id.should eq %(\"ab\"@c)\n    header.body_to_s.should eq %(<\"ab\"@c>)\n  end\n\n  def test_left_id_can_contain_angles_as_quoted_pair : Nil\n    header = AMIME::Header::Identification.new(\"references\", %(\"a\\\\<\\\\>b\"@c))\n    header.id.should eq %(\"a\\\\<\\\\>b\"@c)\n    header.body_to_s.should eq %(<\"a\\\\<\\\\>b\"@c>)\n  end\n\n  def test_left_id_can_be_dot_atom : Nil\n    header = AMIME::Header::Identification.new(\"references\", %(a.b+&%$.c@d))\n    header.id.should eq %(a.b+&%$.c@d)\n    header.body_to_s.should eq %(<a.b+&%$.c@d>)\n  end\n\n  # TODO: Implement when email is validated\n\n  # def test_invalid_left : Nil\n  # end\n\n  # def test_invalid_right : Nil\n  # end\n\n  # def test_invalid_missing_at : Nil\n  # end\n\n  def test_right_id_can_be_dot_atom : Nil\n    header = AMIME::Header::Identification.new(\"references\", %(a@b.c+&%$.d))\n    header.id.should eq %(a@b.c+&%$.d)\n    header.body_to_s.should eq %(<a@b.c+&%$.d>)\n  end\n\n  def test_right_id_can_be_literal : Nil\n    header = AMIME::Header::Identification.new(\"references\", %(a@[1.2.3.4]))\n    header.id.should eq %(a@[1.2.3.4])\n    header.body_to_s.should eq %(<a@[1.2.3.4]>)\n  end\n\n  def test_right_id_is_idn_encoded : Nil\n    header = AMIME::Header::Identification.new(\"references\", \"a@ä\")\n    header.id.should eq \"a@ä\"\n    header.body_to_s.should eq \"<a@xn--4ca>\"\n  end\n\n  def test_set_body : Nil\n    header = AMIME::Header::Identification.new(\"references\", \"a@b\")\n    header.body = \"d@f\"\n    header.ids.should eq [\"d@f\"]\n  end\n\n  def test_get_body : Nil\n    header = AMIME::Header::Identification.new(\"references\", \"a@b\")\n    header.body = \"d@f\"\n    header.body.should eq [\"d@f\"]\n  end\n\n  def test_to_s : Nil\n    AMIME::Header::Identification\n      .new(\"references\", [\"a@b\", \"x@y\"])\n      .to_s.should eq \"references: <a@b> <x@y>\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/header/mailbox_list_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct MailboxListHeaderTest < ASPEC::TestCase\n  def test_mailbox_is_set_for_address : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new \"me@example.com\"])\n      .address_strings\n      .should eq [\"me@example.com\"]\n  end\n\n  def test_mailbox_is_set_for_named_address : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new \"me@example.com\", \"Jon Sno\"])\n      .address_strings\n      .should eq [\"Jon Sno <me@example.com>\"]\n  end\n\n  def test_body_to_s : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new \"me@example.com\", \"Jon Sno\"])\n      .body_to_s\n      .should eq \"Jon Sno <me@example.com>\"\n  end\n\n  def test_body_to_s_multiple : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new(\"me@example.com\", \"Jon Sno\"), AMIME::Address.new(\"you@example.com\", \"Jon Smith\")])\n      .body_to_s\n      .should eq \"Jon Sno <me@example.com>, Jon Smith <you@example.com>\"\n  end\n\n  def test_to_s_multiple : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new(\"me@example.com\", \"Jon Sno\"), AMIME::Address.new(\"you@example.com\", \"Jon Smith\")])\n      .to_s\n      .should eq \"from: Jon Sno <me@example.com>, Jon Smith <you@example.com>\"\n  end\n\n  def test_addresses : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new \"me@example.com\", %(Jon Sno, \"with love\")])\n      .address_strings\n      .should eq [%(\"Jon Sno, \\\\\"with love\\\\\"\" <me@example.com>)]\n  end\n\n  def test_quotes_escaped_chars : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new \"me@example.com\", %(Jon Sno, \\\\escaped\\\\)])\n      .address_strings\n      .should eq [%(\"Jon Sno, \\\\\\\\escaped\\\\\\\\\" <me@example.com>)]\n  end\n\n  def test_quotes_paren : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new \"me@example.com\", %(Jon (Sno))])\n      .address_strings\n      .should eq [%(\"Jon (Sno)\" <me@example.com>)]\n  end\n\n  def test_utf8_in_domain : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new \"me@fußball.com\"])\n      .address_strings\n      .should eq [\"me@xn--fuball-cta.com\"]\n  end\n\n  def test_utf8_in_local_part : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new \"fußball@example.com\"])\n      .address_strings\n      .should eq [\"fußball@example.com\"]\n  end\n\n  def test_multiple_addresses : Nil\n    AMIME::Header::MailboxList\n      .new(\"from\", [AMIME::Address.new(\"me@example.com\"), AMIME::Address.new(\"you@example.com\")])\n      .address_strings\n      .should eq [\"me@example.com\", \"you@example.com\"]\n  end\n\n  def test_encoded_non_ascii : Nil\n    header = AMIME::Header::MailboxList.new(\"sender\", [AMIME::Address.new \"me@example.com\", %(Jon S\\x8F o)])\n    header.charset = \"iso-8859-1\"\n    header.address_strings.should eq [%(Jon =?iso-8859-1?Q?S=8F?= o <me@example.com>)]\n  end\n\n  def test_body : Nil\n    header = AMIME::Header::MailboxList.new(\"from\", [AMIME::Address.new \"me@example.com\", \"Jon Sno\"])\n    header.body = addresses = [AMIME::Address.new \"you@example.com\", \"Jon Smith\"]\n    header.body.should eq addresses\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/header/mailbox_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct MailboxHeaderTest < ASPEC::TestCase\n  def test_happy_path : Nil\n    header = AMIME::Header::Mailbox.new \"sender\", address = AMIME::Address.new \"me@example.com\"\n    header.body.should eq address\n\n    other_address = AMIME::Address.new \"you@example.com\"\n    header.body = other_address\n    header.body.should eq other_address\n  end\n\n  def test_body_to_s_no_name : Nil\n    header = AMIME::Header::Mailbox.new(\"sender\", AMIME::Address.new \"me@example.com\")\n    header.body_to_s.should eq \"me@example.com\"\n\n    header.body = AMIME::Address.new \"me@fußball.com\"\n    header.body_to_s.should eq \"me@xn--fuball-cta.com\"\n  end\n\n  def test_body_to_s_with_name : Nil\n    AMIME::Header::Mailbox\n      .new(\"sender\", AMIME::Address.new \"me@example.com\", \"Jon Sno\")\n      .body_to_s\n      .should eq \"Jon Sno <me@example.com>\"\n  end\n\n  def test_body_to_s_with_quoted_name : Nil\n    AMIME::Header::Mailbox\n      .new(\"sender\", AMIME::Address.new \"me@example.com\", %(Jon Sno, \"with love\"))\n      .body_to_s\n      .should eq %(\"Jon Sno, \\\\\"with love\\\\\"\" <me@example.com>)\n  end\n\n  def test_body_to_s_with_escaped : Nil\n    AMIME::Header::Mailbox\n      .new(\"sender\", AMIME::Address.new \"me@example.com\", %(Jon Sno, \\\\escaped\\\\))\n      .body_to_s\n      .should eq %(\"Jon Sno, \\\\\\\\escaped\\\\\\\\\" <me@example.com>)\n  end\n\n  def test_body_to_s_with_encoded_byte : Nil\n    header = AMIME::Header::Mailbox.new(\"sender\", AMIME::Address.new \"me@example.com\", %(Jon S\\x8F o))\n    header.charset = \"iso-8859-1\"\n    header.body_to_s.should eq %(Jon =?iso-8859-1?Q?S=8F?= o <me@example.com>)\n  end\n\n  def test_utf8_chars_in_local_part : Nil\n    AMIME::Header::Mailbox\n      .new(\"sender\", AMIME::Address.new \"fußball@example.com\")\n      .body_to_s\n      .should eq \"fußball@example.com\"\n  end\n\n  def test_utf8_chars_in_local_part_name_with_space : Nil\n    AMIME::Header::Mailbox\n      .new(\"sender\", AMIME::Address.new \"fußball@example.com\", \"fußball fußball\")\n      .body_to_s\n      .should eq \"=?UTF-8?Q?fu=C3=9Fball_fu=C3=9Fball?= <fußball@example.com>\"\n  end\n\n  def test_utf8_chars_in_local_part_name_with_double_space : Nil\n    AMIME::Header::Mailbox\n      .new(\"sender\", AMIME::Address.new \"fußball@example.com\", \"fußball  fußball\")\n      .body_to_s\n      .should eq \"=?UTF-8?Q?fu=C3=9Fball?=  =?UTF-8?Q?fu=C3=9Fball?= <fußball@example.com>\"\n  end\n\n  def test_to_s_address_only : Nil\n    AMIME::Header::Mailbox\n      .new(\"sender\", AMIME::Address.new \"me@example.com\")\n      .to_s\n      .should eq \"sender: me@example.com\"\n  end\n\n  def test_to_s_address_name : Nil\n    AMIME::Header::Mailbox\n      .new(\"sender\", AMIME::Address.new \"me@example.com\", \"Jon Sno\")\n      .to_s\n      .should eq \"sender: Jon Sno <me@example.com>\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/header/parameterized_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct ParameterizedHeaderTest < ASPEC::TestCase\n  @lang = \"en-us\"\n\n  def test_value_is_returned_verbatim : Nil\n    header = AMIME::Header::Parameterized.new \"content-type\", \"text/plain\"\n    header.body.should eq \"text/plain\"\n  end\n\n  def test_parameters_are_appended : Nil\n    header = AMIME::Header::Parameterized.new \"content-type\", \"text/plain\"\n    header[\"charset\"] = \"UTF-8\"\n    header.body_to_s.should eq \"text/plain; charset=UTF-8\"\n  end\n\n  def test_space_in_param_results_in_quoted_string : Nil\n    header = AMIME::Header::Parameterized.new \"content-type\", \"attachment\"\n    header[\"filename\"] = \"my file.txt\"\n    header.body_to_s.should eq \"attachment; filename=\\\"my file.txt\\\"\"\n  end\n\n  def test_form_data_results_in_quoted_string : Nil\n    header = AMIME::Header::Parameterized.new \"content-disposition\", \"form-data\"\n    header[\"filename\"] = \"file.txt\"\n    header.body_to_s.should eq \"form-data; filename=\\\"file.txt\\\"\"\n  end\n\n  def test_form_data_utf8 : Nil\n    header = AMIME::Header::Parameterized.new \"content-disposition\", \"form-data\"\n    header[\"filename\"] = \"déjà%\\\"\\n\\r.txt\"\n    header.body_to_s.should eq \"form-data; filename=\\\"déjà%%22%0A%0D.txt\\\"\"\n  end\n\n  def test_long_params_are_broken_into_multiple_attribute_strings : Nil\n    value = \"a\" * 180\n\n    header = AMIME::Header::Parameterized.new \"content-disposition\", \"attachment\"\n    header[\"filename\"] = value\n    header.body_to_s.should eq(\n      \"attachment; \" \\\n      \"filename*0*=UTF-8''#{\"a\" * 60};\\r\\n \" \\\n      \"filename*1*=#{\"a\" * 60};\\r\\n \" \\\n      \"filename*2*=#{\"a\" * 60}\"\n    )\n  end\n\n  def test_encoded_param_data_includes_charset_and_language : Nil\n    value = %(#{\"a\" * 20}\\x8F#{\"a\" * 10})\n\n    header = AMIME::Header::Parameterized.new \"content-disposition\", \"attachment\"\n    header.charset = \"iso-8859-1\"\n    header.body = \"attachment\"\n    header[\"filename\"] = value\n    header.lang = @lang\n\n    header.body_to_s.should eq \"attachment; filename*=iso-8859-1'en-us'aaaaaaaaaaaaaaaaaaaa%8Faaaaaaaaaa\"\n  end\n\n  def test_multiple_encoded_param_lines_are_formatted_correctly : Nil\n    value = %(#{\"a\" * 20}\\x8F#{\"a\" * 60})\n\n    header = AMIME::Header::Parameterized.new \"content-disposition\", \"attachment\"\n    header.charset = \"UTF-6\"\n    header.body = \"attachment\"\n    header[\"filename\"] = value\n    header.lang = @lang\n\n    header.body_to_s.should eq \"attachment; filename*0*=UTF-6'en-us'aaaaaaaaaaaaaaaaaaaa%8Faaaaaaaaaaaaaaaaaaaaaaa;\\r\\n filename*1*=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n  end\n\n  def test_to_s : Nil\n    header = AMIME::Header::Parameterized.new \"content-type\", \"text/html\"\n    header[\"charset\"] = \"UTF-8\"\n    header.to_s.should eq \"content-type: text/html; charset=UTF-8\"\n  end\n\n  def test_value_can_be_encoded_if_not_ascii : Nil\n    value = \"go\\x8Fbar\"\n    header = AMIME::Header::Parameterized.new \"x-foo\", value\n    header.charset = \"iso-8859-1\"\n    header[\"lookslike\"] = \"foobar\"\n    header.to_s.should eq \"x-foo: =?iso-8859-1?Q?go=8Fbar?=; lookslike=foobar\"\n  end\n\n  def test_value_and_param_can_be_encoded_if_not_ascii : Nil\n    value = \"go\\x8Fbar\"\n    header = AMIME::Header::Parameterized.new \"x-foo\", value\n    header.charset = \"iso-8859-1\"\n    header[\"says\"] = value\n    header.to_s.should eq \"x-foo: =?iso-8859-1?Q?go=8Fbar?=; says*=iso-8859-1''go%8Fbar\"\n  end\n\n  def test_param_are_encoded_if_not_ascii : Nil\n    value = \"go\\x8Fbar\"\n    header = AMIME::Header::Parameterized.new \"x-foo\", \"bar\"\n    header.charset = \"iso-8859-1\"\n    header[\"says\"] = value\n    header.to_s.should eq \"x-foo: bar; says*=iso-8859-1''go%8Fbar\"\n  end\n\n  def test_params_are_encoded_with_legacy_encoding_enabled : Nil\n    value = \"go\\x8Fbar\"\n    header = AMIME::Header::Parameterized.new \"content-type\", \"bar\"\n    header.charset = \"iso-8859-1\"\n    header[\"says\"] = value\n    header.to_s.should eq %(content-type: bar; says=\"=?iso-8859-1?Q?go=8Fbar?=\")\n  end\n\n  def test_language_information_appears_in_encoded_words : Nil\n    value = \"go\\x8Fbar\"\n    header = AMIME::Header::Parameterized.new \"x-foo\", value\n    header.charset = \"iso-8859-1\"\n    header.lang = \"en\"\n    header[\"says\"] = value\n    header.to_s.should eq \"x-foo: =?iso-8859-1*en?Q?go=8Fbar?=; says*=iso-8859-1'en'go%8Fbar\"\n  end\n\n  def test_set_body : Nil\n    header = AMIME::Header::Parameterized.new \"content-type\", \"text/html\"\n    header.body = \"text/plain\"\n    header.body.should eq \"text/plain\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/header/path_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct PathHeaderTest < ASPEC::TestCase\n  def test_happy_path : Nil\n    header = AMIME::Header::Path.new \"return-path\", address = AMIME::Address.new \"me@example.com\"\n    header.body.should eq address\n\n    address = AMIME::Address.new \"you@example.com\"\n    header.body = address\n    header.body.should eq address\n  end\n\n  # def test_raises_if_invalid_address : Nile\n  # end\n\n  def test_body_to_s : Nil\n    AMIME::Header::Path\n      .new(\"return-path\", AMIME::Address.new \"me@example.com\")\n      .body_to_s.should eq \"<me@example.com>\"\n  end\n\n  def test_body_to_s_utf8_chars_in_local_part : Nil\n    AMIME::Header::Path\n      .new(\"return-path\", AMIME::Address.new \"chrïs@example.com\")\n      .body_to_s.should eq \"<chrïs@example.com>\"\n  end\n\n  def test_body_to_s_idn_encoded_if_needed : Nil\n    AMIME::Header::Path\n      .new(\"return-path\", AMIME::Address.new \"test@fußball.test\")\n      .body_to_s.should eq \"<test@xn--fuball-cta.test>\"\n  end\n\n  def test_to_s : Nil\n    AMIME::Header::Path\n      .new(\"return-path\", AMIME::Address.new \"me@example.com\")\n      .to_s.should eq \"return-path: <me@example.com>\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/header/unstructured_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct UnstructuredHeaderTest < ASPEC::TestCase\n  def test_name : Nil\n    AMIME::Header::Unstructured\n      .new(\"subject\", \"\")\n      .name\n      .should eq \"subject\"\n  end\n\n  def test_body : Nil\n    header = AMIME::Header::Unstructured.new \"foo\", \"bar\"\n    header.body.should eq \"bar\"\n    header.body = \"baz\"\n    header.body.should eq \"baz\"\n  end\n\n  def test_to_s : Nil\n    AMIME::Header::Unstructured\n      .new(\"subject\", \"content\")\n      .to_s\n      .should eq \"subject: content\"\n  end\n\n  def test_to_s_long_lines : Nil\n    AMIME::Header::Unstructured\n      .new(\"x-custom-header\", \"The quick brown fox jumped over the fence, he was a very very scary brown fox with a bushy tail\")\n      .to_s\n      .should eq <<-TXT\n        x-custom-header: The quick brown fox jumped over the fence, he was a very\\r\n         very scary brown fox with a bushy tail\n        TXT\n  end\n\n  def test_only_printable_ascii_appears_in_headers : Nil\n    AMIME::Header::Unstructured\n      .new(\"x-test\", \"\\x8F\")\n      .to_s\n      .should match /[^:\\x00-\\x20\\x80-\\xFF]+: [^\\x80-\\xFF\\r\\n]+$/\n  end\n\n  def test_follows_general_structure : Nil\n    AMIME::Header::Unstructured\n      .new(\"x-test\", \"\\x8F\")\n      .to_s\n      .should match /^x-test: \\=?.*?\\?.*?\\?.*?\\?=$/\n  end\n\n  def test_encoded_words_include_charset_and_encoding : Nil\n    header = AMIME::Header::Unstructured.new(\"x-test\", \"\\x8F\")\n    header.charset = \"iso-8859-1\"\n    header\n      .to_s\n      .should eq \"x-test: =?iso-8859-1?Q?=8F?=\"\n  end\n\n  def test_encoded_words_are_used_to_represent_non_printable_ascii : Nil\n    # Allows SPACE and TAB\n    non_printable_bytes = [] of UInt8\n    non_printable_bytes.concat (0x00_u8..0x08).to_a\n    non_printable_bytes.concat (0x10_u8..0x19).to_a\n    non_printable_bytes << 0x7F_u8\n\n    non_printable_bytes.each do |byte|\n      char = String.build(&.write_byte(byte))\n      encoded_char = sprintf \"=%02X\", byte\n\n      AMIME::Header::Unstructured\n        .new(\"x-test\", char)\n        .to_s\n        .should eq \"x-test: =?UTF-8?Q?#{encoded_char}?=\"\n    end\n  end\n\n  def test_encoded_words_are_used_to_encode8_bit_octets : Nil\n    (0x80_u8..0xFF).each do |byte|\n      char = String.build(&.write_byte(byte))\n      encoded_char = sprintf \"=%02X\", byte\n\n      header = AMIME::Header::Unstructured.new(\"x-test\", char)\n      header.charset = \"iso-8859-1\"\n\n      header.to_s.should eq \"x-test: =?iso-8859-1?Q?#{encoded_char}?=\"\n    end\n  end\n\n  def test_are_no_longer_than_75_chars_per_line : Nil\n    non_ascii_char = String.build(&.write_byte(143_u8))\n\n    header = AMIME::Header::Unstructured.new(\"x-test\", non_ascii_char)\n    header.charset = \"iso-8859-1\"\n\n    header.to_s.should eq \"x-test: =?iso-8859-1?Q?=8F?=\"\n  end\n\n  def test_fwsp_is_used_when_encoder_returns_multiple_lines : Nil\n    header = AMIME::Header::Unstructured.new \"x-test\", \"\\x8Fline_one_here\\r\\nline_two_here\"\n    header.charset = \"iso-8859-1\"\n\n    header.to_s.should eq \"x-test: =?iso-8859-1?Q?=8Fline=5Fone=5Fhere?=\\r\\n =?iso-8859-1?Q?line=5Ftwo=5Fhere?=\"\n  end\n\n  def test_language_information_appears_in_encoded_words : Nil\n    header = AMIME::Header::Unstructured.new \"subject\", \"go\\x8Fbar\"\n    header.charset = \"iso-8859-1\"\n    header.lang = \"en\"\n\n    header.to_s.should eq \"subject: =?iso-8859-1*en?Q?go=8Fbar?=\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/magic_types_guesser_spec.cr",
    "content": "require \"./abstract_types_guesser_test_case\"\nrequire \"./spec_helper\"\n\nstruct MagicTypesGuesserTest < AbstractTypesGuesserTestCase\n  protected def guesser : AMIME::TypesGuesserInterface\n    AMIME::MagicTypesGuesser.new\n  end\n\n  def test_guess_with_known_extension : Nil\n    assert_pending\n\n    self.guesser.guess_mime_type(\"#{__DIR__}/fixtures/mimetypes/test.gif\").should eq \"image/gif\"\n  end\n\n  def test_guess_with_leading_dash : Nil\n    assert_pending\n\n    self.guesser.guess_mime_type(\"#{__DIR__}/fixtures/mimetypes/-test\").should eq \"image/gif\"\n  end\n\n  def test_guess_without_extension : Nil\n    assert_pending\n\n    self.guesser.guess_mime_type(\"#{__DIR__}/fixtures/mimetypes/test\").should eq \"image/gif\"\n  end\n\n  def test_guess_with_unknown_extension : Nil\n    assert_pending\n\n    self.guesser.guess_mime_type(\"#{__DIR__}/fixtures/mimetypes/.unknownextension\").should eq \"application/octet-stream\"\n  end\n\n  def test_guess_with_duplicated_file_type : Nil\n    assert_pending\n\n    self.guesser.guess_mime_type(\"#{__DIR__}/fixtures/test.docx\").should eq \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n  end\n\n  private def assert_pending : Nil\n    pending! \"Guesser is not supported\" if {{ flag?(\"windows\") && !flag?(\"gnu\") }}\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/message_converter_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct MessageConverterTest < ASPEC::TestCase\n  def test_to_email_email_argument : Nil\n    email = self.new_email\n    AMIME::MessageConverter.to_email(email).should be email\n  end\n\n  def test_requires_conversion : Nil\n    file = File.read \"#{__DIR__}/fixtures/mimetypes/test.gif\"\n\n    self.assert_conversion(new_email.text(\"text content\"))\n    self.assert_conversion(new_email.html(%(HTML content <img src=\"cid:test.gif\" />)))\n    self.assert_conversion(new_email.text(\"text content\").html(%(HTML content <img src=\"cid:test.gif\" />)))\n    self.assert_conversion(new_email\n      .text(\"text content\")\n      .html(%(HTML content <img src=\"cid:test.gif\" />))\n      .add_part(AMIME::Part::Data.new(file, \"test.gif\", \"image/gif\").as_inline)\n    )\n    self.assert_conversion(new_email\n      .text(\"text content\")\n      .html(%(HTML content <img src=\"cid:test.gif\" />))\n      .add_part(AMIME::Part::Data.new(file, \"test_attached.gif\", \"image/gif\"))\n    )\n    self.assert_conversion(new_email\n      .text(\"text content\")\n      .html(%(HTML content <img src=\"cid:test.gif\" />))\n      .add_part(AMIME::Part::Data.new(file, \"test.gif\", \"image/gif\").as_inline)\n      .add_part(AMIME::Part::Data.new(file, \"test_attached.gif\", \"image/gif\"))\n    )\n    self.assert_conversion(new_email\n      .text(\"text content\")\n      .add_part(AMIME::Part::Data.new(file, \"test_attached.gif\", \"image/gif\"))\n    )\n    self.assert_conversion(new_email\n      .html(%(HTML content <img src=\"cid:test.gif\" />))\n      .add_part(AMIME::Part::Data.new(file, \"test_attached.gif\", \"image/gif\"))\n    )\n    self.assert_conversion(new_email\n      .html(%(HTML content <img src=\"cid:test.gif\" />))\n      .add_part(AMIME::Part::Data.new(file, \"test_attached.gif\", \"image/gif\").as_inline)\n    )\n    self.assert_conversion(new_email\n      .text(\"text content\")\n      .add_part(AMIME::Part::Data.new(file, \"test_attached.gif\", \"image/gif\").as_inline)\n    )\n  end\n\n  private def assert_conversion(expected : AMIME::Email) : Nil\n    message = AMIME::Message.new expected.headers, expected.generate_body\n    converted = AMIME::MessageConverter.to_email message\n\n    if html_body = expected.html_body\n      html_body.should match /HTML content <img src=\"cid:[\\w\\.]+\" \\/>/\n      expected.html \"html content\"\n      converted.html \"html content\"\n    end\n\n    pointerof(expected.@cached_body).value = nil\n    pointerof(converted.@cached_body).value = nil\n\n    converted.should eq expected\n  end\n\n  private def new_email : AMIME::Email\n    AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\")\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/message_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct MessageTest < ASPEC::TestCase\n  def test_construct : Nil\n    m = AMIME::Message.new\n    m.body.should be_nil\n    m.headers.should eq AMIME::Header::Collection.new\n\n    m = AMIME::Message.new(\n      headers = AMIME::Header::Collection.new.tap(&.add_date_header(\"date\", Time.utc)),\n      body = AMIME::Part::Text.new(\"content\"),\n    )\n    m.headers.should be headers\n    m.body.should be body\n\n    m = AMIME::Message.new\n    m.body = body\n    m.headers = headers\n\n    m.headers.should be headers\n    m.body.should be body\n  end\n\n  def test_raises_when_no_from : Nil\n    expect_raises AMIME::Exception::Logic, \"An email must have a 'from' or a 'sender' header.\" do\n      AMIME::Message.new.prepared_headers\n    end\n  end\n\n  def test_prepared_headers_uses_sender_if_present_but_no_from : Nil\n    m = AMIME::Message.new\n    m.headers.add_mailbox_header \"sender\", \"sender@example.com\"\n\n    m.prepared_headers[\"from\"].should eq AMIME::Header::MailboxList.new \"from\", [AMIME::Address.new \"sender@example.com\"]\n  end\n\n  def test_prepared_headers_clone_headers : Nil\n    m = AMIME::Message.new\n    m.headers.add_mailbox_list_header \"from\", [\"me@example.com\"]\n    m.headers.should_not be m.prepared_headers\n  end\n\n  def test_prepared_headers_sets_required_headers : Nil\n    m = AMIME::Message.new\n    m.headers.add_mailbox_list_header \"from\", [\"me@example.com\"]\n    m.headers.add_mailbox_list_header \"bcc\", [\"spy@example.com\"]\n\n    headers = m.prepared_headers\n    headers.has_key?(\"mime-version\").should be_true\n    headers.has_key?(\"message-id\").should be_true\n    headers.has_key?(\"date\").should be_true\n    headers.has_key?(\"bcc\").should be_false\n  end\n\n  def test_prepared_headers : Nil\n    m = AMIME::Message.new\n    m.headers.add_mailbox_list_header \"from\", [\"me@example.com\"]\n    m.headers.add_date_header \"date\", now = Time.utc\n\n    headers = m.prepared_headers\n\n    headers.all.size.should eq 4\n    headers[\"from\"].should eq AMIME::Header::MailboxList.new \"from\", [AMIME::Address.new \"me@example.com\"]\n    headers[\"mime-version\"].should eq AMIME::Header::Unstructured.new \"mime-version\", \"1.0\"\n    headers[\"date\"].should eq AMIME::Header::Date.new \"date\", now\n  end\n\n  def test_prepared_headers_named_from : Nil\n    m = AMIME::Message.new\n    m.headers.add_mailbox_list_header \"from\", [AMIME::Address.new \"me@example.com\", \"Me\"]\n    headers = m.prepared_headers\n    headers[\"from\"].should eq AMIME::Header::MailboxList.new \"from\", [AMIME::Address.new \"me@example.com\", \"Me\"]\n  end\n\n  def test_prepared_headers_has_sender_when_needed : Nil\n    m = AMIME::Message.new\n    m.headers.add_mailbox_list_header \"from\", [\"me@example.com\"]\n    m.prepared_headers.has_key?(\"sender\").should be_false\n\n    m = AMIME::Message.new\n    m.headers.add_mailbox_list_header \"from\", [\"me@example.com\", \"other@example.com\"]\n    m.prepared_headers[\"sender\", AMIME::Header::Mailbox].body.address.should eq \"me@example.com\"\n\n    m = AMIME::Message.new\n    m.headers.add_mailbox_list_header \"from\", [\"me@example.com\", \"other@example.com\"]\n    m.headers.add_mailbox_header \"sender\", \"other@example.com\"\n    m.prepared_headers[\"sender\", AMIME::Header::Mailbox].body.address.should eq \"other@example.com\"\n  end\n\n  def test_generate_message_id_raises_no_addresses : Nil\n    expect_raises AMIME::Exception::Logic, \"A 'from' header must have at least one email address.\" do\n      m = AMIME::Message.new\n      m.headers.add_mailbox_list_header \"from\", [] of String\n      m.generate_message_id\n    end\n  end\n\n  def test_generate_message_id_raises_no_from_or_sender : Nil\n    expect_raises AMIME::Exception::Logic, \"An email must have a 'from' or 'sender' header.\" do\n      AMIME::Message.new.generate_message_id\n    end\n  end\n\n  def test_to_s_no_content : Nil\n    m = AMIME::Message.new\n    m.headers.add_mailbox_list_header \"from\", [\"me@example.com\"]\n    m.headers.add_date_header \"date\", Time.utc(2025, 1, 1, 12, 30)\n    m.headers.add_id_header \"message-id\", \"MESSAGE_ID\"\n\n    m.to_s.should eq <<-TXT\n    from: me@example.com\\r\n    date: Wed, 1 Jan 2025 12:30:00 +0000\\r\n    message-id: <MESSAGE_ID>\\r\n    mime-version: 1.0\\r\n    content-type: text/plain; charset=UTF-8\\r\n    content-transfer-encoding: quoted-printable\\r\n    \\r\n\n    TXT\n  end\n\n  def test_to_s_with_content : Nil\n    m = AMIME::Message.new body: AMIME::Part::Text.new(\"text content\")\n    m.headers.add_mailbox_list_header \"from\", [\"me@example.com\"]\n    m.headers.add_date_header \"date\", Time.utc(2025, 1, 1, 12, 30)\n    m.headers.add_id_header \"message-id\", \"MESSAGE_ID\"\n\n    m.to_s.should eq <<-TXT\n    from: me@example.com\\r\n    date: Wed, 1 Jan 2025 12:30:00 +0000\\r\n    message-id: <MESSAGE_ID>\\r\n    mime-version: 1.0\\r\n    content-type: text/plain; charset=UTF-8\\r\n    content-transfer-encoding: quoted-printable\\r\n    \\r\n    text content\n    TXT\n  end\n\n  def test_ensure_validity_valid : Nil\n    m = AMIME::Message.new\n    m.headers.add_mailbox_list_header \"from\", [\"me@example.com\"]\n    m.headers.add_mailbox_list_header \"to\", [\"you@example.com\"]\n\n    m.ensure_validity!\n  end\n\n  @[TestWith(\n    { {\"from\" => [\"me@example.com\"]}, AMIME::Exception::Logic, \"An email must have a 'to', 'cc', or 'bcc' header.\" },\n    { {\"from\" => [\"me@example.com\"], \"cc\" => [] of String}, AMIME::Exception::Logic, \"An email must have a 'to', 'cc', or 'bcc' header.\" },\n    { {\"from\" => [\"me@example.com\"], \"bcc\" => [] of String}, AMIME::Exception::Logic, \"An email must have a 'to', 'cc', or 'bcc' header.\" },\n    { {\"to\" => [] of String, \"from\" => [\"me@example.com\"]}, AMIME::Exception::Logic, \"An email must have a 'to', 'cc', or 'bcc' header.\" },\n    { {\"to\" => [\"you@example.com\"]}, AMIME::Exception::Logic, \"An email must have a 'from' or a 'sender' header.\" },\n    { {\"to\" => [\"you@example.com\"], \"from\" => [] of String}, AMIME::Exception::Logic, \"An email must have a 'from' or a 'sender' header.\" },\n  )]\n  def test_ensure_validity(headers : Hash(String, Array(String)), exception_class : ::Exception.class, exception_message : String)\n    m = AMIME::Message.new\n    headers.each do |k, v|\n      m.headers.add_mailbox_list_header k, v\n    end\n\n    expect_raises exception_class, exception_message do\n      m.ensure_validity!\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/native_types_guessuer_spec.cr",
    "content": "require \"./abstract_types_guesser_test_case\"\nrequire \"./spec_helper\"\n\nstruct NativeTypesGuesserTest < AbstractTypesGuesserTestCase\n  protected def guesser : AMIME::TypesGuesserInterface\n    AMIME::NativeTypesGuesser.new\n  end\n\n  def test_guess_with_leading_dash : Nil\n    self.guesser.guess_mime_type(\"#{__DIR__}/fixtures/mimetypes/-test\").should be_nil\n  end\n\n  def test_guess_without_extension : Nil\n    self.guesser.guess_mime_type(\"#{__DIR__}/fixtures/mimetypes/test\").should be_nil\n  end\n\n  def test_guess_with_unknown_extension : Nil\n    self.guesser.guess_mime_type(\"#{__DIR__}/fixtures/mimetypes/.unknownextension\").should be_nil\n  end\n\n  def test_guess_with_duplicated_file_type : Nil\n    # The MIME DB on windows CI doesn't know about this type, but works elsewhere\n    pending! \"Guesser is not supported\" if {{ flag?(\"windows\") && !flag?(\"gnu\") }}\n\n    self.guesser.guess_mime_type(\"#{__DIR__}/fixtures/test.docx\").should eq \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/part/data_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct DataPartTest < ASPEC::TestCase\n  def test_constructor : Nil\n    p = AMIME::Part::Data.new \"content\"\n    p.body.should eq \"content\"\n    p.body_to_s.should eq Base64.encode(\"content\")\n    p.media_type.should eq \"application\"\n    p.media_sub_type.should eq \"octet-stream\"\n\n    p = AMIME::Part::Data.new \"content\", content_type: \"text/html\"\n    p.media_type.should eq \"text\"\n    p.media_sub_type.should eq \"html\"\n  end\n\n  def test_constructor_io : Nil\n    io = IO::Memory.new \"content\"\n\n    p = AMIME::Part::Data.new io\n    p.body.should eq \"content\"\n    p.body_to_s.should eq Base64.encode(\"content\")\n  end\n\n  def test_constructor_real_file : Nil\n    File.open \"#{__DIR__}/../fixtures/content.txt\", \"r\" do |file|\n      p = AMIME::Part::Data.new file\n      p.body.should eq \"content\"\n      p.body_to_s.should eq Base64.encode(\"content\")\n    end\n  end\n\n  def test_constructor_file_part : Nil\n    p = AMIME::Part::Data.new(AMIME::Part::File.new(\"#{__DIR__}/../fixtures/content.txt\"))\n    p.body.should eq \"content\"\n    p.body_to_s.should eq Base64.encode(\"content\")\n  end\n\n  def test_prepared_headers : Nil\n    AMIME::Part::Data\n      .new(\"content\")\n      .prepared_headers\n      .should eq AMIME::Header::Collection.new(\n        AMIME::Header::Parameterized.new(\"content-type\", \"application/octet-stream\"),\n        AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"base64\"),\n        AMIME::Header::Parameterized.new(\"content-disposition\", \"attachment\"),\n      )\n  end\n\n  def test_prepared_headers_image : Nil\n    AMIME::Part::Data\n      .new(\"content\", \"photo.jpg\", \"text/html\")\n      .prepared_headers\n      .should eq AMIME::Header::Collection.new(\n        AMIME::Header::Parameterized.new(\"content-type\", \"text/html\", {\"name\" => \"photo.jpg\"}),\n        AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"base64\"),\n        AMIME::Header::Parameterized.new(\"content-disposition\", \"attachment\", {\"name\" => \"photo.jpg\", \"filename\" => \"photo.jpg\"}),\n      )\n  end\n\n  def test_prepared_headers_as_inline : Nil\n    AMIME::Part::Data\n      .new(\"content\", \"photo.jpg\", \"text/html\")\n      .as_inline\n      .prepared_headers\n      .should eq AMIME::Header::Collection.new(\n        AMIME::Header::Parameterized.new(\"content-type\", \"text/html\", {\"name\" => \"photo.jpg\"}),\n        AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"base64\"),\n        AMIME::Header::Parameterized.new(\"content-disposition\", \"inline\", {\"name\" => \"photo.jpg\", \"filename\" => \"photo.jpg\"}),\n      )\n  end\n\n  def test_prepared_headers_as_inline_with_cid : Nil\n    part = AMIME::Part::Data.new(\"content\", \"photo.jpg\", \"text/html\").as_inline\n    content_id = part.content_id\n\n    part\n      .prepared_headers\n      .should eq AMIME::Header::Collection.new(\n        AMIME::Header::Parameterized.new(\"content-type\", \"text/html\", {\"name\" => \"photo.jpg\"}),\n        AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"base64\"),\n        AMIME::Header::Parameterized.new(\"content-disposition\", \"inline\", {\"name\" => \"photo.jpg\", \"filename\" => \"photo.jpg\"}),\n        AMIME::Header::Identification.new(\"content-id\", content_id)\n      )\n  end\n\n  def test_from_path : Nil\n    part = AMIME::Part::Data.from_path file = \"#{__DIR__}/../fixtures/mimetypes/test.gif\"\n    content = File.read file\n\n    part.body.should eq content\n    part.body_to_s.should eq Base64.encode(content)\n    part.media_type.should eq \"image\"\n    part.media_sub_type.should eq \"gif\"\n\n    part\n      .prepared_headers\n      .should eq AMIME::Header::Collection.new(\n        AMIME::Header::Parameterized.new(\"content-type\", \"image/gif\", {\"name\" => \"test.gif\"}),\n        AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"base64\"),\n        AMIME::Header::Parameterized.new(\"content-disposition\", \"attachment\", {\"name\" => \"test.gif\", \"filename\" => \"test.gif\"}),\n      )\n  end\n\n  def test_from_path_with_meta : Nil\n    part = AMIME::Part::Data.from_path file = \"#{__DIR__}/../fixtures/mimetypes/test.gif\", \"photo.gif\", \"image/jpeg\"\n    content = File.read file\n\n    part.body.should eq content\n    part.body_to_s.should eq Base64.encode(content)\n    part.media_type.should eq \"image\"\n    part.media_sub_type.should eq \"jpeg\"\n\n    part\n      .prepared_headers\n      .should eq AMIME::Header::Collection.new(\n        AMIME::Header::Parameterized.new(\"content-type\", \"image/jpeg\", {\"name\" => \"photo.gif\"}),\n        AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"base64\"),\n        AMIME::Header::Parameterized.new(\"content-disposition\", \"attachment\", {\"name\" => \"photo.gif\", \"filename\" => \"photo.gif\"}),\n      )\n  end\n\n  def test_has_content_id : Nil\n    part = AMIME::Part::Data.new \"content\"\n    part.has_content_id?.should be_false\n    part.content_id\n    part.has_content_id?.should be_true\n  end\n\n  def test_set_content_id : Nil\n    part = AMIME::Part::Data.new \"content\"\n    part.content_id = \"test@test\"\n    part.content_id.should eq \"test@test\"\n  end\n\n  def test_set_content_id_invalid : Nil\n    expect_raises AMIME::Exception::InvalidArgument, \"The 'test' CID is invalid as it does not contain an '@' symbol.\" do\n      AMIME::Part::Data.new(\"content\").content_id = \"test\"\n    end\n  end\n\n  def test_filename : Nil\n    part = AMIME::Part::Data.new \"content\"\n    part.filename.should be_nil\n\n    part = AMIME::Part::Data.new \"content\", \"foo.txt\"\n    part.filename.should eq \"foo.txt\"\n  end\n\n  def test_content_type : Nil\n    part = AMIME::Part::Data.new \"content\"\n    part.content_type.should eq \"application/octet-stream\"\n\n    part = AMIME::Part::Data.new \"content\", content_type: \"application/pdf\"\n    part.content_type.should eq \"application/pdf\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/part/file_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct FilePartTest < ASPEC::TestCase\n  def test_content_type_known_extension : Nil\n    AMIME::Part::File.new(\"#{__DIR__}/../fixtures/mimetypes/test.gif\").content_type.should eq \"image/gif\"\n  end\n\n  def test_content_type_unknown_extension : Nil\n    AMIME::Part::File.new(\"#{__DIR__}/../fixtures/mimetypes/.unknownextension\").content_type.should eq \"application/octet-stream\"\n  end\n\n  def test_size : Nil\n    AMIME::Part::File.new(\"#{__DIR__}/../fixtures/mimetypes/test.gif\").size.should eq 35\n  end\n\n  def test_file_name_inferred : Nil\n    AMIME::Part::File.new(\"#{__DIR__}/../fixtures/mimetypes/test.gif\").filename.should eq \"test.gif\"\n  end\n\n  def test_file_name_explicit : Nil\n    AMIME::Part::File.new(\"#{__DIR__}/../fixtures/mimetypes/test.gif\", \"image.gif\").filename.should eq \"image.gif\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/part/message_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct MessagePartTest < ASPEC::TestCase\n  def test_constructor : Nil\n    part = AMIME::Part::Message.new AMIME::Email.new.from(\"me@example.com\").to(\"you@example.com\").text(\"text content\")\n    part.body.should contain \"text content\"\n    part.body_to_s.should contain \"text content\"\n    part.media_type.should eq \"message\"\n    part.media_sub_type.should eq \"rfc822\"\n  end\n\n  def test_headers : Nil\n    AMIME::Part::Message\n      .new(AMIME::Email.new.from(\"me@example.com\").text(\"text content\").subject(\"subject\"))\n      .prepared_headers\n      .should eq AMIME::Header::Collection.new(\n        AMIME::Header::Parameterized.new(\"content-type\", \"message/rfc822\", {\"name\" => \"subject.eml\"}),\n        AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"base64\"),\n        AMIME::Header::Parameterized.new(\"content-disposition\", \"attachment\", {\"name\" => \"subject.eml\", \"filename\" => \"subject.eml\"}),\n      )\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/part/multipart/alternative_spec.cr",
    "content": "require \"../../spec_helper\"\n\nstruct AlternativePartTest < ASPEC::TestCase\n  def test_constructor : Nil\n    part = AMIME::Part::Multipart::Alternative.new\n    part.media_type.should eq \"multipart\"\n    part.media_sub_type.should eq \"alternative\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/part/multipart/digest_spec.cr",
    "content": "require \"../../spec_helper\"\n\nstruct DigestPartTest < ASPEC::TestCase\n  def test_constructor : Nil\n    part = AMIME::Part::Multipart::Digest.new\n    part.media_type.should eq \"multipart\"\n    part.media_sub_type.should eq \"digest\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/part/multipart/form_spec.cr",
    "content": "require \"../../spec_helper\"\n\nstruct FormPartTest < ASPEC::TestCase\n  def test_constructor : Nil\n    b = AMIME::Part::Text.new \"content\"\n    c = AMIME::Part::Data.from_path \"#{__DIR__}/../../fixtures/mimetypes/test.gif\"\n    part = AMIME::Part::Multipart::Form.new({\n      \"foo\" => content = \"very very long content that will not be cut even if the length is way more than 76 characters, ok?\",\n      \"bar\" => b.dup,\n      \"baz\" => c.dup,\n    })\n\n    part.media_type.should eq \"multipart\"\n    part.media_sub_type.should eq \"form-data\"\n\n    t = AMIME::Part::Text.new content, encoding: \"8bit\"\n    t.disposition = \"form-data\"\n    t.name = \"foo\"\n    t.headers.line_length = Int32::MAX\n\n    b.disposition = \"form-data\"\n    b.encoding = \"8bit\"\n    b.name = \"bar\"\n    b.headers.line_length = Int32::MAX\n\n    c.disposition = \"form-data\"\n    c.encoding = \"8bit\"\n    c.name = \"baz\"\n    c.headers.line_length = Int32::MAX\n\n    part.parts.should eq [t, b, c]\n  end\n\n  def test_nested_array_parts : Nil\n    p1 = AMIME::Part::Text.new \"content\", encoding: \"8bit\"\n\n    part = AMIME::Part::Multipart::Form.new({\n      \"foo\" => p1.dup,\n      \"bar\" => {\n        \"baz\" => {\n          \"0\"   => p1.dup,\n          \"qux\" => p1.dup,\n        },\n      },\n\n      \"2\" => p1.dup,\n\n      \"quux\" => [\n        p1.dup,\n        p1.dup,\n      ],\n    })\n\n    part.media_type.should eq \"multipart\"\n    part.media_sub_type.should eq \"form-data\"\n\n    p1.name = \"foo\"\n    p1.disposition = \"form-data\"\n\n    p2 = p1.dup\n    p2.name = \"bar[baz][0]\"\n    p2.disposition = \"form-data\"\n\n    p3 = p1.dup\n    p3.name = \"bar[baz][qux]\"\n    p3.disposition = \"form-data\"\n\n    p4 = p1.dup\n    p4.name = \"2\"\n    p4.disposition = \"form-data\"\n\n    p5 = p1.dup\n    p5.name = \"quux[0]\"\n    p5.disposition = \"form-data\"\n\n    p6 = p1.dup\n    p6.name = \"quux[1]\"\n    p6.disposition = \"form-data\"\n\n    part.parts.should eq [p1, p2, p3, p4, p5, p6]\n  end\n\n  def test_disallowed_value_type : Nil\n    expect_raises AMIME::Exception::InvalidArgument, \"The value of the form field 'foo[qux][quux]' can only be a String, Hash, Array, or AMIME::Part::Text instance, got 'Int32'.\" do\n      AMIME::Part::Multipart::Form.new({\n        \"foo\" => {\n          \"bar\" => \"baz\",\n          \"qux\" => {\n            \"quux\" => 1,\n          },\n        },\n      })\n    end\n  end\n\n  def test_to_s : Nil\n    p = AMIME::Part::Data.from_path file_path = \"#{__DIR__}/../../fixtures/mimetypes/test.gif\"\n    p.body_to_s.should eq Base64.encode(File.read file_path)\n  end\n\n  def test_content_line_length : Nil\n    part = AMIME::Part::Multipart::Form.new({\n      \"foo\" => AMIME::Part::Data.new(foo = \"foo\" * 1000, \"foo.txt\", \"text/plain\"),\n      \"bar\" => bar = \"bar\" * 1000,\n    })\n\n    part.parts[0].body_to_s.should eq foo\n    part.parts[1].body_to_s.should eq bar\n  end\n\n  def test_boundary_content_type_header : Nil\n    AMIME::Part::Multipart::Form.new({\n      \"file\" => AMIME::Part::Data.new(\"data.csv\", \"data.csv\", \"text/csv\"),\n    })\n      .prepared_headers\n      .to_a\n      .first\n      .should match /^content-type: multipart\\/form-data; boundary=[a-zA-Z0-9\\-_]{50}$/ # 26 `-` + 18 bytes of base64 data\n  end\n\n  def test_body_to_s : Nil\n    string_lines = AMIME::Part::Multipart::Form.new({\n      \"file\" => AMIME::Part::Data.new(\"data.csv\", \"data.csv\", \"text/csv\"),\n    })\n      .body_to_s\n      .lines\n\n    string_lines[0].should match /^[a-zA-Z0-9\\-_]{50}$/ # 26 `-` + 18 bytes of base64 data\n    string_lines[1].should eq \"content-type: text/csv\"\n    string_lines[2].should eq \"content-transfer-encoding: 8bit\"\n    string_lines[3].should eq %(content-disposition: form-data; name=\"file\"; filename=\"data.csv\")\n    string_lines[4].should be_empty\n    string_lines[5].should eq \"data.csv\"\n    string_lines[6].should match /^[a-zA-Z0-9\\-_]{50}$/ # 26 `-` + 18 bytes of base64 data\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/part/multipart/mixed_spec.cr",
    "content": "require \"../../spec_helper\"\n\nstruct MixedPartTest < ASPEC::TestCase\n  def test_constructor : Nil\n    part = AMIME::Part::Multipart::Mixed.new\n    part.media_type.should eq \"multipart\"\n    part.media_sub_type.should eq \"mixed\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/part/multipart/related_spec.cr",
    "content": "require \"../../spec_helper\"\n\nstruct RelatedPartTest < ASPEC::TestCase\n  def test_constructor : Nil\n    part = AMIME::Part::Multipart::Related.new(\n      a = AMIME::Part::Text.new(\"text content\"),\n      {\n        b = AMIME::Part::Text.new(\"html content\", sub_type: \"html\"),\n        c = AMIME::Part::Text.new(\"html content again\", sub_type: \"html\"),\n      }\n    )\n\n    part.media_type.should eq \"multipart\"\n    part.media_sub_type.should eq \"related\"\n    part.parts.should eq [a, b, c]\n    a.headers.has_key?(\"content-id\").should be_false\n    b.headers.has_key?(\"content-id\").should be_true\n    c.headers.has_key?(\"content-id\").should be_true\n  end\n\n  def test_body_to_s\n    body = AMIME::Part::Multipart::Related\n      .new(\n        AMIME::Part::Multipart::Alternative.new(\n          AMIME::Part::Text.new(\"text content\"),\n          AMIME::Part::Text.new(\"html content\", sub_type: \"html\")\n        ),\n        [] of AMIME::Part::Abstract\n      )\n      .body_to_s\n\n    body.should contain \"text content\"\n    body.should contain \"html content\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/part/text_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct TextPartTest < ASPEC::TestCase\n  def test_constructor : Nil\n    p = AMIME::Part::Text.new \"content\"\n    p.body.should eq \"content\"\n    p.body_to_s.should eq \"content\"\n    p.media_type.should eq \"text\"\n    p.media_sub_type.should eq \"plain\"\n\n    p = AMIME::Part::Text.new \"content\", sub_type: \"html\"\n    p.media_type.should eq \"text\"\n    p.media_sub_type.should eq \"html\"\n  end\n\n  def test_constructor_io : Nil\n    io = IO::Memory.new \"content\"\n\n    p = AMIME::Part::Text.new io\n    p.body.should eq \"content\"\n    p.body_to_s.should eq \"content\"\n  end\n\n  def test_constructor_real_file : Nil\n    File.open \"#{__DIR__}/../fixtures/content.txt\", \"r\" do |file|\n      p = AMIME::Part::Text.new file\n      p.body.should eq \"content\"\n      p.body_to_s.should eq \"content\"\n    end\n  end\n\n  def test_constructor_file_part : Nil\n    p = AMIME::Part::Text.new(AMIME::Part::File.new(\"#{__DIR__}/../fixtures/content.txt\"))\n    p.body.should eq \"content\"\n    p.body_to_s.should eq \"content\"\n  end\n\n  def test_constructor_unknown_file : Nil\n    expect_raises AMIME::Exception::InvalidArgument, \"File is not readable.\" do\n      AMIME::Part::Text.new(AMIME::Part::File.new(\"#{__DIR__}/../fixtures/\")).body\n    end\n  end\n\n  def test_headers : Nil\n    p = AMIME::Part::Text.new \"content\"\n    p.prepared_headers.should eq AMIME::Header::Collection.new(\n      AMIME::Header::Parameterized.new(\"content-type\", \"text/plain\", {\"charset\" => \"UTF-8\"}),\n      AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"quoted-printable\"),\n    )\n\n    p = AMIME::Part::Text.new \"content\", charset: \"iso-8859-1\"\n    p.prepared_headers.should eq AMIME::Header::Collection.new(\n      AMIME::Header::Parameterized.new(\"content-type\", \"text/plain\", {\"charset\" => \"iso-8859-1\"}),\n      AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"quoted-printable\"),\n    )\n  end\n\n  def test_encoding : Nil\n    p = AMIME::Part::Text.new \"content\", encoding: \"base64\"\n    p.prepared_headers.should eq AMIME::Header::Collection.new(\n      AMIME::Header::Parameterized.new(\"content-type\", \"text/plain\", {\"charset\" => \"UTF-8\"}),\n      AMIME::Header::Unstructured.new(\"content-transfer-encoding\", \"base64\"),\n    )\n  end\n\n  def ptest_custom_encoder_needs_to_be_registered_first : Nil\n  end\n\n  def ptest_override_custom_encoder : Nil\n  end\n\n  def ptest_custom_encoder : Nil\n  end\nend\n"
  },
  {
    "path": "src/components/mime/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"athena-spec\"\n\nrequire \"../src/athena-mime\"\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/mime/spec/types_spec.cr",
    "content": "require \"./abstract_types_guesser_test_case\"\nrequire \"./spec_helper\"\n\nprivate struct MockGuesser\n  include AMIME::TypesGuesserInterface\n\n  def supported? : Bool\n    false\n  end\n\n  def guess_mime_type(path : String | Path) : String?\n    fail \"Should not have been called\"\n  end\nend\n\nstruct MIMETypesTest < AbstractTypesGuesserTestCase\n  protected def guesser : AMIME::TypesGuesserInterface\n    AMIME::Types.new\n  end\n\n  def test_supported : Nil\n    self.guesser.supported?.should be_true\n  end\n\n  def test_no_supported_guessers_raise : Nil\n    guesser = self.guesser\n    guesser.@guessers.clear\n\n    expect_raises AMIME::Exception::Logic, \"Unable to guess the MIME type as no guessers are available.\" do\n      guesser.guess_mime_type \"#{__DIR__}/fixtures/mimetypes/test\"\n    end\n  end\n\n  def test_extensions : Nil\n    types = AMIME::Types.new\n    types.extensions(\"application/mbox\").should eq({\"mbox\"})\n    types.extensions(\"application/postscript\").should eq({\"ai\", \"eps\", \"ps\"})\n    types.extensions(\"image/svg+xml\").should contain \"svg\"\n    types.extensions(\"image/svg\").should contain \"svg\"\n    types.extensions(\"application/whatever-athena\").should be_empty\n  end\n\n  def test_mime_types : Nil\n    types = AMIME::Types.new\n    types.mime_types(\"mbox\").should eq({\"application/mbox\"})\n    types.mime_types(\"ai\").should contain \"application/postscript\"\n    types.mime_types(\"ps\").should contain \"application/postscript\"\n    types.mime_types(\"svg\").should contain \"image/svg+xml\"\n    types.mime_types(\"svg\").should contain \"image/svg\"\n    types.mime_types(\"athena\").should be_empty\n  end\n\n  def test_custom_mimes_types : Nil\n    types = AMIME::Types.new({\n      \"text/bar\" => {\"foo\"},\n      \"text/baz\" => {\"foo\", \"moof\"},\n    })\n\n    types.mime_types(\"foo\").should contain \"text/bar\"\n    types.mime_types(\"foo\").should contain \"text/baz\"\n    types.extensions(\"text/baz\").should eq([\"foo\", \"moof\"])\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/address.cr",
    "content": "# Represents an email address with an optional name.\nstruct Athena::MIME::Address\n  private FROM_STRING_PATTERN = /(?<displayName>[^<]*)<(?<addrSpec>.*)>[^>]*/\n\n  protected class_getter encoder : AMIME::Encoder::AddressEncoderInterface do\n    AMIME::Encoder::IDNAddress.new\n  end\n\n  # Returns the raw email address portion of this Address.\n  # Use `#encoded_address` to get a safe representation for use in a MIME header.\n  #\n  # ```\n  # address = AMIME::Address.new \"first.last@example.com\", \"First Last\"\n  # address.address # => \"first.last@example.com\"\n  # ```\n  getter address : String\n\n  # Returns the raw name portion of this Address, or an empty string if none was set.\n  # Use `#encoded_name` to get a safe representation for use in a MIME header.\n  #\n  # ```\n  # address = AMIME::Address.new \"first.last@example.com\"\n  # address.name # => \"\"\n  #\n  # address = AMIME::Address.new \"first.last@example.com\", \"First Last\"\n  # address.name # => \"First Last\"\n  # ```\n  getter name : String\n\n  # Creates an array of `AMIME::Address` from the provided *addresses*.\n  #\n  # ```\n  # AMIME::Address.create_multiple \"me@example.com\", \"Mr Smith <smith@example.com>\", AMIME::Address.new(\"you@example.com\") # =>\n  # # [\n  # #   Athena::MIME::Address(@address=\"me@example.com\", @name=\"\"),\n  # #   Athena::MIME::Address(@address=\"smith@example.com\", @name=\"Mr Smith\"),\n  # #   Athena::MIME::Address(@address=\"you@example.com\", @name=\"\"),\n  # # ]\n  # ```\n  def self.create_multiple(*addresses : self | String) : Array(self)\n    self.create_multiple addresses\n  end\n\n  # Creates an array of `AMIME::Address` from the provided enumerable *addresses*.\n  #\n  # ```\n  # AMIME::Address.create_multiple({\"me@example.com\", \"Mr Smith <smith@example.com>\", AMIME::Address.new(\"you@example.com\")}) # =>\n  # # [\n  # #   Athena::MIME::Address(@address=\"me@example.com\", @name=\"\"),\n  # #   Athena::MIME::Address(@address=\"smith@example.com\", @name=\"Mr Smith\"),\n  # #   Athena::MIME::Address(@address=\"you@example.com\", @name=\"\"),\n  # # ]\n  # ```\n  def self.create_multiple(addresses : Enumerable(self | String)) : Array(self)\n    addresses.map do |a|\n      self.create a\n    end.to_a\n  end\n\n  # Creates a new `AMIME::Address`.\n  #\n  # If the *address* is already an `AMIME::Address`, it is returned as is.\n  # Otherwise if it's a `String`, then attempt to parse the name and address from the provided string.\n  def self.create(address : self | String) : self\n    return address if address.is_a? self\n\n    return new(address) unless address.includes? '<'\n\n    unless match = address.match FROM_STRING_PATTERN\n      raise AMIME::Exception::InvalidArgument.new \"Could not parse '#{address}' to a '#{self}' instance.\"\n    end\n\n    new match[\"addrSpec\"], match[\"displayName\"].strip(\" '\\\"\")\n  end\n\n  # Creates a new `AMIME::Address` with the provided *address* and optionally *name*.\n  def initialize(address : String, name : String = \"\")\n    @address = address.strip\n    @name = name.gsub(/\\n|\\r/, \"\", options: :no_utf_check).strip\n\n    # TODO: Validate the email\n  end\n\n  # :nodoc:\n  def_clone\n\n  # Writes an encoded representation of this Address to the provided *io* for use in a MIME header.\n  #\n  # ```\n  # AMIME::Address.new \"contact@athenï.org\", \"George\").to_s # => \"\\\"George\\\" <contact@xn--athen-gta.org>\"\n  # ```\n  def to_s(io : IO) : Nil\n    if name = self.encoded_name.presence\n      return io << %(\"#{name}\" <#{self.encoded_address}>)\n    end\n\n    io << self.encoded_address\n  end\n\n  # Returns an encoded representation of `#address` safe to use within a MIME header.\n  #\n  # ```\n  # AMIME::Address.new(\"contact@athenï.org\").encoded_address # => \"xn--athen-gta.org\"\n  # ```\n  def encoded_address : String\n    self.class.encoder.encode @address\n  end\n\n  # Returns an encoded representation of `#name` safe to use within a MIME header.\n  #\n  # ```\n  # AMIME::Address.new(\"us@example.com\", %(Me, \"You)).encoded_name # => \"Me, \\\"You\"\n  # ```\n  def encoded_name : String\n    @name\n  end\n\n  # Returns `true` if this Address's localpart contains at least one non-ASCII character.\n  # Otherwise returns `false`.\n  #\n  # ```\n  # AMIME::Address.new(\"info@dømi.com\").has_unicode_local_part? # => false\n  # AMIME::Address.new(\"dømi@dømi.com\").has_unicode_local_part? # => true\n  # ```\n  def has_unicode_local_part? : Bool\n    local, _, _ = @address.partition '@'\n\n    !local.ascii_only?\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/athena-mime.cr",
    "content": "require \"./address\"\nrequire \"./message\"\nrequire \"./email\"\nrequire \"./draft_email\"\n\nrequire \"./message_converter\"\n\nrequire \"./encoder/*\"\nrequire \"./exception/*\"\nrequire \"./header/*\"\nrequire \"./part/*\"\nrequire \"./part/multipart/*\"\nrequire \"./types\"\nrequire \"./magic_types_guesser\"\nrequire \"./native_types_guesser\"\n\n# Convenience alias to make referencing `Athena::MIME` types easier.\nalias AMIME = Athena::MIME\n\n# Allows manipulating the MIME messages used to send emails and provides utilities related to MIME types.\nmodule Athena::MIME\n  VERSION = \"0.2.1\"\n\n  # Namespace for types related to encoding part of the MIME message.\n  module Encoder; end\n\n  # Both acts as a namespace for exceptions related to the `Athena::MIME` component, as well as a way to check for exceptions from the component.\n  module Exception; end\n\n  # Namespace for the types used to represent MIME headers.\n  module Header; end\n\n  # Namespace for the types used to represent the parts used to compose a MIME message.\n  module Part\n    # Namespace for Multipart related parts.\n    module Multipart; end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/draft_email.cr",
    "content": "# Represent an un-sent `AMIME::Email` message.\n#\n# ```\n# draft_email = AMIME::DraftEmail\n#   .new\n#   .to(\"you@example.com\")\n#   .subject(\"Important Notification\")\n#   .text(\"Lorem ipsum...\")\n#\n# # ...\n# ```\nclass Athena::MIME::DraftEmail < Athena::MIME::Email\n  def initialize(\n    headers : AMIME::Header::Collection? = nil,\n    body : AMIME::Part::Abstract? = nil,\n  )\n    super\n\n    @headers.add_text_header \"x-unsent\", \"1\"\n  end\n\n  # :inherit:\n  def prepared_headers : AMIME::Header::Collection\n    # Override default behavior as draft emails do not need from/sender/date/message-id headers.\n    # These are added by the client that sends the email.\n\n    headers = @headers.clone\n\n    unless headers.has_key? \"mime-version\"\n      headers.add_text_header \"mime-version\", \"1.0\"\n    end\n\n    headers.delete \"bcc\"\n\n    headers\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/email.cr",
    "content": "# Provides a high-level API for creating an email.\n#\n# ```\n# email = AMIME::Email\n#   .new\n#   .from(\"me@example.com\")\n#   .to(\"you@example.com\")\n#   .cc(\"them@example.com\")\n#   .bcc(\"other@example.com\")\n#   .reply_to(\"me@example.com\")\n#   .priority(:high)\n#   .subject(\"Important Notification\")\n#   .text(\"Lorem ipsum...\")\n#   .html(\"<h1>Lorem ipsum</h1> <p>...</p>\")\n#   .attach_from_path(\"/path/to/file.pdf\", \"my-attachment.pdf\")\n#   .embed_from_path(\"/path/to/logo.png\")\n#\n# # ...\n# ```\nclass Athena::MIME::Email < Athena::MIME::Message\n  enum Priority\n    HIGHEST = 1\n    HIGH\n    NORMAL\n    LOW\n    LOWEST\n\n    # :nodoc:\n    def to_s : String\n      \"#{self.value} (#{super.titleize})\"\n    end\n  end\n\n  @text : IO | String | Nil = nil\n\n  # Returns the charset of the `#text_body` for this email.\n  getter text_charset : String? = nil\n\n  @html : IO | String | Nil = nil\n\n  # Returns the charset of the `#html_body` for this email.\n  getter html_charset : String? = nil\n\n  # Returns an array of `AMIME::Part::Data` representing the email's attachments.\n  getter attachments : Array(AMIME::Part::Data) = Array(AMIME::Part::Data).new\n\n  # Used to avoid wrong body hash in DKIM signatures with multiple parts (e.g. HTML + TEXT) due to multiple boundaries.\n  @cached_body : AMIME::Part::Abstract? = nil\n\n  # :nodoc:\n  def ==(other : self)\n    {% if @type.class? %}\n      return true if same?(other)\n    {% end %}\n    {% for field in @type.instance_vars %}\n      return false unless @{{field.id}} == other.@{{field.id}}\n    {% end %}\n    true\n  end\n\n  # Returns the subject of this email, or `nil` if none is set.\n  def subject : String?\n    if header = @headers[\"subject\"]?\n      return header.body.as String\n    end\n  end\n\n  # Sets the subject of this email to the provided *subject*.\n  def subject(subject : String) : self\n    @headers.upsert \"subject\", subject, ->@headers.add_text_header(String, String)\n\n    self\n  end\n\n  # Returns the date of this email, or `nil` if none is set.\n  def date : Time?\n    if header = @headers[\"date\"]?\n      return header.body.as Time\n    end\n  end\n\n  # Sets the date of this email to the provided *date*.\n  def date(date : Time) : self\n    @headers.upsert \"date\", date, ->@headers.add_date_header(String, Time)\n\n    self\n  end\n\n  # Returns the return path of this email, or `nil` if none is set.\n  def return_path : AMIME::Address?\n    if header = @headers[\"return-path\"]?\n      return header.body.as AMIME::Address\n    end\n  end\n\n  # Sets the return path of this email to the provided *address*.\n  def return_path(address : AMIME::Address | String) : self\n    @headers.upsert \"return-path\", AMIME::Address.create(address), ->@headers.add_path_header(String, AMIME::Address)\n\n    self\n  end\n\n  # Returns the sender of this email, or `nil` if none is set.\n  def sender : AMIME::Address?\n    if header = @headers[\"sender\"]?\n      return header.body.as AMIME::Address\n    end\n  end\n\n  # Sets the sender of this email to the provided *address*.\n  def sender(address : AMIME::Address | String) : self\n    @headers.upsert \"sender\", AMIME::Address.create(address), ->@headers.add_mailbox_header(String, AMIME::Address)\n\n    self\n  end\n\n  # Returns the from addresses of this email, or an empty array if none were set.\n  def from : Array(AMIME::Address)\n    if header = @headers[\"from\"]?\n      return header.body.as(Array(AMIME::Address)).dup\n    end\n\n    [] of AMIME::Address\n  end\n\n  # Sets the from addresses of this email to the provided *addresses*, overriding any previously added ones.\n  def from(*addresses : AMIME::Address | String) : self\n    self.set_list_address_header_body \"from\", addresses\n  end\n\n  # Appends the provided *addresses* to the list of current from addresses.\n  def add_from(*addresses : AMIME::Address | String) : self\n    self.add_list_address_header_body \"from\", addresses\n  end\n\n  # Returns the reply-to addresses of this email, or an empty array if none were set.\n  def reply_to : Array(AMIME::Address)\n    if header = @headers[\"reply-to\"]?\n      return header.body.as(Array(AMIME::Address)).dup\n    end\n\n    [] of AMIME::Address\n  end\n\n  # Sets the reply-to addresses of this email to the provided *addresses*, overriding any previously added ones.\n  def reply_to(*addresses : AMIME::Address | String) : self\n    self.set_list_address_header_body \"reply-to\", addresses\n  end\n\n  # Appends the provided *addresses* to the list of current reply-to addresses.\n  def add_reply_to(*addresses : AMIME::Address | String) : self\n    self.add_list_address_header_body \"reply-to\", addresses\n  end\n\n  # Returns the to addresses of this email, or an empty array if none were set.\n  def to : Array(AMIME::Address)\n    if header = @headers[\"to\"]?\n      return header.body.as(Array(AMIME::Address)).dup\n    end\n\n    [] of AMIME::Address\n  end\n\n  # Sets the to addresses of this email to the provided *addresses*, overriding any previously added ones.\n  def to(*addresses : AMIME::Address | String) : self\n    self.set_list_address_header_body \"to\", addresses\n  end\n\n  # Appends the provided *addresses* to the list of current to addresses.\n  def add_to(*addresses : AMIME::Address | String) : self\n    self.add_list_address_header_body \"to\", addresses\n  end\n\n  # Returns the cc addresses of this email, or an empty array if none were set.\n  def cc : Array(AMIME::Address)\n    if header = @headers[\"cc\"]?\n      return header.body.as(Array(AMIME::Address)).dup\n    end\n\n    [] of AMIME::Address\n  end\n\n  # Sets the cc addresses of this email to the provided *addresses*, overriding any previously added ones.\n  def cc(*addresses : AMIME::Address | String) : self\n    self.set_list_address_header_body \"cc\", addresses\n  end\n\n  # Appends the provided *addresses* to the list of current cc addresses.\n  def add_cc(*addresses : AMIME::Address | String) : self\n    self.add_list_address_header_body \"cc\", addresses\n  end\n\n  # Returns the bcc addresses of this email, or an empty array if none were set.\n  def bcc : Array(AMIME::Address)\n    if header = @headers[\"bcc\"]?\n      return header.body.as(Array(AMIME::Address)).dup\n    end\n\n    [] of AMIME::Address\n  end\n\n  # Sets the cc addresses of this email to the provided *addresses*, overriding any previously added ones.\n  def bcc(*addresses : AMIME::Address | String) : self\n    self.set_list_address_header_body \"bcc\", addresses\n  end\n\n  # Appends the provided *addresses* to the list of current bcc addresses.\n  def add_bcc(*addresses : AMIME::Address | String) : self\n    self.add_list_address_header_body \"bcc\", addresses\n  end\n\n  private def add_list_address_header_body(name : String, addresses : Enumerable(AMIME::Address | String)) : self\n    unless header = @headers[name, AMIME::Header::MailboxList]?\n      return self.set_list_address_header_body name, addresses\n    end\n\n    header.add_addresses AMIME::Address.create_multiple addresses\n\n    self\n  end\n\n  private def set_list_address_header_body(name : String, addresses : Enumerable(AMIME::Address | String)) : self\n    addresses = AMIME::Address.create_multiple addresses\n\n    if header = @headers[name]?\n      header.body = addresses\n    else\n      @headers.add_mailbox_list_header name, addresses\n    end\n\n    self\n  end\n\n  # Returns the priority of this email.\n  def priority : AMIME::Email::Priority\n    priority = (@headers.header_body(\"x-priority\") || \"\").as String\n\n    if !(val = priority.to_i?(strict: false)) || !(member = Priority.from_value? val)\n      return Priority::NORMAL\n    end\n\n    member\n  end\n\n  # Sets the priority of this email to the provided *priority*.\n  def priority(priority : AMIME::Email::Priority) : self\n    @headers.upsert \"x-priority\", priority.to_s, ->@headers.add_text_header(String, String)\n\n    self\n  end\n\n  # Sets the textual content of this email to the provided *body*, optionally with the provided *charset*.\n  def text(body : String | IO | Nil, charset : String = \"UTF-8\") : self\n    @cached_body = nil\n    @text = body\n    @text_charset = charset\n\n    self\n  end\n\n  # Returns the textual content of this email.\n  def text_body : IO | String | Nil\n    @text\n  end\n\n  # Sets the HTML content of this email to the provided *body*, optionally with the provided *charset*.\n  def html(body : String | IO | Nil, charset : String = \"UTF-8\") : self\n    @cached_body = nil\n    @html = body\n    @html_charset = charset\n\n    self\n  end\n\n  # Returns the HTML content of this email.\n  def html_body : IO | String | Nil\n    @html\n  end\n\n  # Adds an attachment with the provided *body*, optionally with the provided *name* and *content_type*.\n  def attach(body : String | IO, name : String? = nil, content_type : String? = nil) : self\n    self.add_part AMIME::Part::Data.new body, name, content_type\n  end\n\n  # Attaches the file at the provided *path* as an attachment, optionally with the provided *name* and *content_type*.\n  def attach_from_path(path : String | Path, name : String? = nil, content_type : String? = nil) : self\n    self.add_part AMIME::Part::Data.new AMIME::Part::File.new(path), name, content_type\n  end\n\n  # Adds an embedded attachment with the provided *body*, optionally with the provided *name* and *content_type*.\n  def embed(body : String | IO, name : String? = nil, content_type : String? = nil) : self\n    self.add_part AMIME::Part::Data.new(body, name, content_type).as_inline\n  end\n\n  # Embeds the file at the provided *path* as an attachment, optionally with the provided *name* and *content_type*.\n  def embed_from_path(path : String | Path, name : String? = nil, content_type : String? = nil) : self\n    self.add_part AMIME::Part::Data.new(AMIME::Part::File.new(path), name, content_type).as_inline\n  end\n\n  # Adds the provided *part* as an email attachment.\n  # Consider using `#attach` or `#embed` or one of their variants to provide a simpler API.\n  def add_part(part : AMIME::Part::Data) : self\n    @cached_body = nil\n    @attachments << part\n\n    self\n  end\n\n  # Returns the MIME representation of this email.\n  def body : AMIME::Part::Abstract\n    if body = super\n      return body\n    end\n\n    self.generate_body\n  end\n\n  # Used in specs\n  protected def generate_body : AMIME::Part::Abstract\n    if cached_body = @cached_body\n      return cached_body\n    end\n\n    self.ensure_body_is_valid\n\n    html_part, other_parts, related_parts = self.prepare_parts\n\n    part = (text = @text) ? AMIME::Part::Text.new(text, @text_charset) : nil\n\n    if html_part\n      part = part ? AMIME::Part::Multipart::Alternative.new(part, html_part) : html_part\n    end\n\n    unless related_parts.empty?\n      part = AMIME::Part::Multipart::Related.new part.not_nil!, related_parts\n    end\n\n    unless other_parts.empty?\n      part = if part\n               AMIME::Part::Multipart::Mixed.new other_parts.unshift(part)\n             else\n               AMIME::Part::Multipart::Mixed.new other_parts\n             end\n    end\n\n    @cached_body = part.not_nil!\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity:\n  private def prepare_parts : {AMIME::Part::Text?, Array(AMIME::Part::Abstract), Array(AMIME::Part::Abstract)}\n    names = [] of String\n    html_part = nil\n    if html = @html\n      html_part = AMIME::Part::Text.new html, @html_charset, \"html\"\n      html = html_part.body\n\n      regexes = {\n        /<img\\s+[^>]*src\\s*=\\s*(?:([\\'\"])cid:(.+?)\\1|cid:([^>\\s]+))/i,\n        /<\\w+\\s+[^>]*background\\s*=\\s*(?:([\\'\"])cid:(.+?)\\1|cid:([^>\\s]+))/i,\n      }\n\n      regexes.each do |regex|\n        html.scan regex do |matches|\n          if m2 = matches[2]?\n            names << m2\n          end\n\n          if m3 = matches[3]?\n            names << m3\n          end\n        end\n      end\n\n      names = names.uniq!\n    end\n\n    other_parts = Array(AMIME::Part::Abstract).new\n    related_parts = Hash(String, AMIME::Part::Abstract).new\n\n    @attachments.each do |part|\n      skip_part = names.each do |name|\n        if name != part.name && (!part.has_content_id? || name != part.content_id)\n          next\n        end\n\n        break true if related_parts.has_key? name\n\n        if html && name != part.content_id\n          html = html.gsub(\"cid:#{name}\", \"cid:#{part.content_id}\")\n        end\n        related_parts[name] = part\n        part.name = part.content_id\n        part.as_inline\n\n        break true\n      end\n\n      next if skip_part\n\n      other_parts << part\n    end\n\n    if html_part\n      html_part = AMIME::Part::Text.new html.not_nil!, @html_charset.not_nil!, \"html\"\n    end\n\n    {html_part, other_parts, related_parts.values}\n  end\n\n  # :inherit:\n  def ensure_validity! : Nil\n    self.ensure_body_is_valid\n\n    if \"1\" == @headers.header_body(\"x-unsent\")\n      raise AMIME::Exception::Logic.new \"Cannot send messages marked as 'draft'.\"\n    end\n\n    super\n  end\n\n  private def ensure_body_is_valid : Nil\n    if @text.nil? && @html.nil? && @attachments.empty?\n      raise AMIME::Exception::Logic.new \"A message must have a text or an HTML part or attachments.\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/address_encoder_interface.cr",
    "content": "# Represents an encoder responsible for encoding an email address.\nmodule Athena::MIME::Encoder::AddressEncoderInterface\n  # Returns an encoded version of the provided *address*.\n  abstract def encode(address : String) : String\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/base64_content.cr",
    "content": "require \"./content_encoder_interface\"\n\nrequire \"base64\"\n\n# A content encoder based on the [Base64](https://datatracker.ietf.org/doc/html/rfc4648) spec.\nstruct Athena::MIME::Encoder::Base64Content\n  include Athena::MIME::Encoder::ContentEncoderInterface\n\n  # :inherit:\n  def encode(input : String, charset : String? = \"UTF-8\", first_line_offset : Int32 = 0, max_line_length : Int32? = nil) : String\n    Base64.encode input\n  end\n\n  # :inherit:\n  def encode(input : IO, max_line_length : Int32? = nil) : String\n    Base64.encode input\n  end\n\n  # :inherit:\n  def name : String\n    \"base64\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/content_encoder_interface.cr",
    "content": "require \"./encoder_interface\"\n\n# A more specialized version of `AMIME::Encoder::EncoderInterface` used to encode MIME message contents.\nmodule Athena::MIME::Encoder::ContentEncoderInterface\n  include Athena::MIME::Encoder::EncoderInterface\n\n  # Returns an string representing the encoded contents of the provided *input* IO.\n  # With lines optionally limited to *max_line_length*, depending on the underlying implementation.\n  abstract def encode(input : IO, max_line_length : Int32? = nil) : String\n\n  # Returns the name of this encoder for use within the `content-transfer-encoding` header.\n  abstract def name : String\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/eight_bit_content.cr",
    "content": "require \"./content_encoder_interface\"\n\n# A content encoder based on the [8bit](https://datatracker.ietf.org/doc/html/rfc1428) spec.\nstruct Athena::MIME::Encoder::EightBitContent\n  include Athena::MIME::Encoder::ContentEncoderInterface\n\n  # :inherit:\n  def encode(input : String, charset : String? = \"UTF-8\", first_line_offset : Int32 = 0, max_line_length : Int32? = nil) : String\n    input\n  end\n\n  # :inherit:\n  def encode(input : IO, max_line_length : Int32? = nil) : String\n    input.gets_to_end\n  end\n\n  # :inherit:\n  def name : String\n    \"8bit\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/encoder_interface.cr",
    "content": "module Athena::MIME::Encoder::EncoderInterface\n  # Returns an encoded version of the provided *input*.\n  #\n  # *first_line_offset* may optionally be used depending on the exact implementation if the first line needs to be shorter.\n  # *max_line_length* may optionally be used depending on the exact implementation to customize the max length of each line.\n  abstract def encode(input : String, charset : String? = \"UTF-8\", first_line_offset : Int32 = 0, max_line_length : Int32? = nil) : String\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/idn_address.cr",
    "content": "require \"uri/punycode\"\n\n# An IDNA encoder ([RFC 5980](https://datatracker.ietf.org/doc/html/rfc5980)), defined in [RFC 3492](https://datatracker.ietf.org/doc/html/rfc3492).\n#\n# Encodes the domain part of an address using IDN. This is compatible will all\n# SMTP servers.\n#\n# NOTE: The local part is left as-is. In case there are non-ASCII characters\n# in the local part then it depends on the SMTP Server if this is supported.\nstruct Athena::MIME::Encoder::IDNAddress\n  include Athena::MIME::Encoder::AddressEncoderInterface\n\n  # :inherit:\n  def encode(address : String) : String\n    if address.includes? '@'\n      local, _, domain = address.partition '@'\n\n      unless domain.ascii_only?\n        address = \"#{local}@#{URI::Punycode.to_ascii domain}\"\n      end\n    end\n\n    address\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/mime_header_encoder_interface.cr",
    "content": "# Represents an encoder responsible for encoding the value of MIME headers.\nmodule Athena::MIME::Encoder::MIMEHeaderEncoderInterface\n  # Returns the name of this content encoding scheme.\n  abstract def name : String\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/quoted_printable_content.cr",
    "content": "# A content encoder based on the [quoted-printable](https://datatracker.ietf.org/doc/html/rfc2045#section-6.7) spec.\nstruct Athena::MIME::Encoder::QuotedPrintableContent\n  include Athena::MIME::Encoder::ContentEncoderInterface\n\n  private MAX_LINE_LENGTH = 75\n\n  # Encodes a string as per https://datatracker.ietf.org/doc/html/rfc2045#section-6.7.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity:\n  def self.quoted_printable_encode(string : String) : String\n    # TODO: Refactor this to be more idiomatic.\n\n    line_pos = 0\n\n    String.build do |result|\n      i = 0\n\n      bytesize = string.bytesize\n      bytes = string.bytes\n\n      while i < bytesize\n        c = bytes[i]\n\n        if c == 0x0D && i + 1 < bytesize && bytes[i + 1] == 0x0A\n          result << \"\\r\\n\"\n          i += 2\n          line_pos = 0\n        else\n          if c.chr.control? || c == 0x7F || c >= 0x80 || c == 0x3D || (c == 0x20 && i + 1 < bytesize && bytes[i + 1] == 0x0D)\n            needs_line_break = false\n\n            line_pos += 3\n            if c <= 0x7F && (line_pos) > MAX_LINE_LENGTH\n              needs_line_break = true\n            elsif c > 0x7F && c <= 0xDF && ((line_pos + 3) > MAX_LINE_LENGTH)\n              needs_line_break = true\n            elsif c > 0xDF && c <= 0xEF && ((line_pos + 6) > MAX_LINE_LENGTH)\n              needs_line_break = true\n            elsif c > 0xEF && c <= 0xF4 && ((line_pos + 9) > MAX_LINE_LENGTH)\n              needs_line_break = true\n            end\n\n            if needs_line_break\n              result << \"=\\r\\n\"\n              line_pos = 3\n            end\n\n            result << '='\n            c.to_s result, base: 16, upcase: true, precision: 2\n          else\n            line_pos += 1\n            if line_pos > MAX_LINE_LENGTH\n              result << \"=\\r\\n\"\n              line_pos = 1\n            end\n            result << c.chr\n          end\n          i += 1\n        end\n      end\n    end\n  end\n\n  # :inherit:\n  def encode(input : String, charset : String? = \"UTF-8\", first_line_offset : Int32 = 0, max_line_length : Int32? = nil) : String\n    self.standardize self.class.quoted_printable_encode input\n  end\n\n  # :inherit:\n  def encode(input : IO, max_line_length : Int32? = nil) : String\n    self.encode input.gets_to_end\n  end\n\n  # :inherit:\n  def name : String\n    \"quoted-printable\"\n  end\n\n  private def standardize(string : String) : String\n    # Transform CR or LF to CRLF\n    string = string.gsub /0D(?!=0A)|(?<!=0D)=0A/, \"=0D=0A\"\n\n    # Transform =0D=0A to CRLF\n    string = string\n      .gsub(\"\\t=0D=0A\", \"=09\\r\\n\")\n      .gsub(\" =0D=0A\", \"=20\\r\\n\")\n      .gsub(\"=0D=0A\", \"\\r\\n\")\n\n    return string if string.empty?\n\n    case string[-1].ord\n    when 0x09 then string.sub(-1, \"=09\")\n    when 0x20 then string.sub(-1, \"=20\")\n    else\n      string\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/quoted_printable_mime_header.cr",
    "content": "require \"./encoder_interface\"\n\n# A MIME header encoder based on the [Q](https://datatracker.ietf.org/doc/html/rfc2047#autoid-6) spec.\nstruct Athena::MIME::Encoder::QuotedPrintableMIMEHeader\n  include Athena::MIME::Encoder::MIMEHeaderEncoderInterface\n  include Athena::MIME::Encoder::EncoderInterface\n\n  private ALLOWED_CHARS = begin\n    allowed_bytes = [] of Char\n    allowed_bytes.concat('a'..'z')\n    allowed_bytes.concat('A'..'Z')\n    allowed_bytes.concat('0'..'9')\n    allowed_bytes.concat ['!', '*', '+', '-', '/']\n    allowed_bytes.concat ['=', '\\r', '\\n'] # Not allowed as per spec, but don't want to modify them as they're handled elsewhere\n  end\n\n  # :inherit:\n  def name : String\n    \"Q\"\n  end\n\n  # :inherit:\n  def encode(input : String, charset : String? = \"UTF-8\", first_line_offset : Int32 = 0, max_line_length : Int32? = nil) : String\n    string = AMIME::Encoder::QuotedPrintableContent\n      .quoted_printable_encode(input)\n      .each_char\n      .join { |char| ALLOWED_CHARS.includes?(char) ? char : sprintf(\"=%02X\", char.ord) }\n\n    # TODO: Maybe refactor this logic into an alternate form of `.quoted_printable_encode`?\n    string.gsub(\n      /(?:=20)|(?:=\\r\\n)|[\\? _\\(\\)\\\"#$%&',\\.]/,\n      {\n        \"=20\"   => \"_\",\n        \"=\\r\\n\" => \"\\r\\n\",\n        \" \"     => \"_\",\n      }\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/encoder/rfc2231.cr",
    "content": "require \"uri\"\n\n# An encoder based on the [RFC2231](https://datatracker.ietf.org/doc/html/rfc2231) spec.\nstruct Athena::MIME::Encoder::RFC2231\n  include Athena::MIME::Encoder::EncoderInterface\n\n  # :nodoc:\n  def_clone\n\n  # :inherit:\n  def encode(input : String, charset : String? = \"UTF-8\", first_line_offset : Int32 = 0, max_line_length : Int32? = nil) : String\n    max_line_length = 75 if !max_line_length || 0 >= max_line_length\n\n    String.build input.size do |io|\n      line_length = first_line_offset\n\n      0.step(to: input.size, by: 4, exclusive: true) do |offset|\n        encoded_string = URI.encode_path_segment input[offset, 4]\n\n        if (line_length + encoded_string.bytesize) > max_line_length\n          io << '\\r'\n          io << '\\n'\n          line_length = 0\n        end\n\n        io << encoded_string\n        line_length += encoded_string.bytesize\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/exception/header_not_found.cr",
    "content": "# Raised when trying to retrieve a header by name, but there are no headers with that name.\nclass Athena::MIME::Exception::HeaderNotFound < ::KeyError\n  include Athena::MIME::Exception\nend\n"
  },
  {
    "path": "src/components/mime/src/exception/invalid_argument.cr",
    "content": "class Athena::MIME::Exception::InvalidArgument < ArgumentError\n  include Athena::MIME::Exception\nend\n"
  },
  {
    "path": "src/components/mime/src/exception/logic.cr",
    "content": "# Represents a code logic error that should lead directly to a fix in your code.\nclass Athena::MIME::Exception::Logic < ::Exception\n  include Athena::MIME::Exception\nend\n"
  },
  {
    "path": "src/components/mime/src/exception/runtime.cr",
    "content": "class Athena::MIME::Exception::Runtime < ::RuntimeError\n  include Athena::MIME::Exception\nend\n"
  },
  {
    "path": "src/components/mime/src/header/abstract.cr",
    "content": "require \"./interface\"\n\n# Base type of all headers that provides common utilities and abstractions.\nabstract class Athena::MIME::Header::Abstract(T)\n  include Interface\n\n  private PHRASE_REGEX = Regex.new(%q(^(?:(?:(?:(?:(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?(\\((?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])|(?:(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x19\\x7F]|[\\x21-\\x27\\x2A-\\x5B\\x5D-\\x7E])|(?:\\\\[\\x00-\\x08\\x0B\\x0C\\x0E-\\x7F])|(?1)))*(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?\\)))*(?:(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?(\\((?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])|(?:(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x19\\x7F]|[\\x21-\\x27\\x2A-\\x5B\\x5D-\\x7E])|(?:\\\\[\\x00-\\x08\\x0B\\x0C\\x0E-\\x7F])|(?1)))*(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?\\)))|(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])))?[a-zA-Z0-9!#\\$%&\\'\\*\\+\\-\\/=\\?\\^_`\\{\\}\\|~]+(?:(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?(\\((?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])|(?:(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x19\\x7F]|[\\x21-\\x27\\x2A-\\x5B\\x5D-\\x7E])|(?:\\\\[\\x00-\\x08\\x0B\\x0C\\x0E-\\x7F])|(?1)))*(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?\\)))*(?:(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?(\\((?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])|(?:(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x19\\x7F]|[\\x21-\\x27\\x2A-\\x5B\\x5D-\\x7E])|(?:\\\\[\\x00-\\x08\\x0B\\x0C\\x0E-\\x7F])|(?1)))*(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?\\)))|(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])))?)|(?:(?:(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?(\\((?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])|(?:(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x19\\x7F]|[\\x21-\\x27\\x2A-\\x5B\\x5D-\\x7E])|(?:\\\\[\\x00-\\x08\\x0B\\x0C\\x0E-\\x7F])|(?1)))*(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?\\)))*(?:(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?(\\((?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])|(?:(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x19\\x7F]|[\\x21-\\x27\\x2A-\\x5B\\x5D-\\x7E])|(?:\\\\[\\x00-\\x08\\x0B\\x0C\\x0E-\\x7F])|(?1)))*(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?\\)))|(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])))?\"((?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?(?:(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x19\\x7F]|[\\x21\\x23-\\x5B\\x5D-\\x7E])|(?:\\\\[\\x00-\\x08\\x0B\\x0C\\x0E-\\x7F])))*(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?\"(?:(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?(\\((?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])|(?:(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x19\\x7F]|[\\x21-\\x27\\x2A-\\x5B\\x5D-\\x7E])|(?:\\\\[\\x00-\\x08\\x0B\\x0C\\x0E-\\x7F])|(?1)))*(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?\\)))*(?:(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?(\\((?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])|(?:(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x19\\x7F]|[\\x21-\\x27\\x2A-\\x5B\\x5D-\\x7E])|(?:\\\\[\\x00-\\x08\\x0B\\x0C\\x0E-\\x7F])|(?1)))*(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])?\\)))|(?:(?:[ \\t]*(?:\\r\\n))?[ \\t])))?))+?)$), options: :dollar_endonly)\n\n  protected class_getter encoder : AMIME::Encoder::QuotedPrintableMIMEHeader do\n    AMIME::Encoder::QuotedPrintableMIMEHeader.new\n  end\n\n  # :inherit:\n  getter name : String\n\n  # :inherit:\n  property max_line_length : Int32 = 76\n\n  # Sets the language used in this header.\n  # E.g. `en-us`.\n  property lang : String? = nil\n\n  # Sets the character set used in this header.\n  # Defaults to `UTF-8`.\n  property charset : String = \"UTF-8\"\n\n  def initialize(@name : String); end\n\n  # Returns the body of this header.\n  abstract def body : T\n\n  # Sets the body of this header.\n  abstract def body=(body : T)\n\n  # :nodoc:\n  def_clone\n\n  macro inherited\n    # :nodoc:\n    def ==(other : self)\n      \\{% if @type.class? %}\n        return true if same?(other)\n      \\{% end %}\n      \\{% for field in @type.instance_vars %}\n        return false unless @\\{{field.id}} == other.@\\{{field.id}}\n      \\{% end %}\n      true\n    end\n  end\n\n  # :nodoc:\n  def to_s(io : IO) : Nil\n    # TODO: Is there a way to make this more stream based?\n    io << self.tokens_to_string self.to_tokens\n  end\n\n  # Generates tokens from the given string which include CRLF as individual tokens.\n  private def generate_token_lines(token : String) : Array(String)\n    token.split /(\\r\\n)/, options: :no_utf_check\n  end\n\n  # Takes an array of tokens which appear in the header and turns them into an RFC 2822 compliant string, adding FWSP where needed.\n  private def tokens_to_string(tokens : Array(String)) : String\n    line_pos = 0\n\n    String.build do |io|\n      io << @name << ':' << ' '\n      line_pos += @name.bytesize + 2\n\n      tokens.each do |token|\n        if \"\\r\\n\" == token\n          line_pos = 0\n        elsif (line_pos + token.bytesize) > @max_line_length\n          io << \"\\r\\n\"\n          line_pos = token.bytesize\n        else\n          line_pos += token.bytesize\n        end\n\n        io << token\n      end\n    end\n  end\n\n  # Generate a list of all tokens in the final header.\n  private def to_tokens(string : String? = nil) : Array(String)\n    string = string || self.body_to_s\n\n    tokens = [] of String\n    string.split /(?=[ \\t])/, options: :no_utf_check do |token|\n      tokens.concat self.generate_token_lines token\n    end\n\n    tokens\n  end\n\n  private def token_needs_encoding?(token : String) : Bool\n    return true unless token.valid_encoding?\n\n    token.each_char.any? do |char|\n      ord = char.ord\n\n      0x00 <= ord <= 0x08 ||\n        0x10 <= ord <= 0x19 ||\n        0x7F <= ord <= 0xFF ||\n        char.in?('\\r', '\\n')\n    end\n  end\n\n  # Splits a string into tokens in blocks of words which can be encoded quickly.\n  private def encodable_word_tokens(string : String) : Array(String)\n    tokens = [] of String\n    encoded_token = \"\"\n\n    string.split /(?=[\\t ])/, options: :no_utf_check do |token|\n      if self.token_needs_encoding? token\n        encoded_token += token\n      else\n        unless encoded_token.empty?\n          tokens << encoded_token\n          encoded_token = \"\"\n        end\n        tokens << token\n      end\n    end\n\n    unless encoded_token.empty?\n      tokens << encoded_token\n    end\n\n    tokens\n  end\n\n  # Encode needed word tokens within a string of input.\n  private def encode_words(header : AMIME::Header::Interface, input : String, used_length : Int32 = -1) : String\n    bytes_written = 0\n\n    String.build do |io|\n      tokens = self.encodable_word_tokens input\n\n      tokens.each do |token|\n        # See RFC 2822, Sect 2.2 (really 2.2 ??)\n        if self.token_needs_encoding? token\n          # Dont encode starting WSP\n          case first_char = token[0]\n          when ' ', '\\t'\n            io << first_char\n            bytes_written += first_char.bytesize\n            token = token[1..]\n          end\n\n          if -1 == used_length\n            used_length = \"#{header.name}: \".bytesize + bytes_written\n          end\n\n          encoded_token = self.token_as_encoded_word token, used_length\n          io << encoded_token\n          bytes_written += encoded_token.bytesize\n        else\n          io << token\n          bytes_written += token.bytesize\n        end\n      end\n    end\n  end\n\n  # Encodes the provided *token* for safe insertion into headers.\n  private def token_as_encoded_word(token : String, first_line_offset : Int32 = 0) : String\n    # Adjust first_line_offset to account or space needed for syntax\n    charset_decl = @charset\n    if lang = @lang\n      charset_decl = \"#{charset_decl}*#{lang}\"\n    end\n    encoded_wrapper_length = \"=?#{charset_decl}?#{AMIME::Header::Abstract.encoder.name}??=\".bytesize\n\n    if first_line_offset >= 75\n      # TODO: Is this needed?\n      first_line_offset = 0\n    end\n\n    encoded_text_lines = AMIME::Header::Abstract.encoder.encode(token, @charset, first_line_offset, 75 - encoded_wrapper_length).split \"\\r\\n\"\n\n    if \"iso-2022-jp\" != @charset.downcase\n      encoded_text_lines.map! do |line|\n        \"=?#{charset_decl}?#{AMIME::Header::Abstract.encoder.name}?#{line}?=\"\n      end\n    end\n\n    encoded_text_lines.join \"\\r\\n \"\n  end\n\n  # Produces a compliant, formatted RFC 2822 'phrase' based on the provided *input*.\n  private def create_phrase(header : AMIME::Header::Interface, input : String, charset : String, shorten : Bool = false) : String\n    phrase_str = input\n\n    if !phrase_str.matches? PHRASE_REGEX, options: :no_utf_check\n      # If it's just ASCII try escaping some chars and make it a quoted string\n      if phrase_str.ascii_only?\n        {'\\\\', '\"'}.each do |char|\n          phrase_str = phrase_str.gsub char, \"\\\\#{char}\"\n        end\n        phrase_str = %(\"#{phrase_str}\")\n      else\n        # Otherwise it needs encoded\n        used_length = shorten ? \"#{header.name}: \".bytesize : 0\n\n        phrase_str = self.encode_words header, input, used_length\n      end\n    elsif phrase_str.includes? '('\n      {'\\\\', '\"'}.each do |char|\n        phrase_str = phrase_str.gsub char, \"\\\\#{char}\"\n      end\n      phrase_str = %(\"#{phrase_str}\")\n    end\n\n    phrase_str\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/header/collection.cr",
    "content": "# Represents a collection of MIME headers.\nclass Athena::MIME::Header::Collection\n  private UNIQUE_HEADERS = [\n    \"bcc\",\n    \"cc\",\n    \"date\",\n    \"from\",\n    \"in-reply-to\",\n    \"message-id\",\n    \"references\",\n    \"reply-to\",\n    \"sender\",\n    \"subject\",\n    \"to\",\n  ]\n\n  private HEADER_CLASS_MAP = {\n    \"date\" => AMIME::Header::Date,\n  } of String => AMIME::Header::Abstract.class | Array(AMIME::Header::Abstract.class)\n\n  # :nodoc:\n  enum Type\n    TEXT\n    DATE\n  end\n\n  # Checks the provided *header* to ensure its name and type are compatible.\n  #\n  # ```\n  # AMIME::Header::Collection.check_header_class AMIME::Header::Date.new(\"date\", Time.utc) # => nil\n  # AMIME::Header::Collection.check_header_class AMIME::Header::Unstructured.new(\"date\", \"blah\")\n  # # => AMIME::Exception::Logic: The 'date' header must be an instance of 'Athena::MIME::Header::Date' (got 'Athena::MIME::Header::Unstructured').\n  # ```\n  #\n  # ameba:disable Metrics/CyclomaticComplexity:\n  def self.check_header_class(header : AMIME::Header::Interface) : Nil\n    is_valid, header_classes = case header.name.downcase\n                               when \"date\"        then {header.is_a?(AMIME::Header::Date), {AMIME::Header::Date}}\n                               when \"from\"        then {header.is_a?(AMIME::Header::MailboxList), {AMIME::Header::MailboxList}}\n                               when \"sender\"      then {header.is_a?(AMIME::Header::Mailbox), {AMIME::Header::Mailbox}}\n                               when \"reply-to\"    then {header.is_a?(AMIME::Header::MailboxList), {AMIME::Header::MailboxList}}\n                               when \"to\"          then {header.is_a?(AMIME::Header::MailboxList), {AMIME::Header::MailboxList}}\n                               when \"cc\"          then {header.is_a?(AMIME::Header::MailboxList), {AMIME::Header::MailboxList}}\n                               when \"bcc\"         then {header.is_a?(AMIME::Header::MailboxList), {AMIME::Header::MailboxList}}\n                               when \"message-id\"  then {header.is_a?(AMIME::Header::Identification), {AMIME::Header::Identification}}\n                               when \"return-path\" then {header.is_a?(AMIME::Header::Path), {AMIME::Header::MailboxList}}\n                                 # `in-reply-to` and `references` are less strict than RFC 2822 (3.6.4) to allow users entering the original email's `message-id`, even if that is no valid `message-id`\n                               when \"in-reply-to\" then {header.is_a?(AMIME::Header::Unstructured) || header.is_a?(AMIME::Header::Identification), {AMIME::Header::Unstructured, AMIME::Header::Identification}}\n                               when \"references\"  then {header.is_a?(AMIME::Header::Unstructured) || header.is_a?(AMIME::Header::Identification), {AMIME::Header::Unstructured, AMIME::Header::Identification}}\n                               else\n                                 {true, [] of NoReturn}\n                               end\n\n    return if is_valid\n\n    raise AMIME::Exception::Logic.new \"The '#{header.name}' header must be an instance of '#{header_classes.join(\"' or '\")}' (got '#{header.class}').\"\n  end\n\n  # Returns `true` if the provided *header* name is required to be unique.\n  def self.unique_header?(name : String) : Bool\n    UNIQUE_HEADERS.includes? name.downcase\n  end\n\n  # Returns the\n  getter line_length : Int32 = 76\n\n  @headers = Hash(String, Array(AMIME::Header::Interface)).new { |hash, key| hash[key] = Array(AMIME::Header::Interface).new }\n\n  def self.new(*headers : AMIME::Header::Interface)\n    new headers\n  end\n\n  def initialize(headers : Enumerable(AMIME::Header::Interface) = [] of AMIME::Header::Interface)\n    headers.each do |h|\n      self << h\n    end\n  end\n\n  def_clone\n\n  def_equals @headers, @line_length\n\n  # Sets the max line length to use for this collection.\n  def line_length=(@line_length : Int32) : Nil\n    self.all do |header|\n      header.max_line_length = @line_length\n    end\n  end\n\n  # :nodoc:\n  def to_s(io : IO) : Nil\n    self.all do |header|\n      header.to_s(io)\n      io << '\\r' << '\\n'\n    end\n  end\n\n  # Returns the string representation of each header in the collection as an array of strings.\n  def to_a : Array(String)\n    headers = [] of String\n\n    self.all do |header|\n      headers << header.to_s unless header.body_to_s.blank?\n    end\n\n    headers\n  end\n\n  # Returns an array of all `AMIME::Header::Interface` instances stored within the collection.\n  def all : Array(AMIME::Header::Interface)\n    @headers.each_value.flat_map do |headers|\n      headers\n    end.to_a\n  end\n\n  # Yields each `AMIME::Header::Interface` instance stored within the collection.\n  def all(& : AMIME::Header::Interface ->) : Nil\n    @headers.each_value do |headers|\n      headers.each do |header|\n        yield header\n      end\n    end\n  end\n\n  # Yields each `AMIME::Header::Interface` instance stored within the collection with the provided *name*.\n  def all(name : String, & : AMIME::Header::Interface ->) : Nil\n    @headers[name.downcase]?.try &.each do |header|\n      yield header\n    end\n  end\n\n  # Returns the names of all headers stored within the collection as an array of strings.\n  def names : Array(String)\n    @headers.keys\n  end\n\n  # Removes the header(s) with the provided *name* from the collection.\n  def delete(name : String) : Nil\n    @headers.delete name\n  end\n\n  # Returns the first header with the provided *name*.\n  # Raises an `AMIME::Exception::HeaderNotFound` exception if no header with that name exists.\n  def [](name : String) : AMIME::Header::Interface\n    name = name.downcase\n\n    if !(header_list = @headers[name]?) || !(first_header = header_list.first?)\n      raise AMIME::Exception::HeaderNotFound.new \"No headers with the name '#{name}' exist.\"\n    end\n\n    first_header\n  end\n\n  # Returns the first header with the provided *name* casted to type `T`.\n  # Raises an `AMIME::Exception::HeaderNotFound` exception if no header with that name exists.\n  def [](name : String, _type : T.class) : T forall T\n    self.[name].as T\n  end\n\n  # Returns the first header with the provided *name* casted to type `T`, or `nil` if no headers with that name exist.\n  def []?(name : String, _type : T.class) : T? forall T\n    return unless header = self.[name]?\n\n    header.as T\n  end\n\n  # Returns the first header with the provided *name*, or `nil` if no headers with that name exist.\n  def []?(name : String) : AMIME::Header::Interface?\n    name = name.downcase\n\n    return unless headers = @headers[name]?\n\n    headers.first?\n  end\n\n  # Adds the provided *header* to the collection.\n  def <<(header : AMIME::Header::Interface) : self\n    self.class.check_header_class header\n\n    header.max_line_length = @line_length\n    name = header.name.downcase\n\n    if UNIQUE_HEADERS.includes?(name) && (header_list = @headers[name]?) && header_list.size > 0\n      raise AMIME::Exception::Logic.new \"Cannot set header '#{name}' as it is already defined and must be unique.\"\n    end\n\n    @headers[name] << header\n\n    self\n  end\n\n  # Returns the body of the first header with the provided *name*.\n  def header_body(name : String)\n    return unless header = self.[name]?\n\n    header.body\n  end\n\n  # Returns `true` if the collection contains a header with the provided *name*, otherwise `false`.\n  def has_key?(name : String) : Bool\n    @headers.has_key? name.downcase\n  end\n\n  # Adds an `AMIME::Header::Identification` header to the collection with the provided *name* and *body*.\n  def add_id_header(name : String, body : String | Array(String)) : self\n    self << AMIME::Header::Identification.new name, body\n  end\n\n  # Adds an `AMIME::Header::Unstructured` header to the collection with the provided *name* and *body*.\n  def add_text_header(name : String, body : String) : self\n    self << AMIME::Header::Unstructured.new name, body\n  end\n\n  # Adds an `AMIME::Header::Date` header to the collection with the provided *name* and *body*.\n  def add_date_header(name : String, body : Time) : self\n    self << AMIME::Header::Date.new name, body\n  end\n\n  # Adds an `AMIME::Header::Path` header to the collection with the provided *name* and *body*.\n  def add_path_header(name : String, body : AMIME::Address | String) : self\n    self << AMIME::Header::Path.new name, AMIME::Address.create(body)\n  end\n\n  # Adds an `AMIME::Header::Mailbox` header to the collection with the provided *name* and *body*.\n  def add_mailbox_header(name : String, body : AMIME::Address | String) : self\n    self << AMIME::Header::Mailbox.new name, AMIME::Address.create(body)\n  end\n\n  # Adds an `AMIME::Header::MailboxList` header to the collection with the provided *name* and *body*.\n  def add_mailbox_list_header(name : String, body : Enumerable(AMIME::Address | String)) : self\n    self << AMIME::Header::MailboxList.new name, AMIME::Address.create_multiple(body)\n  end\n\n  # Adds an `AMIME::Header::Parameterized` header to the collection with the provided *name* and *body*.\n  def add_parameterized_header(name : String, body : String, params : Hash(String, String) = {} of String => String) : self\n    self << AMIME::Header::Parameterized.new name, body, params\n  end\n\n  # Returns the value of the provided *parameter* for the first `AMIME::Header::Parameterized` header with the provided *name*.\n  #\n  # ```\n  # headers = AMIME::Header::Collection.new\n  # headers.add_parameterized_header \"content-type\", \"text/plain\", {\"charset\" => \"UTF-8\"}\n  # headers.header_parameter \"content-type\", \"charset\" # => \"UTF-8\"\n  # ```\n  def header_parameter(name : String, parameter : String) : String?\n    header = self.[name]\n\n    unless header.is_a? Parameterized\n      raise AMIME::Exception::Logic.new \"Unable to get parameter '#{parameter}' on header '#{name}' as the header is not of class '#{AMIME::Header::Parameterized}'.\"\n    end\n\n    header[parameter]\n  end\n\n  protected def header_parameter(name : String, parameter : String, value : String?) : Nil\n    header = self.[name]\n\n    unless header.is_a? Parameterized\n      raise AMIME::Exception::Logic.new \"Unable to set parameter '#{parameter}' on header '#{name}' as the header is not of class '#{AMIME::Header::Parameterized}'.\"\n    end\n\n    header[parameter] = value\n  end\n\n  protected def upsert(name : String, body : T, adder : Proc(String, T, Nil)) : Nil forall T\n    if header = self[name]?\n      return header.body = body\n    end\n\n    adder.call name, body\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/header/date.cr",
    "content": "# Represents a `date` MIME Header.\nclass Athena::MIME::Header::Date < Athena::MIME::Header::Abstract(Time)\n  @value : Time\n\n  def initialize(name : String, @value : Time)\n    super name\n  end\n\n  # :inherit:\n  def body : Time\n    @value\n  end\n\n  # :inherit:\n  def body=(body : Time)\n    @value = body\n  end\n\n  protected def body_to_s(io : IO) : Nil\n    @value.to_rfc2822 io\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/header/identification.cr",
    "content": "# Represents an ID MIME Header for something like `message-id` or `content-id` (one or more addresses).\nclass Athena::MIME::Header::Identification < Athena::MIME::Header::Abstract(Array(String))\n  getter ids : Array(String) = [] of String\n  getter ids_as_addresses : Array(AMIME::Address) = [] of AMIME::Address\n\n  def initialize(name : String, value : String | Array(String))\n    super name\n\n    self.id = value\n  end\n\n  # :inherit:\n  def body : Array(String)\n    @ids\n  end\n\n  # :inherit:\n  def body=(body : String | Array(String))\n    self.id = body\n  end\n\n  # Returns the ID used in the value of this header.\n  # If multiple IDs are set, only the first is returned.\n  def id : String?\n    @ids.first?\n  end\n\n  # Sets the ID used in the value of this header.\n  def id=(id : String | Array(String)) : Nil\n    self.ids = id.is_a?(String) ? [id] : id\n  end\n\n  # Sets a collection of IDs to use in the value of this header.\n  def ids=(ids : Array(String)) : Nil\n    @ids.clear\n    @ids_as_addresses.clear\n\n    ids.each do |id|\n      @ids << id\n      @ids_as_addresses << AMIME::Address.new id\n    end\n  end\n\n  protected def body_to_s(io : IO) : Nil\n    @ids_as_addresses.join io, ' ' do |address, i|\n      i << '<'\n      address.to_s i\n      i << '>'\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/header/interface.cr",
    "content": "# An OO representation of a MIME header.\nmodule Athena::MIME::Header::Interface\n  # Returns the name of the header.\n  abstract def name : String\n\n  # Returns the body of the header.\n  # The type depends on the specific concrete class.\n  def body; end\n\n  # Sets the body of the header.\n  # The type depends on the specific concrete class.\n  def body=(body); end\n\n  # Controls how long each header line may be before needing wrapped.\n  # Defaults to `76`.\n  abstract def max_line_length : Int32\n\n  # :ditto:\n  abstract def max_line_length=(max_line_length : Int32)\n\n  # Render this header as a compliant string.\n  abstract def to_s(io : IO) : Nil\n\n  # Returns the header's body, prepared for folding into a final header value.\n  #\n  # This is not necessarily RFC 2822 compliant since folding white space is not added at this stage (see `#to_s` for that).\n  def body_to_s : String\n    String.build do |io|\n      self.body_to_s io\n    end\n  end\n\n  protected abstract def body_to_s(io : IO) : Nil\nend\n"
  },
  {
    "path": "src/components/mime/src/header/mailbox.cr",
    "content": "# Represents a Mailbox MIME Header for something like `sender` (one named address).\nclass Athena::MIME::Header::Mailbox < Athena::MIME::Header::Abstract(Athena::MIME::Address)\n  @value : AMIME::Address\n\n  def initialize(name : String, @value : AMIME::Address)\n    super name\n  end\n\n  # :inherit:\n  def body : AMIME::Address\n    @value\n  end\n\n  # :inherit:\n  def body=(body : AMIME::Address)\n    @value = body\n  end\n\n  protected def body_to_s(io : IO) : Nil\n    str = @value.encoded_address\n\n    if name = @value.name.presence\n      str = \"#{self.create_phrase(self, name, @charset, true)} <#{str}>\"\n    end\n\n    io << str\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/header/mailbox_list.cr",
    "content": "# Represents a Mailbox MIME Header for something like `from`, `to`, `cc`, or `bcc` (one or more named address).\nclass Athena::MIME::Header::MailboxList < Athena::MIME::Header::Abstract(Array(Athena::MIME::Address))\n  @value : Array(AMIME::Address)\n\n  def initialize(name : String, @value : Array(AMIME::Address))\n    super name\n  end\n\n  # :inherit:\n  def body : Array(AMIME::Address)\n    @value\n  end\n\n  # :inherit:\n  def body=(body : Array(AMIME::Address))\n    @value = body\n  end\n\n  # Adds the provided *addresses* to use in the value of this header.\n  def add_addresses(addresses : Array(AMIME::Address)) : Nil\n    @value.concat addresses\n  end\n\n  # Returns the full mailbox list of this Header as an array of valid RFC 2822 strings.\n  def address_strings : Array(String)\n    first = true\n\n    @value.map do |address|\n      str = address.encoded_address\n\n      if name = address.name.presence\n        str = \"#{self.create_phrase(self, name, @charset, first)} <#{str}>\"\n      end\n\n      str\n    ensure\n      first = false\n    end\n  end\n\n  protected def body_to_s(io : IO) : Nil\n    self.address_strings.join io, \", \"\n  end\n\n  private def token_needs_encoding?(token : String) : Bool\n    token.matches?(/[()<>\\[\\]:;@\\,.\"]/, options: :no_utf_check) || super\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/header/parameterized.cr",
    "content": "require \"./unstructured\"\n\n# Represents a MIME Header for something like `content-type` (key/value pairs of metadata included in the value).\nclass Athena::MIME::Header::Parameterized < Athena::MIME::Header::Unstructured\n  # RFC 2231's definition of a token.\n  private TOKEN_REGEX = Regex.new \"^(?:[\\x21\\x23-\\x27\\x2A\\x2B\\x2D\\x2E\\x30-\\x39\\x41-\\x5A\\x5E-\\x7E]+)$\", :dollar_endonly\n\n  # Represents the parameters associated with this header.\n  property parameters : Hash(String, String) = {} of String => String\n\n  @encoder : AMIME::Encoder::RFC2231? = nil\n\n  def initialize(\n    name : String,\n    value : String,\n    parameters : Hash(String, String) = {} of String => String,\n  )\n    super name, value\n\n    parameters.each do |k, v|\n      self.[k] = v\n    end\n\n    if \"content-type\" != name.downcase\n      @encoder = AMIME::Encoder::RFC2231.new\n    end\n  end\n\n  # Returns the value of the parameter with the provided *name*\n  def [](name : String) : String\n    @parameters[name]? || \"\"\n  end\n\n  # Set the value of the parameter with the provided *name* to *value*.\n  def []=(key : String, value : String) : Nil\n    @parameters.merge!({key => value})\n  end\n\n  protected def body_to_s(io : IO) : Nil\n    super\n\n    @parameters.each do |k, v|\n      next unless v.presence\n\n      io << ';' << ' '\n\n      self.write_parameter io, k, v\n    end\n  end\n\n  # Write an RFC 2047 compliant header parameter from the *name* and *value* to *io*.\n  # ameba:disable Metrics/CyclomaticComplexity:\n  private def write_parameter(io : IO, name : String, value : String) : Nil\n    orig_value = value\n\n    encoded = false\n\n    # Allow room for parameter name, indices, \"=\", and DQUOTEs\n    max_value_length = @max_line_length - \"#{name}=*N\\\"\\\";\".bytesize - 1\n    first_line_offset = 0\n\n    # If it's not already a valid parameter\n    if !value.matches? TOKEN_REGEX, options: :no_utf_check\n      # TODO: Text or something else?\n      # ... and it's not ASCII\n      unless value.ascii_only?\n        encoded = true\n\n        # Allow space for the indices, charset, and language\n        max_value_length = @max_line_length - \"#{name}*N*=\\\"\\\";\".bytesize - 1\n        first_line_offset = \"#{@charset}'#{@lang}'\".bytesize\n      end\n\n      if name.in?(\"name\", \"filename\") && \"form-data\" == @value && \"content-disposition\" == @name.downcase && !value.ascii_only?\n        # WHATWG HTML living standard 4.10.21.8 2 specifies:\n        # For field names and filenames for file fields, the result of the\n        # encoding in the previous bullet point must be escaped by replacing\n        # any 0x0A (LF) bytes with the byte sequence `%0A`, 0x0D (CR) with `%0D`\n        # and 0x22 (\") with `%22`.\n        # The user agent must not perform any other escapes.\n        value = value.gsub({'\"' => \"%22\", '\\r' => \"%0D\", '\\n' => \"%0A\"})\n\n        if value.bytesize <= max_line_length\n          io << name\n          io << '='\n          io << '\"'\n          io << value\n          io << '\"'\n          return\n        end\n\n        value = orig_value\n      end\n    end\n\n    # Encode if needed\n    if encoded || value.bytesize > max_value_length\n      if encoder = @encoder\n        value = encoder.encode orig_value, @charset, first_line_offset, max_value_length\n      else\n        # TODO: Do we really need to continue to support this non-RFC compliant flow?\n        value = self.token_as_encoded_word orig_value\n        encoded = false\n      end\n    end\n\n    value_lines = @encoder ? value.split(\"\\r\\n\") : [value]\n\n    if value_lines.size > 1\n      value_lines.each_with_index.join io, \";\\r\\n \" do |(line, idx), i|\n        i << \"#{name}*#{idx}\"\n        self.write_end_of_parameter_value i, line, true, idx.zero?\n      end\n\n      return\n    end\n\n    io << name\n\n    self.write_end_of_parameter_value io, value_lines[0], encoded, true\n  end\n\n  private def write_end_of_parameter_value(io : IO, value : String, encoded : Bool = false, first_line : Bool = false) : Nil\n    force_http_quoting = \"form-data\" == @value && \"content-disposition\" == @name.downcase\n\n    if force_http_quoting || !value.matches?(TOKEN_REGEX, options: :no_utf_check)\n      value = %(\"#{value}\")\n    end\n\n    prepend = '='\n\n    if encoded\n      prepend = \"*=\"\n      if first_line\n        prepend = \"*=#{@charset}'#{@lang}'\"\n      end\n    end\n\n    io << prepend\n    io << value\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/header/path.cr",
    "content": "# Represents a Path MIME Header for something like `return-path` (one address).\nclass Athena::MIME::Header::Path < Athena::MIME::Header::Abstract(Athena::MIME::Address)\n  @value : AMIME::Address\n\n  def initialize(name : String, @value : AMIME::Address)\n    super name\n  end\n\n  # :inherit:\n  def body : AMIME::Address\n    @value\n  end\n\n  # :inherit:\n  def body=(body : AMIME::Address)\n    @value = body\n  end\n\n  protected def body_to_s(io : IO) : Nil\n    io << '<'\n    @value.to_s io\n    io << '>'\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/header/unstructured.cr",
    "content": "# Represents a simple MIME Header (key/value).\nclass Athena::MIME::Header::Unstructured < Athena::MIME::Header::Abstract(String)\n  @value : String\n\n  def initialize(name : String, @value : String)\n    super name\n  end\n\n  # :inherit:\n  def body : String\n    @value\n  end\n\n  # :inherit:\n  def body=(body : String)\n    @value = body\n  end\n\n  protected def body_to_s(io : IO) : Nil\n    io << self.encode_words self, @value\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/magic_types_guesser.cr",
    "content": "require \"./types_guesser_interface\"\n\n# A `AMIME::TypesGuesserInterface` implementation based on [libmagic](https://www.darwinsys.com/file/).\n#\n# Only natively supported on Unix systems and MSYS2 where the `file` package is easily installable.\n# If you have the available lib files on Windows MSVC you may build with `-Dathena_use_libmagic` to explicitly enable the implementation.\nstruct Athena::MIME::MagicTypesGuesser\n  include Athena::MIME::TypesGuesserInterface\n\n  def initialize(\n    @magic_file : String? = nil,\n  ); end\n\n  # As of now `libmagic` is only really supported on Unix and MSYS2.\n  # Respect this by default, but also allow using a dedicated flag to enable just in case.\n  {% if flag?(\"athena_use_libmagic\") || flag?(\"unix\") || (flag?(\"windows\") && flag?(\"gnu\")) %}\n    @[Link(\"magic\", pkg_config: \"libmagic\")]\n    lib LibMagic\n      type MagicT = Void*\n\n      enum Flags\n        MIME_TYPE = 0x000010 # Return the MIME type\n      end\n\n      fun magic_open(\n        flags : LibC::Int,\n      ) : MagicT\n\n      fun magic_close(\n        magic : MagicT,\n      ) : Void\n\n      fun magic_file(\n        magic : MagicT,\n        filename : LibC::Char*,\n      ) : LibC::Char*\n\n      fun magic_load(\n        magic : MagicT,\n        filename : LibC::Char*,\n      ) : LibC::Int\n\n      fun magic_error(\n        magic : MagicT,\n      ) : LibC::Char*\n    end\n\n    # :inherit:\n    def supported? : Bool\n      true\n    end\n\n    # :inherit:\n    def guess_mime_type(path : String | Path) : String?\n      if !File.file?(path) || !File::Info.readable?(path)\n        raise AMIME::Exception::InvalidArgument.new \"The file '#{path}' does not exist or is not readable.\"\n      end\n\n      unless magic = LibMagic.magic_open LibMagic::Flags::MIME_TYPE\n        raise AMIME::Exception::Runtime.new \"Failed to open libmagic.\"\n      end\n\n      begin\n        magic_load = if magic_file = @magic_file\n                       LibMagic.magic_load magic, magic_file\n                     else\n                       LibMagic.magic_load magic, nil\n                     end\n\n        unless magic_load.zero?\n          raise AMIME::Exception::Runtime.new String.new LibMagic.magic_error magic\n        end\n\n        unless mime_type = LibMagic.magic_file magic, path.to_s\n          raise AMIME::Exception::Runtime.new String.new LibMagic.magic_error magic\n        end\n\n        String.new mime_type\n      ensure\n        LibMagic.magic_close magic\n      end\n    end\n  {% else %}\n    # :inherit:\n    def supported? : Bool\n      false\n    end\n\n    # :inherit:\n    def guess_mime_type(path : String | Path) : String?\n      if !File.file?(path) || !File::Info.readable?(path)\n        raise AMIME::Exception::InvalidArgument.new \"The file '#{path}' does not exist or is not readable.\"\n      end\n\n      nil\n    end\n  {% end %}\nend\n"
  },
  {
    "path": "src/components/mime/src/message.cr",
    "content": "# Provides a low-level API for creating an email.\n#\n# See [Creating Raw Email Message](/MIME/#creating-raw-email-messages) for more information.\nclass Athena::MIME::Message\n  # Represents the `AMIME::Header`s a part of this message.\n  property headers : AMIME::Header::Collection\n\n  # Represents the `AMIME::Part`s that comprise this message.\n  property body : AMIME::Part::Abstract?\n\n  def initialize(\n    headers : AMIME::Header::Collection? = nil,\n    @body : AMIME::Part::Abstract? = nil,\n  )\n    # TODO: Need to clone this?\n    @headers = headers || AMIME::Header::Collection.new\n  end\n\n  # Returns a cloned `AMIME::Header::Collection` consisting of a final representation of the headers associated with this message.\n  # I.e. Ensures the message's headers include the required ones.\n  def prepared_headers : AMIME::Header::Collection\n    headers = @headers.clone\n\n    unless headers.has_key? \"from\"\n      unless headers.has_key? \"sender\"\n        raise AMIME::Exception::Logic.new \"An email must have a 'from' or a 'sender' header.\"\n      end\n\n      headers.add_mailbox_list_header \"from\", [headers[\"sender\", AMIME::Header::Mailbox].body]\n    end\n\n    unless headers.has_key? \"mime-version\"\n      headers.add_text_header \"mime-version\", \"1.0\"\n    end\n\n    unless headers.has_key? \"date\"\n      headers.add_date_header \"date\", Time.utc\n    end\n\n    # Determine the \"real\" sender\n    if !headers.has_key?(\"sender\") && (from_addresses = headers[\"from\", AMIME::Header::MailboxList].body) && from_addresses.size > 1\n      headers.add_mailbox_header \"sender\", from_addresses.first\n    end\n\n    unless headers.has_key? \"message-id\"\n      headers.add_id_header \"message-id\", self.generate_message_id\n    end\n\n    # Remove bcc which should _NOT_ be part of the sent message\n    headers.delete \"bcc\"\n\n    headers\n  end\n\n  # :nodoc:\n  def to_s(io : IO) : Nil\n    body = self.body || AMIME::Part::Text.new \"\"\n\n    self.prepared_headers.to_s io\n    body.to_s io\n  end\n\n  # Asserts that this message is in a valid state to be sent, raising an `AMIME::Exception::Logic` error if not.\n  def ensure_validity! : Nil\n    if (!(tos = @headers.header_body(\"to\")) || tos.as(Array(AMIME::Address)).empty?) && (!(ccs = @headers.header_body(\"cc\")) || ccs.as(Array(AMIME::Address)).empty?) && (!(bccs = @headers.header_body(\"bcc\")) || bccs.as(Array(AMIME::Address)).empty?)\n      raise AMIME::Exception::Logic.new \"An email must have a 'to', 'cc', or 'bcc' header.\"\n    end\n\n    if (!(from_addresses = @headers.header_body(\"from\")) || from_addresses.as(Array(AMIME::Address)).empty?) && !@headers.header_body(\"sender\")\n      raise AMIME::Exception::Logic.new \"An email must have a 'from' or a 'sender' header.\"\n    end\n  end\n\n  # Returns a string that uniquely represents this message.\n  def generate_message_id : String\n    sender = if sender_header = @headers[\"sender\", AMIME::Header::Mailbox]?\n               sender_header.body\n             elsif from_header = @headers[\"from\", AMIME::Header::MailboxList]?\n               if (from_addresses = from_header.body).empty?\n                 raise AMIME::Exception::Logic.new \"A 'from' header must have at least one email address.\"\n               end\n\n               from_addresses.first\n             else\n               raise AMIME::Exception::Logic.new \"An email must have a 'from' or 'sender' header.\"\n             end\n\n    \"#{Random::Secure.hex(16)}@#{sender.address.partition('@').last}\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/message_converter.cr",
    "content": "module Athena::MIME::MessageConverter\n  # Utility method to convert `AMIME::Message`s to `AMIME::Email`s.\n  def self.to_email(message : AMIME::Message) : AMIME::Email\n    return message if message.is_a? AMIME::Email\n\n    body = message.body\n\n    case body\n    when AMIME::Part::Text                   then return self.create_email_from_text_part message, body\n    when AMIME::Part::Multipart::Alternative then return self.create_email_from_alternative_part message, body\n    when AMIME::Part::Multipart::Related     then return self.create_email_from_related_part message, body\n    when AMIME::Part::Multipart::Mixed\n      parts = body.parts\n\n      email = case part = parts.first?\n              when AMIME::Part::Multipart::Related     then self.create_email_from_related_part message, part\n              when AMIME::Part::Multipart::Alternative then self.create_email_from_alternative_part message, part\n              when AMIME::Part::Text                   then self.create_email_from_text_part message, part\n              else\n                raise AMIME::Exception::Runtime.new \"Unable to create an Email from an instance of '#{message.class}' as the body is too complex.\"\n              end\n\n      parts.shift\n\n      return self.add_parts email, parts\n    end\n\n    raise AMIME::Exception::Runtime.new \"Unable to create an Email from an instance of '#{message.class}' as the body is too complex.\"\n  end\n\n  private def self.create_email_from_text_part(message : AMIME::Message, part : AMIME::Part::Text) : AMIME::Email\n    if \"text\" == part.media_type && \"plain\" == part.media_sub_type\n      return AMIME::Email\n        .new(message.headers.clone)\n        .text(part.body, part.prepared_headers.header_parameter(\"content-type\", \"charset\") || \"UTF-8\")\n    end\n\n    if \"text\" == part.media_type && \"html\" == part.media_sub_type\n      return AMIME::Email\n        .new(message.headers.clone)\n        .html(part.body, part.prepared_headers.header_parameter(\"content-type\", \"charset\") || \"UTF-8\")\n    end\n\n    raise AMIME::Exception::Runtime.new \"Unable to create an Email from an instance of '#{message.class}' as the body is too complex.\"\n  end\n\n  private def self.create_email_from_alternative_part(message : AMIME::Message, part : AMIME::Part::Multipart::Alternative) : AMIME::Email\n    parts = part.parts\n\n    if 2 == parts.size &&\n       (first_part = parts[0]).is_a?(AMIME::Part::Text) &&\n       \"text\" == first_part.media_type && \"plain\" == first_part.media_sub_type &&\n       (second_part = parts[1]).is_a?(AMIME::Part::Text) &&\n       \"text\" == second_part.media_type && \"html\" == second_part.media_sub_type\n      return AMIME::Email\n        .new(message.headers.clone)\n        .text(first_part.body, first_part.prepared_headers.header_parameter(\"content-type\", \"charset\") || \"UTF-8\")\n        .html(second_part.body, first_part.prepared_headers.header_parameter(\"content-type\", \"charset\") || \"UTF-8\")\n    end\n\n    raise AMIME::Exception::Runtime.new \"Unable to create an Email from an instance of '#{message.class}' as the body is too complex.\"\n  end\n\n  private def self.create_email_from_related_part(message : AMIME::Message, part : AMIME::Part::Multipart::Related) : AMIME::Email\n    parts = part.parts\n\n    first_part = parts.first?\n\n    email = case first_part = parts.first?\n            when AMIME::Part::Multipart::Alternative then self.create_email_from_alternative_part message, first_part\n            when AMIME::Part::Text                   then self.create_email_from_text_part message, first_part\n            else\n              raise AMIME::Exception::Runtime.new \"Unable to create an Email from an instance of '#{message.class}' as the body is too complex.\"\n            end\n\n    parts.shift\n\n    self.add_parts email, parts\n  end\n\n  private def self.add_parts(email : AMIME::Email, parts : Enumerable(AMIME::Part::Abstract)) : AMIME::Email\n    parts.each do |part|\n      unless part.is_a? AMIME::Part::Data\n        raise AMIME::Exception::Runtime.new \"Unable to create an Email from an instance of '#{email.class}' as the body is too complex.\"\n      end\n\n      email.add_part part\n    end\n\n    email\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/native_types_guesser.cr",
    "content": "require \"./types_guesser_interface\"\nrequire \"mime\"\n\n# A `AMIME::TypesGuesserInterface` implementation based Crystal's [MIME](https://crystal-lang.org/api/MIME.html) module.\n#\n# This guesser is mainly intended as a fallback for when `AMIME::MagicTypesGuesser` isn't available (MSVC Windows).\nstruct Athena::MIME::NativeTypesGuesser\n  include Athena::MIME::TypesGuesserInterface\n\n  # :inherit:\n  def supported? : Bool\n    true\n  end\n\n  # :inherit:\n  #\n  # NOTE: Guessing is based solely on the extension of the provided *path*.\n  def guess_mime_type(path : String | Path) : String?\n    if !File.file?(path) || !File::Info.readable?(path)\n      raise AMIME::Exception::InvalidArgument.new \"The file '#{path}' does not exist or is not readable.\"\n    end\n\n    ::MIME.from_filename? path\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/abstract.cr",
    "content": "# Base type of all parts that provides common utilities and abstractions.\nabstract class Athena::MIME::Part::Abstract\n  # Returns the headers associated with this part.\n  getter headers : AMIME::Header::Collection = AMIME::Header::Collection.new\n\n  macro inherited\n    # :nodoc:\n    def ==(other : self)\n      \\{% if @type.class? %}\n        return true if same?(other)\n      \\{% end %}\n      \\{% for field in @type.instance_vars %}\n        return false unless @\\{{field.id}} == other.@\\{{field.id}}\n      \\{% end %}\n      true\n    end\n  end\n\n  protected abstract def body_to_s(io : IO) : Nil\n\n  # Returns the media type of this part.\n  # E.g. `application` within `application/pdf`.\n  abstract def media_type : String\n\n  # Returns the media sub-type of this part.\n  # E.g. `pdf` within `application/pdf`.\n  abstract def media_sub_type : String\n\n  # Returns a cloned `AMIME::Header::Collection` consisting of a final representation of the headers associated with this message.\n  # I.e. Ensures the message's headers include the required ones.\n  def prepared_headers : AMIME::Header::Collection\n    headers = @headers.clone\n\n    headers.upsert \"content-type\", \"#{self.media_type}/#{self.media_sub_type}\", ->headers.add_parameterized_header(String, String)\n\n    headers\n  end\n\n  # Returns a string representation of the body of this part, excluding any headers.\n  def body_to_s : String\n    String.build do |io|\n      self.body_to_s io\n    end\n  end\n\n  # def inspect(io : IO) : Nil\n  #   self.media_type.to_s io\n  #   io << '/'\n  #   self.media_sub_type.to_s io\n  # end\n\n  # :nodoc:\n  def to_s(io : IO) : Nil\n    self.prepared_headers.to_s io\n    io << '\\r' << '\\n'\n    self.body_to_s io\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/abstract_multipart.cr",
    "content": "require \"mime/multipart\"\n\n# Base type of all *multipart* based parts.\nabstract class Athena::MIME::Part::AbstractMultipart < Athena::MIME::Part::Abstract\n  private getter boundary : String { ::MIME::Multipart.generate_boundary }\n\n  # Returns the parts that make up this multipart part.\n  getter parts : Array(Athena::MIME::Part::Abstract) = [] of AMIME::Part::Abstract\n\n  def self.new(*parts : AMIME::Part::Abstract) : self\n    new parts\n  end\n\n  def initialize(parts : Enumerable(AMIME::Part::Abstract) = [] of AMIME::Part::Abstract)\n    parts.each do |part|\n      @parts << part\n    end\n  end\n\n  # :inherit:\n  def media_type : String\n    \"multipart\"\n  end\n\n  # :inherit:\n  def prepared_headers : AMIME::Header::Collection\n    headers = super\n\n    headers.header_parameter \"content-type\", \"boundary\", self.boundary\n\n    headers\n  end\n\n  protected def body_to_s(io : IO) : Nil\n    self.parts.each do |part|\n      io << self.boundary\n      io << '\\r' << '\\n'\n      part.to_s io\n      io << '\\r' << '\\n'\n    end\n\n    io << self.boundary\n    io << '\\r' << '\\n'\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/data.cr",
    "content": "require \"./text\"\n\n# Represents attached/embedded content within the MIME message.\nclass Athena::MIME::Part::Data < Athena::MIME::Part::Text\n  # Creates the part using the contents the file at the provided *path* as the body, optionally with the provided *name* and *content_type*.\n  # The file is lazily read.\n  def self.from_path(path : String | Path, name : String? = nil, content_type : String? = nil) : self\n    new AMIME::Part::File.new(path), name, content_type\n  end\n\n  # Returns the media type of this part based on its body.\n  getter media_type : String\n\n  # Returns the name of the file associated with this part.\n  getter filename : String?\n  @content_id : String?\n\n  def initialize(\n    body : String | IO | AMIME::Part::File,\n    filename : String? = nil,\n    content_type : String? = nil,\n    encoding : String? = nil,\n  )\n    if body.is_a?(AMIME::Part::File) && filename.nil?\n      filename = body.filename\n    end\n\n    content_type ||= body.is_a?(AMIME::Part::File) ? body.content_type : \"application/octet-stream\"\n\n    @media_type, sub_type = content_type.split '/'\n\n    super body, nil, sub_type, encoding\n\n    if filename\n      @filename = filename\n      self.name = filename\n    end\n\n    self.disposition = \"attachment\"\n  end\n\n  # :inherit:\n  def prepared_headers : AMIME::Header::Collection\n    headers = super\n\n    if cid = @content_id\n      headers.upsert \"content-id\", cid, ->headers.add_id_header(String, String)\n    end\n\n    if name = @filename\n      headers.header_parameter \"content-disposition\", \"filename\", name\n    end\n\n    headers\n  end\n\n  # Marks this part as representing embedded content versus an attached file.\n  def as_inline : self\n    self.disposition = \"inline\"\n\n    self\n  end\n\n  # Sets the content ID of this part to the provided *id*.\n  def content_id=(id : String) : self\n    if !id.includes? '@'\n      raise AMIME::Exception::InvalidArgument.new \"The '#{id}' CID is invalid as it does not contain an '@' symbol.\"\n    end\n\n    @content_id = id\n\n    self\n  end\n\n  # Returns the content type of this part.\n  def content_type : String\n    \"#{self.media_type}/#{self.media_sub_type}\"\n  end\n\n  # Returns the content ID of this part, generating a unique one if one was not already set.\n  def content_id : String\n    @content_id ||= self.generate_content_id\n  end\n\n  # Returns `true` if this part has a `#content_id` currently set.\n  def has_content_id? : Bool\n    !@content_id.nil?\n  end\n\n  private def generate_content_id : String\n    \"#{Random::Secure.hex(16)}@athena\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/file.cr",
    "content": "# An abstraction that allows representing a file without needing to keep the file open.\nclass Athena::MIME::Part::File\n  class_getter mime_types : AMIME::Types { AMIME::Types.new }\n\n  # Returns the path to the file on the filesystem.\n  getter path : String\n\n  def initialize(\n    path : String | Path,\n    @filename : String? = nil,\n  )\n    @path = path.to_s\n  end\n\n  # Attempts to guess the content type of the file based on its path.\n  # Falls back to `application/octet-stream`.\n  def content_type : String\n    if mime_type = self.class.mime_types.mime_types(::File.extname(@path).lstrip('.')).first?\n      return mime_type\n    end\n\n    \"application/octet-stream\"\n  end\n\n  # Returns the size of the file in bytes.\n  def size : Int\n    ::File.size @path\n  end\n\n  # Returns the name of the file, inferring it based on the basename of its path if not provided explicitly.\n  def filename : String\n    @filename ||= ::File.basename @path\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/message.cr",
    "content": "# Represents a part that encapsulates an `AMIME::Message`.\nclass Athena::MIME::Part::Message < Athena::MIME::Part::Data\n  @message : AMIME::Message\n\n  def initialize(\n    @message : AMIME::Message,\n  )\n    super \"\", %(#{@message.headers.header_body(\"subject\")}.eml)\n  end\n\n  # :inherit:\n  def media_type : String\n    \"message\"\n  end\n\n  # :inherit:\n  def media_sub_type : String\n    \"rfc822\"\n  end\n\n  # :inherit:\n  def body : String\n    @message.body.to_s\n  end\n\n  # :inherit:\n  def body_to_s : String\n    self.body\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/multipart/alternative.cr",
    "content": "# Represents an `alternative` part.\nclass Athena::MIME::Part::Multipart::Alternative < Athena::MIME::Part::AbstractMultipart\n  # :inherit:\n  def media_sub_type : String\n    \"alternative\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/multipart/digest.cr",
    "content": "# Represents a `digest` part.\nclass Athena::MIME::Part::Multipart::Digest < Athena::MIME::Part::AbstractMultipart\n  # :inherit:\n  def media_sub_type : String\n    \"digest\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/multipart/form.cr",
    "content": "# Represents a `form-data` part.\nclass Athena::MIME::Part::Multipart::Form < Athena::MIME::Part::AbstractMultipart\n  getter parts : Array(Athena::MIME::Part::Abstract) = Array(Athena::MIME::Part::Abstract).new\n\n  def initialize(\n    fields : Hash = {} of NoReturn => NoReturn,\n  )\n    super()\n\n    self.headers.line_length = Int32::MAX\n    self.prepare_fields fields\n  end\n\n  # :inherit:\n  def media_sub_type : String\n    \"form-data\"\n  end\n\n  private def prepare_fields(fields : Hash) : Nil\n    fields.each do |k, v|\n      self.visit_field k, v\n    end\n  end\n\n  private def visit_field(key, value, root : String? = nil) : Nil\n    field_name = root ? \"#{root}[#{key}]\" : key\n\n    case value\n    when Hash\n      value.each do |k, v|\n        self.visit_field k, v, field_name\n      end\n\n      return\n    when Array\n      value.each_with_index do |v, idx|\n        self.visit_field idx.to_s, v, field_name\n      end\n\n      return\n    when String, AMIME::Part::Text\n      self.prepare_part field_name, value\n    else\n      raise AMIME::Exception::InvalidArgument.new \"The value of the form field '#{field_name}' can only be a String, Hash, Array, or AMIME::Part::Text instance, got '#{value.class}'.\"\n    end\n  end\n\n  private def prepare_part(name : String, value : String | AMIME::Part::Text) : Nil\n    case value\n    in String            then self.configure_part name, AMIME::Part::Text.new(value, encoding: \"8bit\")\n    in AMIME::Part::Text then self.configure_part name, value\n    end\n  end\n\n  private def configure_part(name : String, part : AMIME::Part::Text) : Nil\n    part.name = name\n    part.disposition = \"form-data\"\n    part.headers.line_length = Int32::MAX\n    part.encoding = \"8bit\"\n\n    @parts << part\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/multipart/mixed.cr",
    "content": "# Represents a `mixed` part.\nclass Athena::MIME::Part::Multipart::Mixed < Athena::MIME::Part::AbstractMultipart\n  # :inherit:\n  def media_sub_type : String\n    \"mixed\"\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/multipart/related.cr",
    "content": "# Represents a `related` part.\nclass Athena::MIME::Part::Multipart::Related < Athena::MIME::Part::AbstractMultipart\n  @main_part : AMIME::Part::Abstract\n\n  def initialize(\n    @main_part : AMIME::Part::Abstract,\n    parts : Enumerable(AMIME::Part::Abstract),\n  )\n    self.prepare_parts parts\n\n    super parts\n  end\n\n  # :inherit:\n  def media_sub_type : String\n    \"related\"\n  end\n\n  # :inherit:\n  def parts : Array(AMIME::Part::Abstract)\n    super.unshift @main_part\n  end\n\n  private def generate_content_id : String\n    \"#{Random::Secure.hex(16)}@athena\"\n  end\n\n  private def prepare_parts(parts : Enumerable(AMIME::Part::Abstract)) : Nil\n    parts.each do |part|\n      headers = part.headers\n      unless headers.has_key? \"content-id\"\n        headers.upsert \"content-id\", self.generate_content_id, ->headers.add_id_header(String, String)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/part/text.cr",
    "content": "# Represents textual content a part of an email.\nclass Athena::MIME::Part::Text < Athena::MIME::Part::Abstract\n  private DEFAULT_ENCODERS = [\"quoted-printable\", \"base64\", \"8bit\"]\n\n  @@encoders = Hash(String, AMIME::Encoder::ContentEncoderInterface).new\n\n  # Controls the `content-disposition` header value for this part.\n  property disposition : String? = nil\n\n  # Returns the name of this part.\n  property name : String? = nil\n\n  @body : String | IO | AMIME::Part::File\n  protected setter encoding : String\n\n  def initialize(\n    body : String | IO | AMIME::Part::File,\n    @charset : String? = \"UTF-8\",\n    @sub_type : String = \"plain\",\n    encoding : String? = nil,\n  )\n    if body.is_a? AMIME::Part::File\n      if !::File::Info.readable?(body.path) || ::File.directory?(body.path)\n        raise AMIME::Exception::InvalidArgument.new \"File is not readable.\"\n      end\n    end\n\n    @body = body\n\n    if encoding\n      raise AMIME::Exception::InvalidArgument.new \"Unexpected encoding type\" unless DEFAULT_ENCODERS.includes? encoding\n\n      @encoding = encoding\n    else\n      @encoding = choose_encoding\n    end\n  end\n\n  # :inherit:\n  def media_type : String\n    \"text\"\n  end\n\n  # :inherit:\n  def media_sub_type : String\n    @sub_type\n  end\n\n  protected def body_to_s(io : IO) : Nil\n    io << self.encoder.encode self.body, @charset\n  end\n\n  # Returns the raw contents of this part as a string.\n  # Use `#body_to_s` to get a properly encoded representation.\n  def body : String\n    case body = @body\n    in AMIME::Part::File\n      ::File.read body.path\n    in String then body\n    in IO\n      body.rewind if body.responds_to? :rewind\n\n      body.gets_to_end\n    end\n  end\n\n  def prepared_headers : AMIME::Header::Collection\n    headers = super\n\n    headers.upsert \"content-type\", \"#{self.media_type}/#{self.media_sub_type}\", ->headers.add_parameterized_header(String, String)\n\n    if charset = @charset\n      headers.header_parameter \"content-type\", \"charset\", charset\n    end\n\n    if (name = @name.presence) && (\"form-data\" != @disposition)\n      headers.header_parameter \"content-type\", \"name\", name\n    end\n\n    headers.upsert \"content-transfer-encoding\", @encoding, ->headers.add_text_header(String, String)\n\n    if !headers.has_key?(\"content-disposition\") && (disposition = @disposition)\n      headers.upsert \"content-disposition\", disposition, ->headers.add_parameterized_header(String, String)\n\n      if name = @name\n        headers.header_parameter \"content-disposition\", \"name\", name\n      end\n    end\n\n    headers\n  end\n\n  private def choose_encoding : String\n    @charset.nil? ? \"base64\" : \"quoted-printable\"\n  end\n\n  private def encoder : AMIME::Encoder::ContentEncoderInterface\n    case @encoding\n    when \"8bit\"             then @@encoders[@encoding] ||= AMIME::Encoder::EightBitContent.new\n    when \"quoted-printable\" then @@encoders[@encoding] ||= AMIME::Encoder::QuotedPrintableContent.new\n    when \"base64\"           then @@encoders[@encoding] ||= AMIME::Encoder::Base64Content.new\n    else\n      @@encoders[@encoding]\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/types/data.cr",
    "content": "class Athena::MIME::Types\n  # Map MIME types to their default prefered extension.\n  #\n  # Last updated from upstream on 2025-05-11.\n  private MAP = {\n    \"application/acrobat\"                                                       => {\"pdf\"},\n    \"application/andrew-inset\"                                                  => {\"ez\"},\n    \"application/annodex\"                                                       => {\"anx\"},\n    \"application/appinstaller\"                                                  => {\"appinstaller\"},\n    \"application/applixware\"                                                    => {\"aw\"},\n    \"application/appx\"                                                          => {\"appx\"},\n    \"application/appxbundle\"                                                    => {\"appxbundle\"},\n    \"application/atom+xml\"                                                      => {\"atom\"},\n    \"application/atomcat+xml\"                                                   => {\"atomcat\"},\n    \"application/atomdeleted+xml\"                                               => {\"atomdeleted\"},\n    \"application/atomsvc+xml\"                                                   => {\"atomsvc\"},\n    \"application/atsc-dwd+xml\"                                                  => {\"dwd\"},\n    \"application/atsc-held+xml\"                                                 => {\"held\"},\n    \"application/atsc-rsat+xml\"                                                 => {\"rsat\"},\n    \"application/automationml-aml+xml\"                                          => {\"aml\"},\n    \"application/automationml-amlx+zip\"                                         => {\"amlx\"},\n    \"application/bat\"                                                           => {\"bat\"},\n    \"application/bdoc\"                                                          => {\"bdoc\"},\n    \"application/buildstream+yaml\"                                              => {\"bst\"},\n    \"application/bzip2\"                                                         => {\"bz2\", \"bz\"},\n    \"application/calendar+xml\"                                                  => {\"xcs\"},\n    \"application/cbor\"                                                          => {\"cbor\"},\n    \"application/ccxml+xml\"                                                     => {\"ccxml\"},\n    \"application/cdfx+xml\"                                                      => {\"cdfx\"},\n    \"application/cdmi-capability\"                                               => {\"cdmia\"},\n    \"application/cdmi-container\"                                                => {\"cdmic\"},\n    \"application/cdmi-domain\"                                                   => {\"cdmid\"},\n    \"application/cdmi-object\"                                                   => {\"cdmio\"},\n    \"application/cdmi-queue\"                                                    => {\"cdmiq\"},\n    \"application/cdr\"                                                           => {\"cdr\"},\n    \"application/coreldraw\"                                                     => {\"cdr\"},\n    \"application/cpl+xml\"                                                       => {\"cpl\"},\n    \"application/csv\"                                                           => {\"csv\"},\n    \"application/cu-seeme\"                                                      => {\"cu\"},\n    \"application/cwl\"                                                           => {\"cwl\"},\n    \"application/dash+xml\"                                                      => {\"mpd\"},\n    \"application/dash-patch+xml\"                                                => {\"mpp\"},\n    \"application/davmount+xml\"                                                  => {\"davmount\"},\n    \"application/dbase\"                                                         => {\"dbf\"},\n    \"application/dbf\"                                                           => {\"dbf\"},\n    \"application/dicom\"                                                         => {\"dcm\"},\n    \"application/docbook+xml\"                                                   => {\"dbk\", \"docbook\"},\n    \"application/dssc+der\"                                                      => {\"dssc\"},\n    \"application/dssc+xml\"                                                      => {\"xdssc\"},\n    \"application/ecmascript\"                                                    => {\"ecma\", \"es\"},\n    \"application/emf\"                                                           => {\"emf\"},\n    \"application/emma+xml\"                                                      => {\"emma\"},\n    \"application/emotionml+xml\"                                                 => {\"emotionml\"},\n    \"application/epub+zip\"                                                      => {\"epub\"},\n    \"application/exi\"                                                           => {\"exi\"},\n    \"application/express\"                                                       => {\"exp\"},\n    \"application/fdf\"                                                           => {\"fdf\"},\n    \"application/fdt+xml\"                                                       => {\"fdt\"},\n    \"application/fits\"                                                          => {\"fits\", \"fit\", \"fts\"},\n    \"application/font-tdpfr\"                                                    => {\"pfr\"},\n    \"application/font-woff\"                                                     => {\"woff\"},\n    \"application/futuresplash\"                                                  => {\"swf\", \"spl\"},\n    \"application/geo+json\"                                                      => {\"geojson\", \"geo.json\"},\n    \"application/gml+xml\"                                                       => {\"gml\"},\n    \"application/gnunet-directory\"                                              => {\"gnd\"},\n    \"application/gpx\"                                                           => {\"gpx\"},\n    \"application/gpx+xml\"                                                       => {\"gpx\"},\n    \"application/gxf\"                                                           => {\"gxf\"},\n    \"application/gzip\"                                                          => {\"gz\"},\n    \"application/hjson\"                                                         => {\"hjson\"},\n    \"application/hta\"                                                           => {\"hta\"},\n    \"application/hyperstudio\"                                                   => {\"stk\"},\n    \"application/ico\"                                                           => {\"ico\"},\n    \"application/ics\"                                                           => {\"vcs\", \"ics\", \"ifb\", \"icalendar\"},\n    \"application/illustrator\"                                                   => {\"ai\"},\n    \"application/inkml+xml\"                                                     => {\"ink\", \"inkml\"},\n    \"application/ipfix\"                                                         => {\"ipfix\"},\n    \"application/its+xml\"                                                       => {\"its\"},\n    \"application/java\"                                                          => {\"class\"},\n    \"application/java-archive\"                                                  => {\"jar\", \"war\", \"ear\"},\n    \"application/java-byte-code\"                                                => {\"class\"},\n    \"application/java-serialized-object\"                                        => {\"ser\"},\n    \"application/java-vm\"                                                       => {\"class\"},\n    \"application/javascript\"                                                    => {\"js\", \"cjs\", \"jsm\", \"mjs\"},\n    \"application/jrd+json\"                                                      => {\"jrd\"},\n    \"application/json\"                                                          => {\"json\", \"map\"},\n    \"application/json-patch+json\"                                               => {\"json-patch\"},\n    \"application/json5\"                                                         => {\"json5\"},\n    \"application/jsonml+json\"                                                   => {\"jsonml\"},\n    \"application/ld+json\"                                                       => {\"jsonld\"},\n    \"application/lgr+xml\"                                                       => {\"lgr\"},\n    \"application/lost+xml\"                                                      => {\"lostxml\"},\n    \"application/lotus123\"                                                      => {\"123\", \"wk1\", \"wk3\", \"wk4\", \"wks\"},\n    \"application/m3u\"                                                           => {\"m3u\", \"m3u8\", \"vlc\"},\n    \"application/mac-binhex40\"                                                  => {\"hqx\"},\n    \"application/mac-compactpro\"                                                => {\"cpt\"},\n    \"application/mads+xml\"                                                      => {\"mads\"},\n    \"application/manifest+json\"                                                 => {\"webmanifest\"},\n    \"application/marc\"                                                          => {\"mrc\"},\n    \"application/marcxml+xml\"                                                   => {\"mrcx\"},\n    \"application/mathematica\"                                                   => {\"ma\", \"nb\", \"mb\"},\n    \"application/mathml+xml\"                                                    => {\"mathml\", \"mml\"},\n    \"application/mbox\"                                                          => {\"mbox\"},\n    \"application/mdb\"                                                           => {\"mdb\"},\n    \"application/media-policy-dataset+xml\"                                      => {\"mpf\"},\n    \"application/mediaservercontrol+xml\"                                        => {\"mscml\"},\n    \"application/metalink+xml\"                                                  => {\"metalink\"},\n    \"application/metalink4+xml\"                                                 => {\"meta4\"},\n    \"application/mets+xml\"                                                      => {\"mets\"},\n    \"application/microsoftpatch\"                                                => {\"msp\"},\n    \"application/microsoftupdate\"                                               => {\"msu\"},\n    \"application/mmt-aei+xml\"                                                   => {\"maei\"},\n    \"application/mmt-usd+xml\"                                                   => {\"musd\"},\n    \"application/mods+xml\"                                                      => {\"mods\"},\n    \"application/mp21\"                                                          => {\"m21\", \"mp21\"},\n    \"application/mp4\"                                                           => {\"mp4\", \"mpg4\", \"mp4s\", \"m4p\"},\n    \"application/mrb-consumer+xml\"                                              => {\"xdf\"},\n    \"application/mrb-publish+xml\"                                               => {\"xdf\"},\n    \"application/ms-tnef\"                                                       => {\"tnef\", \"tnf\"},\n    \"application/msaccess\"                                                      => {\"mdb\"},\n    \"application/msexcel\"                                                       => {\"xls\", \"xlc\", \"xll\", \"xlm\", \"xlw\", \"xla\", \"xlt\", \"xld\"},\n    \"application/msix\"                                                          => {\"msix\"},\n    \"application/msixbundle\"                                                    => {\"msixbundle\"},\n    \"application/mspowerpoint\"                                                  => {\"ppz\", \"ppt\", \"pps\", \"pot\"},\n    \"application/msword\"                                                        => {\"doc\", \"dot\"},\n    \"application/msword-template\"                                               => {\"dot\"},\n    \"application/mxf\"                                                           => {\"mxf\"},\n    \"application/n-quads\"                                                       => {\"nq\"},\n    \"application/n-triples\"                                                     => {\"nt\"},\n    \"application/nappdf\"                                                        => {\"pdf\"},\n    \"application/node\"                                                          => {\"cjs\"},\n    \"application/octet-stream\"                                                  => {\"bin\", \"dms\", \"lrf\", \"mar\", \"so\", \"dist\", \"distz\", \"pkg\", \"bpk\", \"dump\", \"elc\", \"deploy\", \"exe\", \"dll\", \"deb\", \"dmg\", \"iso\", \"img\", \"msi\", \"msp\", \"msm\", \"buffer\"},\n    \"application/oda\"                                                           => {\"oda\"},\n    \"application/oebps-package+xml\"                                             => {\"opf\"},\n    \"application/ogg\"                                                           => {\"ogx\"},\n    \"application/omdoc+xml\"                                                     => {\"omdoc\"},\n    \"application/onenote\"                                                       => {\"onetoc\", \"onetoc2\", \"onetmp\", \"onepkg\"},\n    \"application/ovf\"                                                           => {\"ova\"},\n    \"application/owl+xml\"                                                       => {\"owx\"},\n    \"application/oxps\"                                                          => {\"oxps\"},\n    \"application/p2p-overlay+xml\"                                               => {\"relo\"},\n    \"application/patch-ops-error+xml\"                                           => {\"xer\"},\n    \"application/pcap\"                                                          => {\"pcap\", \"cap\", \"dmp\"},\n    \"application/pdf\"                                                           => {\"pdf\"},\n    \"application/pgp\"                                                           => {\"pgp\", \"gpg\", \"asc\"},\n    \"application/pgp-encrypted\"                                                 => {\"pgp\", \"gpg\", \"asc\"},\n    \"application/pgp-keys\"                                                      => {\"asc\", \"skr\", \"pkr\", \"pgp\", \"gpg\", \"key\"},\n    \"application/pgp-signature\"                                                 => {\"sig\", \"asc\", \"pgp\", \"gpg\"},\n    \"application/photoshop\"                                                     => {\"psd\"},\n    \"application/pics-rules\"                                                    => {\"prf\"},\n    \"application/pkcs10\"                                                        => {\"p10\"},\n    \"application/pkcs12\"                                                        => {\"p12\", \"pfx\"},\n    \"application/pkcs7-mime\"                                                    => {\"p7m\", \"p7c\"},\n    \"application/pkcs7-signature\"                                               => {\"p7s\"},\n    \"application/pkcs8\"                                                         => {\"p8\"},\n    \"application/pkcs8-encrypted\"                                               => {\"p8e\"},\n    \"application/pkix-attr-cert\"                                                => {\"ac\"},\n    \"application/pkix-cert\"                                                     => {\"cer\"},\n    \"application/pkix-crl\"                                                      => {\"crl\"},\n    \"application/pkix-pkipath\"                                                  => {\"pkipath\"},\n    \"application/pkixcmp\"                                                       => {\"pki\"},\n    \"application/pls\"                                                           => {\"pls\"},\n    \"application/pls+xml\"                                                       => {\"pls\"},\n    \"application/postscript\"                                                    => {\"ai\", \"eps\", \"ps\"},\n    \"application/powerpoint\"                                                    => {\"ppz\", \"ppt\", \"pps\", \"pot\"},\n    \"application/provenance+xml\"                                                => {\"provx\"},\n    \"application/prs.cww\"                                                       => {\"cww\"},\n    \"application/prs.wavefront-obj\"                                             => {\"obj\"},\n    \"application/prs.xsf+xml\"                                                   => {\"xsf\"},\n    \"application/pskc+xml\"                                                      => {\"pskcxml\"},\n    \"application/ram\"                                                           => {\"ram\"},\n    \"application/raml+yaml\"                                                     => {\"raml\"},\n    \"application/rdf+xml\"                                                       => {\"rdf\", \"owl\", \"rdfs\"},\n    \"application/reginfo+xml\"                                                   => {\"rif\"},\n    \"application/relax-ng-compact-syntax\"                                       => {\"rnc\"},\n    \"application/resource-lists+xml\"                                            => {\"rl\"},\n    \"application/resource-lists-diff+xml\"                                       => {\"rld\"},\n    \"application/rls-services+xml\"                                              => {\"rs\"},\n    \"application/route-apd+xml\"                                                 => {\"rapd\"},\n    \"application/route-s-tsid+xml\"                                              => {\"sls\"},\n    \"application/route-usd+xml\"                                                 => {\"rusd\"},\n    \"application/rpki-ghostbusters\"                                             => {\"gbr\"},\n    \"application/rpki-manifest\"                                                 => {\"mft\"},\n    \"application/rpki-roa\"                                                      => {\"roa\"},\n    \"application/rsd+xml\"                                                       => {\"rsd\"},\n    \"application/rss+xml\"                                                       => {\"rss\"},\n    \"application/rtf\"                                                           => {\"rtf\"},\n    \"application/sbml+xml\"                                                      => {\"sbml\"},\n    \"application/schema+json\"                                                   => {\"json\"},\n    \"application/scvp-cv-request\"                                               => {\"scq\"},\n    \"application/scvp-cv-response\"                                              => {\"scs\"},\n    \"application/scvp-vp-request\"                                               => {\"spq\"},\n    \"application/scvp-vp-response\"                                              => {\"spp\"},\n    \"application/sdp\"                                                           => {\"sdp\"},\n    \"application/senml+xml\"                                                     => {\"senmlx\"},\n    \"application/sensml+xml\"                                                    => {\"sensmlx\"},\n    \"application/set-payment-initiation\"                                        => {\"setpay\"},\n    \"application/set-registration-initiation\"                                   => {\"setreg\"},\n    \"application/shf+xml\"                                                       => {\"shf\"},\n    \"application/sieve\"                                                         => {\"siv\", \"sieve\"},\n    \"application/smil\"                                                          => {\"smil\", \"smi\", \"sml\", \"kino\"},\n    \"application/smil+xml\"                                                      => {\"smi\", \"smil\", \"sml\", \"kino\"},\n    \"application/sparql-query\"                                                  => {\"rq\", \"qs\"},\n    \"application/sparql-results+xml\"                                            => {\"srx\"},\n    \"application/sql\"                                                           => {\"sql\"},\n    \"application/srgs\"                                                          => {\"gram\"},\n    \"application/srgs+xml\"                                                      => {\"grxml\"},\n    \"application/sru+xml\"                                                       => {\"sru\"},\n    \"application/ssdl+xml\"                                                      => {\"ssdl\"},\n    \"application/ssml+xml\"                                                      => {\"ssml\"},\n    \"application/stuffit\"                                                       => {\"sit\", \"hqx\"},\n    \"application/swid+xml\"                                                      => {\"swidtag\"},\n    \"application/tei+xml\"                                                       => {\"tei\", \"teicorpus\"},\n    \"application/tga\"                                                           => {\"tga\", \"icb\", \"tpic\", \"vda\", \"vst\"},\n    \"application/thraud+xml\"                                                    => {\"tfi\"},\n    \"application/timestamped-data\"                                              => {\"tsd\"},\n    \"application/toml\"                                                          => {\"toml\"},\n    \"application/trig\"                                                          => {\"trig\"},\n    \"application/ttml+xml\"                                                      => {\"ttml\"},\n    \"application/typescript\"                                                    => {\"cts\", \"mts\", \"ts\"},\n    \"application/ubjson\"                                                        => {\"ubj\"},\n    \"application/urc-ressheet+xml\"                                              => {\"rsheet\"},\n    \"application/urc-targetdesc+xml\"                                            => {\"td\"},\n    \"application/vnd.1000minds.decision-model+xml\"                              => {\"1km\"},\n    \"application/vnd.3gpp.pic-bw-large\"                                         => {\"plb\"},\n    \"application/vnd.3gpp.pic-bw-small\"                                         => {\"psb\"},\n    \"application/vnd.3gpp.pic-bw-var\"                                           => {\"pvb\"},\n    \"application/vnd.3gpp2.tcap\"                                                => {\"tcap\"},\n    \"application/vnd.3m.post-it-notes\"                                          => {\"pwn\"},\n    \"application/vnd.accpac.simply.aso\"                                         => {\"aso\"},\n    \"application/vnd.accpac.simply.imp\"                                         => {\"imp\"},\n    \"application/vnd.acucobol\"                                                  => {\"acu\"},\n    \"application/vnd.acucorp\"                                                   => {\"atc\", \"acutc\"},\n    \"application/vnd.adobe.air-application-installer-package+zip\"               => {\"air\"},\n    \"application/vnd.adobe.flash.movie\"                                         => {\"swf\", \"spl\"},\n    \"application/vnd.adobe.formscentral.fcdt\"                                   => {\"fcdt\"},\n    \"application/vnd.adobe.fxp\"                                                 => {\"fxp\", \"fxpl\"},\n    \"application/vnd.adobe.illustrator\"                                         => {\"ai\"},\n    \"application/vnd.adobe.xdp+xml\"                                             => {\"xdp\"},\n    \"application/vnd.adobe.xfdf\"                                                => {\"xfdf\"},\n    \"application/vnd.age\"                                                       => {\"age\"},\n    \"application/vnd.ahead.space\"                                               => {\"ahead\"},\n    \"application/vnd.airzip.filesecure.azf\"                                     => {\"azf\"},\n    \"application/vnd.airzip.filesecure.azs\"                                     => {\"azs\"},\n    \"application/vnd.amazon.ebook\"                                              => {\"azw\"},\n    \"application/vnd.amazon.mobi8-ebook\"                                        => {\"azw3\", \"kfx\"},\n    \"application/vnd.americandynamics.acc\"                                      => {\"acc\"},\n    \"application/vnd.amiga.ami\"                                                 => {\"ami\"},\n    \"application/vnd.android.package-archive\"                                   => {\"apk\"},\n    \"application/vnd.anser-web-certificate-issue-initiation\"                    => {\"cii\"},\n    \"application/vnd.anser-web-funds-transfer-initiation\"                       => {\"fti\"},\n    \"application/vnd.antix.game-component\"                                      => {\"atx\"},\n    \"application/vnd.apache.parquet\"                                            => {\"parquet\"},\n    \"application/vnd.appimage\"                                                  => {\"appimage\"},\n    \"application/vnd.apple.installer+xml\"                                       => {\"mpkg\"},\n    \"application/vnd.apple.keynote\"                                             => {\"key\", \"keynote\"},\n    \"application/vnd.apple.mpegurl\"                                             => {\"m3u8\", \"m3u\"},\n    \"application/vnd.apple.numbers\"                                             => {\"numbers\"},\n    \"application/vnd.apple.pages\"                                               => {\"pages\"},\n    \"application/vnd.apple.pkpass\"                                              => {\"pkpass\"},\n    \"application/vnd.apple.pkpasses\"                                            => {\"pkpasses\"},\n    \"application/vnd.aristanetworks.swi\"                                        => {\"swi\"},\n    \"application/vnd.astraea-software.iota\"                                     => {\"iota\"},\n    \"application/vnd.audiograph\"                                                => {\"aep\"},\n    \"application/vnd.balsamiq.bmml+xml\"                                         => {\"bmml\"},\n    \"application/vnd.blueice.multipass\"                                         => {\"mpm\"},\n    \"application/vnd.bmi\"                                                       => {\"bmi\"},\n    \"application/vnd.businessobjects\"                                           => {\"rep\"},\n    \"application/vnd.chemdraw+xml\"                                              => {\"cdxml\"},\n    \"application/vnd.chess-pgn\"                                                 => {\"pgn\"},\n    \"application/vnd.chipnuts.karaoke-mmd\"                                      => {\"mmd\"},\n    \"application/vnd.cinderella\"                                                => {\"cdy\"},\n    \"application/vnd.citationstyles.style+xml\"                                  => {\"csl\"},\n    \"application/vnd.claymore\"                                                  => {\"cla\"},\n    \"application/vnd.cloanto.rp9\"                                               => {\"rp9\"},\n    \"application/vnd.clonk.c4group\"                                             => {\"c4g\", \"c4d\", \"c4f\", \"c4p\", \"c4u\"},\n    \"application/vnd.cluetrust.cartomobile-config\"                              => {\"c11amc\"},\n    \"application/vnd.cluetrust.cartomobile-config-pkg\"                          => {\"c11amz\"},\n    \"application/vnd.coffeescript\"                                              => {\"coffee\"},\n    \"application/vnd.comicbook+zip\"                                             => {\"cbz\"},\n    \"application/vnd.comicbook-rar\"                                             => {\"cbr\"},\n    \"application/vnd.commonspace\"                                               => {\"csp\"},\n    \"application/vnd.contact.cmsg\"                                              => {\"cdbcmsg\"},\n    \"application/vnd.corel-draw\"                                                => {\"cdr\"},\n    \"application/vnd.cosmocaller\"                                               => {\"cmc\"},\n    \"application/vnd.crick.clicker\"                                             => {\"clkx\"},\n    \"application/vnd.crick.clicker.keyboard\"                                    => {\"clkk\"},\n    \"application/vnd.crick.clicker.palette\"                                     => {\"clkp\"},\n    \"application/vnd.crick.clicker.template\"                                    => {\"clkt\"},\n    \"application/vnd.crick.clicker.wordbank\"                                    => {\"clkw\"},\n    \"application/vnd.criticaltools.wbs+xml\"                                     => {\"wbs\"},\n    \"application/vnd.ctc-posml\"                                                 => {\"pml\"},\n    \"application/vnd.cups-ppd\"                                                  => {\"ppd\"},\n    \"application/vnd.curl.car\"                                                  => {\"car\"},\n    \"application/vnd.curl.pcurl\"                                                => {\"pcurl\"},\n    \"application/vnd.dart\"                                                      => {\"dart\"},\n    \"application/vnd.data-vision.rdz\"                                           => {\"rdz\"},\n    \"application/vnd.dbf\"                                                       => {\"dbf\"},\n    \"application/vnd.debian.binary-package\"                                     => {\"deb\", \"udeb\"},\n    \"application/vnd.dece.data\"                                                 => {\"uvf\", \"uvvf\", \"uvd\", \"uvvd\"},\n    \"application/vnd.dece.ttml+xml\"                                             => {\"uvt\", \"uvvt\"},\n    \"application/vnd.dece.unspecified\"                                          => {\"uvx\", \"uvvx\"},\n    \"application/vnd.dece.zip\"                                                  => {\"uvz\", \"uvvz\"},\n    \"application/vnd.denovo.fcselayout-link\"                                    => {\"fe_launch\"},\n    \"application/vnd.dna\"                                                       => {\"dna\"},\n    \"application/vnd.dolby.mlp\"                                                 => {\"mlp\"},\n    \"application/vnd.dpgraph\"                                                   => {\"dpg\"},\n    \"application/vnd.dreamfactory\"                                              => {\"dfac\"},\n    \"application/vnd.ds-keypoint\"                                               => {\"kpxx\"},\n    \"application/vnd.dvb.ait\"                                                   => {\"ait\"},\n    \"application/vnd.dvb.service\"                                               => {\"svc\"},\n    \"application/vnd.dynageo\"                                                   => {\"geo\"},\n    \"application/vnd.ecowin.chart\"                                              => {\"mag\"},\n    \"application/vnd.efi.img\"                                                   => {\"raw-disk-image\", \"img\"},\n    \"application/vnd.efi.iso\"                                                   => {\"iso\", \"iso9660\"},\n    \"application/vnd.emusic-emusic_package\"                                     => {\"emp\"},\n    \"application/vnd.enliven\"                                                   => {\"nml\"},\n    \"application/vnd.epson.esf\"                                                 => {\"esf\"},\n    \"application/vnd.epson.msf\"                                                 => {\"msf\"},\n    \"application/vnd.epson.quickanime\"                                          => {\"qam\"},\n    \"application/vnd.epson.salt\"                                                => {\"slt\"},\n    \"application/vnd.epson.ssf\"                                                 => {\"ssf\"},\n    \"application/vnd.eszigno3+xml\"                                              => {\"es3\", \"et3\"},\n    \"application/vnd.etsi.asic-e+zip\"                                           => {\"asice\"},\n    \"application/vnd.ezpix-album\"                                               => {\"ez2\"},\n    \"application/vnd.ezpix-package\"                                             => {\"ez3\"},\n    \"application/vnd.fdf\"                                                       => {\"fdf\"},\n    \"application/vnd.fdsn.mseed\"                                                => {\"mseed\"},\n    \"application/vnd.fdsn.seed\"                                                 => {\"seed\", \"dataless\"},\n    \"application/vnd.flatpak\"                                                   => {\"flatpak\", \"xdgapp\"},\n    \"application/vnd.flatpak.ref\"                                               => {\"flatpakref\"},\n    \"application/vnd.flatpak.repo\"                                              => {\"flatpakrepo\"},\n    \"application/vnd.flographit\"                                                => {\"gph\"},\n    \"application/vnd.fluxtime.clip\"                                             => {\"ftc\"},\n    \"application/vnd.framemaker\"                                                => {\"fm\", \"frame\", \"maker\", \"book\"},\n    \"application/vnd.frogans.fnc\"                                               => {\"fnc\"},\n    \"application/vnd.frogans.ltf\"                                               => {\"ltf\"},\n    \"application/vnd.fsc.weblaunch\"                                             => {\"fsc\"},\n    \"application/vnd.fujitsu.oasys\"                                             => {\"oas\"},\n    \"application/vnd.fujitsu.oasys2\"                                            => {\"oa2\"},\n    \"application/vnd.fujitsu.oasys3\"                                            => {\"oa3\"},\n    \"application/vnd.fujitsu.oasysgp\"                                           => {\"fg5\"},\n    \"application/vnd.fujitsu.oasysprs\"                                          => {\"bh2\"},\n    \"application/vnd.fujixerox.ddd\"                                             => {\"ddd\"},\n    \"application/vnd.fujixerox.docuworks\"                                       => {\"xdw\"},\n    \"application/vnd.fujixerox.docuworks.binder\"                                => {\"xbd\"},\n    \"application/vnd.fuzzysheet\"                                                => {\"fzs\"},\n    \"application/vnd.genomatix.tuxedo\"                                          => {\"txd\"},\n    \"application/vnd.geo+json\"                                                  => {\"geojson\", \"geo.json\"},\n    \"application/vnd.geogebra.file\"                                             => {\"ggb\"},\n    \"application/vnd.geogebra.slides\"                                           => {\"ggs\"},\n    \"application/vnd.geogebra.tool\"                                             => {\"ggt\"},\n    \"application/vnd.geometry-explorer\"                                         => {\"gex\", \"gre\"},\n    \"application/vnd.geonext\"                                                   => {\"gxt\"},\n    \"application/vnd.geoplan\"                                                   => {\"g2w\"},\n    \"application/vnd.geospace\"                                                  => {\"g3w\"},\n    \"application/vnd.gerber\"                                                    => {\"gbr\"},\n    \"application/vnd.gmx\"                                                       => {\"gmx\"},\n    \"application/vnd.google-apps.document\"                                      => {\"gdoc\"},\n    \"application/vnd.google-apps.presentation\"                                  => {\"gslides\"},\n    \"application/vnd.google-apps.spreadsheet\"                                   => {\"gsheet\"},\n    \"application/vnd.google-earth.kml+xml\"                                      => {\"kml\"},\n    \"application/vnd.google-earth.kmz\"                                          => {\"kmz\"},\n    \"application/vnd.gov.sk.xmldatacontainer+xml\"                               => {\"xdcf\"},\n    \"application/vnd.grafeq\"                                                    => {\"gqf\", \"gqs\"},\n    \"application/vnd.groove-account\"                                            => {\"gac\"},\n    \"application/vnd.groove-help\"                                               => {\"ghf\"},\n    \"application/vnd.groove-identity-message\"                                   => {\"gim\"},\n    \"application/vnd.groove-injector\"                                           => {\"grv\"},\n    \"application/vnd.groove-tool-message\"                                       => {\"gtm\"},\n    \"application/vnd.groove-tool-template\"                                      => {\"tpl\"},\n    \"application/vnd.groove-vcard\"                                              => {\"vcg\"},\n    \"application/vnd.haansoft-hwp\"                                              => {\"hwp\"},\n    \"application/vnd.haansoft-hwt\"                                              => {\"hwt\"},\n    \"application/vnd.hal+xml\"                                                   => {\"hal\"},\n    \"application/vnd.handheld-entertainment+xml\"                                => {\"zmm\"},\n    \"application/vnd.hbci\"                                                      => {\"hbci\"},\n    \"application/vnd.hhe.lesson-player\"                                         => {\"les\"},\n    \"application/vnd.hp-hpgl\"                                                   => {\"hpgl\"},\n    \"application/vnd.hp-hpid\"                                                   => {\"hpid\"},\n    \"application/vnd.hp-hps\"                                                    => {\"hps\"},\n    \"application/vnd.hp-jlyt\"                                                   => {\"jlt\"},\n    \"application/vnd.hp-pcl\"                                                    => {\"pcl\"},\n    \"application/vnd.hp-pclxl\"                                                  => {\"pclxl\"},\n    \"application/vnd.hydrostatix.sof-data\"                                      => {\"sfd-hdstx\"},\n    \"application/vnd.ibm.minipay\"                                               => {\"mpy\"},\n    \"application/vnd.ibm.modcap\"                                                => {\"afp\", \"listafp\", \"list3820\"},\n    \"application/vnd.ibm.rights-management\"                                     => {\"irm\"},\n    \"application/vnd.ibm.secure-container\"                                      => {\"sc\"},\n    \"application/vnd.iccprofile\"                                                => {\"icc\", \"icm\"},\n    \"application/vnd.igloader\"                                                  => {\"igl\"},\n    \"application/vnd.immervision-ivp\"                                           => {\"ivp\"},\n    \"application/vnd.immervision-ivu\"                                           => {\"ivu\"},\n    \"application/vnd.insors.igm\"                                                => {\"igm\"},\n    \"application/vnd.intercon.formnet\"                                          => {\"xpw\", \"xpx\"},\n    \"application/vnd.intergeo\"                                                  => {\"i2g\"},\n    \"application/vnd.intu.qbo\"                                                  => {\"qbo\"},\n    \"application/vnd.intu.qfx\"                                                  => {\"qfx\"},\n    \"application/vnd.ipunplugged.rcprofile\"                                     => {\"rcprofile\"},\n    \"application/vnd.irepository.package+xml\"                                   => {\"irp\"},\n    \"application/vnd.is-xpr\"                                                    => {\"xpr\"},\n    \"application/vnd.isac.fcs\"                                                  => {\"fcs\"},\n    \"application/vnd.jam\"                                                       => {\"jam\"},\n    \"application/vnd.jcp.javame.midlet-rms\"                                     => {\"rms\"},\n    \"application/vnd.jisp\"                                                      => {\"jisp\"},\n    \"application/vnd.joost.joda-archive\"                                        => {\"joda\"},\n    \"application/vnd.kahootz\"                                                   => {\"ktz\", \"ktr\"},\n    \"application/vnd.kde.karbon\"                                                => {\"karbon\"},\n    \"application/vnd.kde.kchart\"                                                => {\"chrt\"},\n    \"application/vnd.kde.kformula\"                                              => {\"kfo\"},\n    \"application/vnd.kde.kivio\"                                                 => {\"flw\"},\n    \"application/vnd.kde.kontour\"                                               => {\"kon\"},\n    \"application/vnd.kde.kpresenter\"                                            => {\"kpr\", \"kpt\"},\n    \"application/vnd.kde.kspread\"                                               => {\"ksp\"},\n    \"application/vnd.kde.kword\"                                                 => {\"kwd\", \"kwt\"},\n    \"application/vnd.kenameaapp\"                                                => {\"htke\"},\n    \"application/vnd.kidspiration\"                                              => {\"kia\"},\n    \"application/vnd.kinar\"                                                     => {\"kne\", \"knp\"},\n    \"application/vnd.koan\"                                                      => {\"skp\", \"skd\", \"skt\", \"skm\"},\n    \"application/vnd.kodak-descriptor\"                                          => {\"sse\"},\n    \"application/vnd.las.las+xml\"                                               => {\"lasxml\"},\n    \"application/vnd.llamagraphics.life-balance.desktop\"                        => {\"lbd\"},\n    \"application/vnd.llamagraphics.life-balance.exchange+xml\"                   => {\"lbe\"},\n    \"application/vnd.lotus-1-2-3\"                                               => {\"123\", \"wk1\", \"wk3\", \"wk4\", \"wks\"},\n    \"application/vnd.lotus-approach\"                                            => {\"apr\"},\n    \"application/vnd.lotus-freelance\"                                           => {\"pre\"},\n    \"application/vnd.lotus-notes\"                                               => {\"nsf\"},\n    \"application/vnd.lotus-organizer\"                                           => {\"org\"},\n    \"application/vnd.lotus-screencam\"                                           => {\"scm\"},\n    \"application/vnd.lotus-wordpro\"                                             => {\"lwp\"},\n    \"application/vnd.macports.portpkg\"                                          => {\"portpkg\"},\n    \"application/vnd.mapbox-vector-tile\"                                        => {\"mvt\"},\n    \"application/vnd.mcd\"                                                       => {\"mcd\"},\n    \"application/vnd.medcalcdata\"                                               => {\"mc1\"},\n    \"application/vnd.mediastation.cdkey\"                                        => {\"cdkey\"},\n    \"application/vnd.mfer\"                                                      => {\"mwf\"},\n    \"application/vnd.mfmp\"                                                      => {\"mfm\"},\n    \"application/vnd.micrografx.flo\"                                            => {\"flo\"},\n    \"application/vnd.micrografx.igx\"                                            => {\"igx\"},\n    \"application/vnd.microsoft.portable-executable\"                             => {\"exe\", \"dll\", \"cpl\", \"drv\", \"scr\", \"efi\", \"ocx\", \"sys\", \"lib\"},\n    \"application/vnd.mif\"                                                       => {\"mif\"},\n    \"application/vnd.mobius.daf\"                                                => {\"daf\"},\n    \"application/vnd.mobius.dis\"                                                => {\"dis\"},\n    \"application/vnd.mobius.mbk\"                                                => {\"mbk\"},\n    \"application/vnd.mobius.mqy\"                                                => {\"mqy\"},\n    \"application/vnd.mobius.msl\"                                                => {\"msl\"},\n    \"application/vnd.mobius.plc\"                                                => {\"plc\"},\n    \"application/vnd.mobius.txf\"                                                => {\"txf\"},\n    \"application/vnd.mophun.application\"                                        => {\"mpn\"},\n    \"application/vnd.mophun.certificate\"                                        => {\"mpc\"},\n    \"application/vnd.mozilla.xul+xml\"                                           => {\"xul\"},\n    \"application/vnd.ms-3mfdocument\"                                            => {\"3mf\"},\n    \"application/vnd.ms-access\"                                                 => {\"mdb\"},\n    \"application/vnd.ms-artgalry\"                                               => {\"cil\"},\n    \"application/vnd.ms-asf\"                                                    => {\"asf\"},\n    \"application/vnd.ms-cab-compressed\"                                         => {\"cab\"},\n    \"application/vnd.ms-excel\"                                                  => {\"xls\", \"xlm\", \"xla\", \"xlc\", \"xlt\", \"xlw\", \"xll\", \"xld\"},\n    \"application/vnd.ms-excel.addin.macroenabled.12\"                            => {\"xlam\"},\n    \"application/vnd.ms-excel.sheet.binary.macroenabled.12\"                     => {\"xlsb\"},\n    \"application/vnd.ms-excel.sheet.macroenabled.12\"                            => {\"xlsm\"},\n    \"application/vnd.ms-excel.template.macroenabled.12\"                         => {\"xltm\"},\n    \"application/vnd.ms-fontobject\"                                             => {\"eot\"},\n    \"application/vnd.ms-htmlhelp\"                                               => {\"chm\"},\n    \"application/vnd.ms-ims\"                                                    => {\"ims\"},\n    \"application/vnd.ms-lrm\"                                                    => {\"lrm\"},\n    \"application/vnd.ms-officetheme\"                                            => {\"thmx\"},\n    \"application/vnd.ms-outlook\"                                                => {\"msg\"},\n    \"application/vnd.ms-pki.seccat\"                                             => {\"cat\"},\n    \"application/vnd.ms-pki.stl\"                                                => {\"stl\"},\n    \"application/vnd.ms-powerpoint\"                                             => {\"ppt\", \"pps\", \"pot\", \"ppz\"},\n    \"application/vnd.ms-powerpoint.addin.macroenabled.12\"                       => {\"ppam\"},\n    \"application/vnd.ms-powerpoint.presentation.macroenabled.12\"                => {\"pptm\"},\n    \"application/vnd.ms-powerpoint.slide.macroenabled.12\"                       => {\"sldm\"},\n    \"application/vnd.ms-powerpoint.slideshow.macroenabled.12\"                   => {\"ppsm\"},\n    \"application/vnd.ms-powerpoint.template.macroenabled.12\"                    => {\"potm\"},\n    \"application/vnd.ms-project\"                                                => {\"mpp\", \"mpt\"},\n    \"application/vnd.ms-publisher\"                                              => {\"pub\"},\n    \"application/vnd.ms-tnef\"                                                   => {\"tnef\", \"tnf\"},\n    \"application/vnd.ms-visio.drawing.macroenabled.main+xml\"                    => {\"vsdm\"},\n    \"application/vnd.ms-visio.drawing.main+xml\"                                 => {\"vsdx\"},\n    \"application/vnd.ms-visio.stencil.macroenabled.main+xml\"                    => {\"vssm\"},\n    \"application/vnd.ms-visio.stencil.main+xml\"                                 => {\"vssx\"},\n    \"application/vnd.ms-visio.template.macroenabled.main+xml\"                   => {\"vstm\"},\n    \"application/vnd.ms-visio.template.main+xml\"                                => {\"vstx\"},\n    \"application/vnd.ms-word\"                                                   => {\"doc\"},\n    \"application/vnd.ms-word.document.macroenabled.12\"                          => {\"docm\"},\n    \"application/vnd.ms-word.template.macroenabled.12\"                          => {\"dotm\"},\n    \"application/vnd.ms-works\"                                                  => {\"wps\", \"wks\", \"wcm\", \"wdb\", \"xlr\"},\n    \"application/vnd.ms-wpl\"                                                    => {\"wpl\"},\n    \"application/vnd.ms-xpsdocument\"                                            => {\"xps\"},\n    \"application/vnd.msaccess\"                                                  => {\"mdb\"},\n    \"application/vnd.mseq\"                                                      => {\"mseq\"},\n    \"application/vnd.musician\"                                                  => {\"mus\"},\n    \"application/vnd.muvee.style\"                                               => {\"msty\"},\n    \"application/vnd.mynfc\"                                                     => {\"taglet\"},\n    \"application/vnd.nato.bindingdataobject+xml\"                                => {\"bdo\"},\n    \"application/vnd.neurolanguage.nlu\"                                         => {\"nlu\"},\n    \"application/vnd.nintendo.snes.rom\"                                         => {\"sfc\", \"smc\"},\n    \"application/vnd.nitf\"                                                      => {\"ntf\", \"nitf\"},\n    \"application/vnd.noblenet-directory\"                                        => {\"nnd\"},\n    \"application/vnd.noblenet-sealer\"                                           => {\"nns\"},\n    \"application/vnd.noblenet-web\"                                              => {\"nnw\"},\n    \"application/vnd.nokia.n-gage.ac+xml\"                                       => {\"ac\"},\n    \"application/vnd.nokia.n-gage.data\"                                         => {\"ngdat\"},\n    \"application/vnd.nokia.n-gage.symbian.install\"                              => {\"n-gage\"},\n    \"application/vnd.nokia.radio-preset\"                                        => {\"rpst\"},\n    \"application/vnd.nokia.radio-presets\"                                       => {\"rpss\"},\n    \"application/vnd.novadigm.edm\"                                              => {\"edm\"},\n    \"application/vnd.novadigm.edx\"                                              => {\"edx\"},\n    \"application/vnd.novadigm.ext\"                                              => {\"ext\"},\n    \"application/vnd.oasis.docbook+xml\"                                         => {\"dbk\", \"docbook\"},\n    \"application/vnd.oasis.opendocument.base\"                                   => {\"odb\"},\n    \"application/vnd.oasis.opendocument.chart\"                                  => {\"odc\"},\n    \"application/vnd.oasis.opendocument.chart-template\"                         => {\"otc\"},\n    \"application/vnd.oasis.opendocument.database\"                               => {\"odb\"},\n    \"application/vnd.oasis.opendocument.formula\"                                => {\"odf\"},\n    \"application/vnd.oasis.opendocument.formula-template\"                       => {\"odft\", \"otf\"},\n    \"application/vnd.oasis.opendocument.graphics\"                               => {\"odg\"},\n    \"application/vnd.oasis.opendocument.graphics-flat-xml\"                      => {\"fodg\"},\n    \"application/vnd.oasis.opendocument.graphics-template\"                      => {\"otg\"},\n    \"application/vnd.oasis.opendocument.image\"                                  => {\"odi\"},\n    \"application/vnd.oasis.opendocument.image-template\"                         => {\"oti\"},\n    \"application/vnd.oasis.opendocument.presentation\"                           => {\"odp\"},\n    \"application/vnd.oasis.opendocument.presentation-flat-xml\"                  => {\"fodp\"},\n    \"application/vnd.oasis.opendocument.presentation-template\"                  => {\"otp\"},\n    \"application/vnd.oasis.opendocument.spreadsheet\"                            => {\"ods\"},\n    \"application/vnd.oasis.opendocument.spreadsheet-flat-xml\"                   => {\"fods\"},\n    \"application/vnd.oasis.opendocument.spreadsheet-template\"                   => {\"ots\"},\n    \"application/vnd.oasis.opendocument.text\"                                   => {\"odt\"},\n    \"application/vnd.oasis.opendocument.text-flat-xml\"                          => {\"fodt\"},\n    \"application/vnd.oasis.opendocument.text-master\"                            => {\"odm\"},\n    \"application/vnd.oasis.opendocument.text-master-template\"                   => {\"otm\"},\n    \"application/vnd.oasis.opendocument.text-template\"                          => {\"ott\"},\n    \"application/vnd.oasis.opendocument.text-web\"                               => {\"oth\"},\n    \"application/vnd.olpc-sugar\"                                                => {\"xo\"},\n    \"application/vnd.oma.dd2+xml\"                                               => {\"dd2\"},\n    \"application/vnd.openblox.game+xml\"                                         => {\"obgx\"},\n    \"application/vnd.openofficeorg.extension\"                                   => {\"oxt\"},\n    \"application/vnd.openstreetmap.data+xml\"                                    => {\"osm\"},\n    \"application/vnd.openxmlformats-officedocument.presentationml.presentation\" => {\"pptx\"},\n    \"application/vnd.openxmlformats-officedocument.presentationml.slide\"        => {\"sldx\"},\n    \"application/vnd.openxmlformats-officedocument.presentationml.slideshow\"    => {\"ppsx\"},\n    \"application/vnd.openxmlformats-officedocument.presentationml.template\"     => {\"potx\"},\n    \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"         => {\"xlsx\"},\n    \"application/vnd.openxmlformats-officedocument.spreadsheetml.template\"      => {\"xltx\"},\n    \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"   => {\"docx\"},\n    \"application/vnd.openxmlformats-officedocument.wordprocessingml.template\"   => {\"dotx\"},\n    \"application/vnd.osgeo.mapguide.package\"                                    => {\"mgp\"},\n    \"application/vnd.osgi.dp\"                                                   => {\"dp\"},\n    \"application/vnd.osgi.subsystem\"                                            => {\"esa\"},\n    \"application/vnd.palm\"                                                      => {\"pdb\", \"pqa\", \"oprc\", \"prc\"},\n    \"application/vnd.pawaafile\"                                                 => {\"paw\"},\n    \"application/vnd.pg.format\"                                                 => {\"str\"},\n    \"application/vnd.pg.osasli\"                                                 => {\"ei6\"},\n    \"application/vnd.picsel\"                                                    => {\"efif\"},\n    \"application/vnd.pmi.widget\"                                                => {\"wg\"},\n    \"application/vnd.pocketlearn\"                                               => {\"plf\"},\n    \"application/vnd.powerbuilder6\"                                             => {\"pbd\"},\n    \"application/vnd.previewsystems.box\"                                        => {\"box\"},\n    \"application/vnd.proteus.magazine\"                                          => {\"mgz\"},\n    \"application/vnd.publishare-delta-tree\"                                     => {\"qps\"},\n    \"application/vnd.pvi.ptid1\"                                                 => {\"ptid\"},\n    \"application/vnd.pwg-xhtml-print+xml\"                                       => {\"xhtm\"},\n    \"application/vnd.quark.quarkxpress\"                                         => {\"qxd\", \"qxt\", \"qwd\", \"qwt\", \"qxl\", \"qxb\", \"qxp\"},\n    \"application/vnd.rar\"                                                       => {\"rar\"},\n    \"application/vnd.realvnc.bed\"                                               => {\"bed\"},\n    \"application/vnd.recordare.musicxml\"                                        => {\"mxl\"},\n    \"application/vnd.recordare.musicxml+xml\"                                    => {\"musicxml\"},\n    \"application/vnd.rig.cryptonote\"                                            => {\"cryptonote\"},\n    \"application/vnd.rim.cod\"                                                   => {\"cod\"},\n    \"application/vnd.rn-realmedia\"                                              => {\"rm\", \"rmj\", \"rmm\", \"rms\", \"rmx\", \"rmvb\"},\n    \"application/vnd.rn-realmedia-vbr\"                                          => {\"rmvb\", \"rm\", \"rmj\", \"rmm\", \"rms\", \"rmx\"},\n    \"application/vnd.route66.link66+xml\"                                        => {\"link66\"},\n    \"application/vnd.sailingtracker.track\"                                      => {\"st\"},\n    \"application/vnd.sdp\"                                                       => {\"sdp\"},\n    \"application/vnd.seemail\"                                                   => {\"see\"},\n    \"application/vnd.sema\"                                                      => {\"sema\"},\n    \"application/vnd.semd\"                                                      => {\"semd\"},\n    \"application/vnd.semf\"                                                      => {\"semf\"},\n    \"application/vnd.shana.informed.formdata\"                                   => {\"ifm\"},\n    \"application/vnd.shana.informed.formtemplate\"                               => {\"itp\"},\n    \"application/vnd.shana.informed.interchange\"                                => {\"iif\"},\n    \"application/vnd.shana.informed.package\"                                    => {\"ipk\"},\n    \"application/vnd.simtech-mindmapper\"                                        => {\"twd\", \"twds\"},\n    \"application/vnd.smaf\"                                                      => {\"mmf\", \"smaf\"},\n    \"application/vnd.smart.teacher\"                                             => {\"teacher\"},\n    \"application/vnd.snap\"                                                      => {\"snap\"},\n    \"application/vnd.software602.filler.form+xml\"                               => {\"fo\"},\n    \"application/vnd.solent.sdkm+xml\"                                           => {\"sdkm\", \"sdkd\"},\n    \"application/vnd.spotfire.dxp\"                                              => {\"dxp\"},\n    \"application/vnd.spotfire.sfs\"                                              => {\"sfs\"},\n    \"application/vnd.sqlite3\"                                                   => {\"sqlite3\"},\n    \"application/vnd.squashfs\"                                                  => {\"sfs\", \"sqfs\", \"sqsh\", \"squashfs\"},\n    \"application/vnd.stardivision.calc\"                                         => {\"sdc\"},\n    \"application/vnd.stardivision.chart\"                                        => {\"sds\"},\n    \"application/vnd.stardivision.draw\"                                         => {\"sda\"},\n    \"application/vnd.stardivision.impress\"                                      => {\"sdd\"},\n    \"application/vnd.stardivision.impress-packed\"                               => {\"sdp\"},\n    \"application/vnd.stardivision.mail\"                                         => {\"sdm\"},\n    \"application/vnd.stardivision.math\"                                         => {\"smf\"},\n    \"application/vnd.stardivision.writer\"                                       => {\"sdw\", \"vor\"},\n    \"application/vnd.stardivision.writer-global\"                                => {\"sgl\"},\n    \"application/vnd.stepmania.package\"                                         => {\"smzip\"},\n    \"application/vnd.stepmania.stepchart\"                                       => {\"sm\"},\n    \"application/vnd.sun.wadl+xml\"                                              => {\"wadl\"},\n    \"application/vnd.sun.xml.base\"                                              => {\"odb\"},\n    \"application/vnd.sun.xml.calc\"                                              => {\"sxc\"},\n    \"application/vnd.sun.xml.calc.template\"                                     => {\"stc\"},\n    \"application/vnd.sun.xml.draw\"                                              => {\"sxd\"},\n    \"application/vnd.sun.xml.draw.template\"                                     => {\"std\"},\n    \"application/vnd.sun.xml.impress\"                                           => {\"sxi\"},\n    \"application/vnd.sun.xml.impress.template\"                                  => {\"sti\"},\n    \"application/vnd.sun.xml.math\"                                              => {\"sxm\"},\n    \"application/vnd.sun.xml.writer\"                                            => {\"sxw\"},\n    \"application/vnd.sun.xml.writer.global\"                                     => {\"sxg\"},\n    \"application/vnd.sun.xml.writer.template\"                                   => {\"stw\"},\n    \"application/vnd.sus-calendar\"                                              => {\"sus\", \"susp\"},\n    \"application/vnd.svd\"                                                       => {\"svd\"},\n    \"application/vnd.symbian.install\"                                           => {\"sis\", \"sisx\"},\n    \"application/vnd.syncml+xml\"                                                => {\"xsm\"},\n    \"application/vnd.syncml.dm+wbxml\"                                           => {\"bdm\"},\n    \"application/vnd.syncml.dm+xml\"                                             => {\"xdm\"},\n    \"application/vnd.syncml.dmddf+xml\"                                          => {\"ddf\"},\n    \"application/vnd.tao.intent-module-archive\"                                 => {\"tao\"},\n    \"application/vnd.tcpdump.pcap\"                                              => {\"pcap\", \"cap\", \"dmp\"},\n    \"application/vnd.tmobile-livetv\"                                            => {\"tmo\"},\n    \"application/vnd.trid.tpt\"                                                  => {\"tpt\"},\n    \"application/vnd.triscape.mxs\"                                              => {\"mxs\"},\n    \"application/vnd.trueapp\"                                                   => {\"tra\"},\n    \"application/vnd.truedoc\"                                                   => {\"pfr\"},\n    \"application/vnd.ufdl\"                                                      => {\"ufd\", \"ufdl\"},\n    \"application/vnd.uiq.theme\"                                                 => {\"utz\"},\n    \"application/vnd.umajin\"                                                    => {\"umj\"},\n    \"application/vnd.unity\"                                                     => {\"unityweb\"},\n    \"application/vnd.uoml+xml\"                                                  => {\"uoml\", \"uo\"},\n    \"application/vnd.vcx\"                                                       => {\"vcx\"},\n    \"application/vnd.visio\"                                                     => {\"vsd\", \"vst\", \"vss\", \"vsw\"},\n    \"application/vnd.visionary\"                                                 => {\"vis\"},\n    \"application/vnd.vsf\"                                                       => {\"vsf\"},\n    \"application/vnd.wap.wbxml\"                                                 => {\"wbxml\"},\n    \"application/vnd.wap.wmlc\"                                                  => {\"wmlc\"},\n    \"application/vnd.wap.wmlscriptc\"                                            => {\"wmlsc\"},\n    \"application/vnd.webturbo\"                                                  => {\"wtb\"},\n    \"application/vnd.wolfram.player\"                                            => {\"nbp\"},\n    \"application/vnd.wordperfect\"                                               => {\"wpd\", \"wp\", \"wp4\", \"wp5\", \"wp6\", \"wpp\"},\n    \"application/vnd.wqd\"                                                       => {\"wqd\"},\n    \"application/vnd.wt.stf\"                                                    => {\"stf\"},\n    \"application/vnd.xara\"                                                      => {\"xar\"},\n    \"application/vnd.xdgapp\"                                                    => {\"flatpak\", \"xdgapp\"},\n    \"application/vnd.xfdl\"                                                      => {\"xfdl\"},\n    \"application/vnd.yamaha.hv-dic\"                                             => {\"hvd\"},\n    \"application/vnd.yamaha.hv-script\"                                          => {\"hvs\"},\n    \"application/vnd.yamaha.hv-voice\"                                           => {\"hvp\"},\n    \"application/vnd.yamaha.openscoreformat\"                                    => {\"osf\"},\n    \"application/vnd.yamaha.openscoreformat.osfpvg+xml\"                         => {\"osfpvg\"},\n    \"application/vnd.yamaha.smaf-audio\"                                         => {\"saf\"},\n    \"application/vnd.yamaha.smaf-phrase\"                                        => {\"spf\"},\n    \"application/vnd.yellowriver-custom-menu\"                                   => {\"cmp\"},\n    \"application/vnd.youtube.yt\"                                                => {\"yt\"},\n    \"application/vnd.zul\"                                                       => {\"zir\", \"zirz\"},\n    \"application/vnd.zzazz.deck+xml\"                                            => {\"zaz\"},\n    \"application/voicexml+xml\"                                                  => {\"vxml\"},\n    \"application/wasm\"                                                          => {\"wasm\"},\n    \"application/watcherinfo+xml\"                                               => {\"wif\"},\n    \"application/widget\"                                                        => {\"wgt\"},\n    \"application/winhlp\"                                                        => {\"hlp\"},\n    \"application/wk1\"                                                           => {\"123\", \"wk1\", \"wk3\", \"wk4\", \"wks\"},\n    \"application/wmf\"                                                           => {\"wmf\"},\n    \"application/wordperfect\"                                                   => {\"wp\", \"wp4\", \"wp5\", \"wp6\", \"wpd\", \"wpp\"},\n    \"application/wsdl+xml\"                                                      => {\"wsdl\"},\n    \"application/wspolicy+xml\"                                                  => {\"wspolicy\"},\n    \"application/wwf\"                                                           => {\"wwf\"},\n    \"application/x-123\"                                                         => {\"123\", \"wk1\", \"wk3\", \"wk4\", \"wks\"},\n    \"application/x-7z-compressed\"                                               => {\"7z\", \"7z.001\"},\n    \"application/x-abiword\"                                                     => {\"abw\", \"abw.CRASHED\", \"abw.gz\", \"zabw\"},\n    \"application/x-ace\"                                                         => {\"ace\"},\n    \"application/x-ace-compressed\"                                              => {\"ace\"},\n    \"application/x-alz\"                                                         => {\"alz\"},\n    \"application/x-amiga-disk-format\"                                           => {\"adf\"},\n    \"application/x-amipro\"                                                      => {\"sam\"},\n    \"application/x-annodex\"                                                     => {\"anx\"},\n    \"application/x-aportisdoc\"                                                  => {\"pdb\", \"pdc\"},\n    \"application/x-apple-diskimage\"                                             => {\"dmg\"},\n    \"application/x-apple-systemprofiler+xml\"                                    => {\"spx\"},\n    \"application/x-appleworks-document\"                                         => {\"cwk\"},\n    \"application/x-applix-spreadsheet\"                                          => {\"as\"},\n    \"application/x-applix-word\"                                                 => {\"aw\"},\n    \"application/x-archive\"                                                     => {\"a\", \"ar\", \"lib\"},\n    \"application/x-arj\"                                                         => {\"arj\"},\n    \"application/x-asar\"                                                        => {\"asar\"},\n    \"application/x-asp\"                                                         => {\"asp\"},\n    \"application/x-atari-2600-rom\"                                              => {\"a26\"},\n    \"application/x-atari-7800-rom\"                                              => {\"a78\"},\n    \"application/x-atari-lynx-rom\"                                              => {\"lnx\"},\n    \"application/x-authorware-bin\"                                              => {\"aab\", \"x32\", \"u32\", \"vox\"},\n    \"application/x-authorware-map\"                                              => {\"aam\"},\n    \"application/x-authorware-seg\"                                              => {\"aas\"},\n    \"application/x-awk\"                                                         => {\"awk\"},\n    \"application/x-bat\"                                                         => {\"bat\"},\n    \"application/x-bcpio\"                                                       => {\"bcpio\"},\n    \"application/x-bdoc\"                                                        => {\"bdoc\"},\n    \"application/x-bittorrent\"                                                  => {\"torrent\"},\n    \"application/x-blender\"                                                     => {\"blend\", \"blender\"},\n    \"application/x-blorb\"                                                       => {\"blb\", \"blorb\"},\n    \"application/x-bps-patch\"                                                   => {\"bps\"},\n    \"application/x-bsdiff\"                                                      => {\"bsdiff\"},\n    \"application/x-bz2\"                                                         => {\"bz2\"},\n    \"application/x-bzdvi\"                                                       => {\"dvi.bz2\"},\n    \"application/x-bzip\"                                                        => {\"bz\", \"bz2\"},\n    \"application/x-bzip-compressed-tar\"                                         => {\"tar.bz2\", \"tbz2\", \"tb2\"},\n    \"application/x-bzip1\"                                                       => {\"bz\"},\n    \"application/x-bzip1-compressed-tar\"                                        => {\"tar.bz\", \"tbz\"},\n    \"application/x-bzip2\"                                                       => {\"bz2\", \"boz\"},\n    \"application/x-bzip2-compressed-tar\"                                        => {\"tar.bz2\", \"tbz2\", \"tb2\"},\n    \"application/x-bzip3\"                                                       => {\"bz3\"},\n    \"application/x-bzip3-compressed-tar\"                                        => {\"tar.bz3\", \"tbz3\"},\n    \"application/x-bzpdf\"                                                       => {\"pdf.bz2\"},\n    \"application/x-bzpostscript\"                                                => {\"ps.bz2\"},\n    \"application/x-cb7\"                                                         => {\"cb7\"},\n    \"application/x-cbr\"                                                         => {\"cbr\", \"cba\", \"cbt\", \"cbz\", \"cb7\"},\n    \"application/x-cbt\"                                                         => {\"cbt\"},\n    \"application/x-cbz\"                                                         => {\"cbz\"},\n    \"application/x-ccmx\"                                                        => {\"ccmx\"},\n    \"application/x-cd-image\"                                                    => {\"iso\", \"iso9660\"},\n    \"application/x-cdlink\"                                                      => {\"vcd\"},\n    \"application/x-cdr\"                                                         => {\"cdr\"},\n    \"application/x-cdrdao-toc\"                                                  => {\"toc\"},\n    \"application/x-cfs-compressed\"                                              => {\"cfs\"},\n    \"application/x-chat\"                                                        => {\"chat\"},\n    \"application/x-chess-pgn\"                                                   => {\"pgn\"},\n    \"application/x-chm\"                                                         => {\"chm\"},\n    \"application/x-chrome-extension\"                                            => {\"crx\"},\n    \"application/x-cisco-vpn-settings\"                                          => {\"pcf\"},\n    \"application/x-cocoa\"                                                       => {\"cco\"},\n    \"application/x-compress\"                                                    => {\"Z\"},\n    \"application/x-compressed-iso\"                                              => {\"cso\"},\n    \"application/x-compressed-tar\"                                              => {\"tar.gz\", \"tgz\"},\n    \"application/x-conference\"                                                  => {\"nsc\"},\n    \"application/x-coreldraw\"                                                   => {\"cdr\"},\n    \"application/x-cpio\"                                                        => {\"cpio\"},\n    \"application/x-cpio-compressed\"                                             => {\"cpio.gz\"},\n    \"application/x-csh\"                                                         => {\"csh\"},\n    \"application/x-cue\"                                                         => {\"cue\"},\n    \"application/x-dar\"                                                         => {\"dar\"},\n    \"application/x-dbase\"                                                       => {\"dbf\"},\n    \"application/x-dbf\"                                                         => {\"dbf\"},\n    \"application/x-dc-rom\"                                                      => {\"dc\"},\n    \"application/x-deb\"                                                         => {\"deb\", \"udeb\"},\n    \"application/x-debian-package\"                                              => {\"deb\", \"udeb\"},\n    \"application/x-designer\"                                                    => {\"ui\"},\n    \"application/x-desktop\"                                                     => {\"desktop\", \"kdelnk\"},\n    \"application/x-dgc-compressed\"                                              => {\"dgc\"},\n    \"application/x-dia-diagram\"                                                 => {\"dia\"},\n    \"application/x-dia-shape\"                                                   => {\"shape\"},\n    \"application/x-director\"                                                    => {\"dir\", \"dcr\", \"dxr\", \"cst\", \"cct\", \"cxt\", \"w3d\", \"fgd\", \"swa\"},\n    \"application/x-discjuggler-cd-image\"                                        => {\"cdi\"},\n    \"application/x-docbook+xml\"                                                 => {\"dbk\", \"docbook\"},\n    \"application/x-doom\"                                                        => {\"wad\"},\n    \"application/x-doom-wad\"                                                    => {\"wad\"},\n    \"application/x-dosexec\"                                                     => {\"exe\"},\n    \"application/x-dreamcast-rom\"                                               => {\"iso\"},\n    \"application/x-dtbncx+xml\"                                                  => {\"ncx\"},\n    \"application/x-dtbook+xml\"                                                  => {\"dtb\"},\n    \"application/x-dtbresource+xml\"                                             => {\"res\"},\n    \"application/x-dvi\"                                                         => {\"dvi\"},\n    \"application/x-e-theme\"                                                     => {\"etheme\"},\n    \"application/x-egon\"                                                        => {\"egon\"},\n    \"application/x-emf\"                                                         => {\"emf\"},\n    \"application/x-envoy\"                                                       => {\"evy\"},\n    \"application/x-eris-link+cbor\"                                              => {\"eris\"},\n    \"application/x-eva\"                                                         => {\"eva\"},\n    \"application/x-excellon\"                                                    => {\"drl\"},\n    \"application/x-fd-file\"                                                     => {\"fd\", \"qd\"},\n    \"application/x-fds-disk\"                                                    => {\"fds\"},\n    \"application/x-fictionbook\"                                                 => {\"fb2\"},\n    \"application/x-fictionbook+xml\"                                             => {\"fb2\"},\n    \"application/x-fishscript\"                                                  => {\"fish\"},\n    \"application/x-flash-video\"                                                 => {\"flv\"},\n    \"application/x-fluid\"                                                       => {\"fl\"},\n    \"application/x-font-afm\"                                                    => {\"afm\"},\n    \"application/x-font-bdf\"                                                    => {\"bdf\"},\n    \"application/x-font-ghostscript\"                                            => {\"gsf\"},\n    \"application/x-font-linux-psf\"                                              => {\"psf\"},\n    \"application/x-font-otf\"                                                    => {\"otf\"},\n    \"application/x-font-pcf\"                                                    => {\"pcf\", \"pcf.Z\", \"pcf.gz\"},\n    \"application/x-font-snf\"                                                    => {\"snf\"},\n    \"application/x-font-speedo\"                                                 => {\"spd\"},\n    \"application/x-font-truetype\"                                               => {\"ttf\"},\n    \"application/x-font-ttf\"                                                    => {\"ttf\"},\n    \"application/x-font-ttx\"                                                    => {\"ttx\"},\n    \"application/x-font-type1\"                                                  => {\"pfa\", \"pfb\", \"pfm\", \"afm\", \"gsf\"},\n    \"application/x-font-woff\"                                                   => {\"woff\"},\n    \"application/x-frame\"                                                       => {\"fm\"},\n    \"application/x-freearc\"                                                     => {\"arc\"},\n    \"application/x-futuresplash\"                                                => {\"spl\"},\n    \"application/x-gameboy-color-rom\"                                           => {\"gbc\", \"cgb\"},\n    \"application/x-gameboy-rom\"                                                 => {\"gb\", \"sgb\"},\n    \"application/x-gamecube-iso-image\"                                          => {\"iso\"},\n    \"application/x-gamecube-rom\"                                                => {\"iso\"},\n    \"application/x-gamegear-rom\"                                                => {\"gg\"},\n    \"application/x-gba-rom\"                                                     => {\"gba\", \"agb\"},\n    \"application/x-gca-compressed\"                                              => {\"gca\"},\n    \"application/x-gd-rom-cue\"                                                  => {\"gdi\"},\n    \"application/x-gdscript\"                                                    => {\"gd\"},\n    \"application/x-gedcom\"                                                      => {\"ged\", \"gedcom\"},\n    \"application/x-genesis-32x-rom\"                                             => {\"32x\", \"mdx\"},\n    \"application/x-genesis-rom\"                                                 => {\"gen\", \"smd\", \"md\", \"sgd\"},\n    \"application/x-gerber\"                                                      => {\"gbr\"},\n    \"application/x-gerber-job\"                                                  => {\"gbrjob\"},\n    \"application/x-gettext\"                                                     => {\"po\"},\n    \"application/x-gettext-translation\"                                         => {\"gmo\", \"mo\"},\n    \"application/x-glade\"                                                       => {\"glade\"},\n    \"application/x-glulx\"                                                       => {\"ulx\"},\n    \"application/x-gnome-app-info\"                                              => {\"desktop\", \"kdelnk\"},\n    \"application/x-gnucash\"                                                     => {\"gnucash\", \"gnc\", \"xac\"},\n    \"application/x-gnumeric\"                                                    => {\"gnumeric\"},\n    \"application/x-gnuplot\"                                                     => {\"gp\", \"gplt\", \"gnuplot\"},\n    \"application/x-go-sgf\"                                                      => {\"sgf\"},\n    \"application/x-godot-resource\"                                              => {\"res\", \"tres\"},\n    \"application/x-godot-scene\"                                                 => {\"scn\", \"tscn\", \"escn\"},\n    \"application/x-godot-shader\"                                                => {\"gdshader\"},\n    \"application/x-gpx\"                                                         => {\"gpx\"},\n    \"application/x-gpx+xml\"                                                     => {\"gpx\"},\n    \"application/x-gramps-xml\"                                                  => {\"gramps\"},\n    \"application/x-graphite\"                                                    => {\"gra\"},\n    \"application/x-gtar\"                                                        => {\"gtar\", \"tar\", \"gem\"},\n    \"application/x-gtk-builder\"                                                 => {\"ui\"},\n    \"application/x-gz-font-linux-psf\"                                           => {\"psf.gz\"},\n    \"application/x-gzdvi\"                                                       => {\"dvi.gz\"},\n    \"application/x-gzip\"                                                        => {\"gz\"},\n    \"application/x-gzpdf\"                                                       => {\"pdf.gz\"},\n    \"application/x-gzpostscript\"                                                => {\"ps.gz\"},\n    \"application/x-hdf\"                                                         => {\"hdf\", \"hdf4\", \"h4\", \"hdf5\", \"h5\"},\n    \"application/x-hfe-file\"                                                    => {\"hfe\"},\n    \"application/x-hfe-floppy-image\"                                            => {\"hfe\"},\n    \"application/x-httpd-php\"                                                   => {\"php\"},\n    \"application/x-hwp\"                                                         => {\"hwp\"},\n    \"application/x-hwt\"                                                         => {\"hwt\"},\n    \"application/x-ica\"                                                         => {\"ica\"},\n    \"application/x-install-instructions\"                                        => {\"install\"},\n    \"application/x-ips-patch\"                                                   => {\"ips\"},\n    \"application/x-ipynb+json\"                                                  => {\"ipynb\"},\n    \"application/x-iso9660-appimage\"                                            => {\"appimage\"},\n    \"application/x-iso9660-image\"                                               => {\"iso\", \"iso9660\"},\n    \"application/x-it87\"                                                        => {\"it87\"},\n    \"application/x-iwork-keynote-sffkey\"                                        => {\"key\"},\n    \"application/x-iwork-numbers-sffnumbers\"                                    => {\"numbers\"},\n    \"application/x-iwork-pages-sffpages\"                                        => {\"pages\"},\n    \"application/x-jar\"                                                         => {\"jar\"},\n    \"application/x-java\"                                                        => {\"class\"},\n    \"application/x-java-archive\"                                                => {\"jar\"},\n    \"application/x-java-archive-diff\"                                           => {\"jardiff\"},\n    \"application/x-java-class\"                                                  => {\"class\"},\n    \"application/x-java-jce-keystore\"                                           => {\"jceks\"},\n    \"application/x-java-jnlp-file\"                                              => {\"jnlp\"},\n    \"application/x-java-keystore\"                                               => {\"jks\", \"ks\"},\n    \"application/x-java-pack200\"                                                => {\"pack\"},\n    \"application/x-java-vm\"                                                     => {\"class\"},\n    \"application/x-javascript\"                                                  => {\"cjs\", \"js\", \"jsm\", \"mjs\"},\n    \"application/x-jbuilder-project\"                                            => {\"jpr\", \"jpx\"},\n    \"application/x-karbon\"                                                      => {\"karbon\"},\n    \"application/x-kchart\"                                                      => {\"chrt\"},\n    \"application/x-keepass2\"                                                    => {\"kdbx\"},\n    \"application/x-kexi-connectiondata\"                                         => {\"kexic\"},\n    \"application/x-kexiproject-shortcut\"                                        => {\"kexis\"},\n    \"application/x-kexiproject-sqlite\"                                          => {\"kexi\"},\n    \"application/x-kexiproject-sqlite2\"                                         => {\"kexi\"},\n    \"application/x-kexiproject-sqlite3\"                                         => {\"kexi\"},\n    \"application/x-kformula\"                                                    => {\"kfo\"},\n    \"application/x-killustrator\"                                                => {\"kil\"},\n    \"application/x-kivio\"                                                       => {\"flw\"},\n    \"application/x-kontour\"                                                     => {\"kon\"},\n    \"application/x-kpovmodeler\"                                                 => {\"kpm\"},\n    \"application/x-kpresenter\"                                                  => {\"kpr\", \"kpt\"},\n    \"application/x-krita\"                                                       => {\"kra\", \"krz\"},\n    \"application/x-kspread\"                                                     => {\"ksp\"},\n    \"application/x-kugar\"                                                       => {\"kud\"},\n    \"application/x-kword\"                                                       => {\"kwd\", \"kwt\"},\n    \"application/x-latex\"                                                       => {\"latex\"},\n    \"application/x-lha\"                                                         => {\"lha\", \"lzh\"},\n    \"application/x-lhz\"                                                         => {\"lhz\"},\n    \"application/x-linguist\"                                                    => {\"ts\"},\n    \"application/x-lmdb\"                                                        => {\"mdb\", \"lmdb\"},\n    \"application/x-lotus123\"                                                    => {\"123\", \"wk1\", \"wk3\", \"wk4\", \"wks\"},\n    \"application/x-lrzip\"                                                       => {\"lrz\"},\n    \"application/x-lrzip-compressed-tar\"                                        => {\"tar.lrz\", \"tlrz\"},\n    \"application/x-lua-bytecode\"                                                => {\"luac\"},\n    \"application/x-lyx\"                                                         => {\"lyx\"},\n    \"application/x-lz4\"                                                         => {\"lz4\"},\n    \"application/x-lz4-compressed-tar\"                                          => {\"tar.lz4\"},\n    \"application/x-lzh-compressed\"                                              => {\"lzh\", \"lha\"},\n    \"application/x-lzip\"                                                        => {\"lz\"},\n    \"application/x-lzip-compressed-tar\"                                         => {\"tar.lz\"},\n    \"application/x-lzma\"                                                        => {\"lzma\"},\n    \"application/x-lzma-compressed-tar\"                                         => {\"tar.lzma\", \"tlz\"},\n    \"application/x-lzop\"                                                        => {\"lzo\"},\n    \"application/x-lzpdf\"                                                       => {\"pdf.lz\"},\n    \"application/x-m4\"                                                          => {\"m4\"},\n    \"application/x-magicpoint\"                                                  => {\"mgp\"},\n    \"application/x-makeself\"                                                    => {\"run\"},\n    \"application/x-mame-chd\"                                                    => {\"chd\"},\n    \"application/x-markaby\"                                                     => {\"mab\"},\n    \"application/x-mathematica\"                                                 => {\"nb\"},\n    \"application/x-mdb\"                                                         => {\"mdb\"},\n    \"application/x-mie\"                                                         => {\"mie\"},\n    \"application/x-mif\"                                                         => {\"mif\"},\n    \"application/x-mimearchive\"                                                 => {\"mhtml\", \"mht\"},\n    \"application/x-mobi8-ebook\"                                                 => {\"azw3\", \"kfx\"},\n    \"application/x-mobipocket-ebook\"                                            => {\"prc\", \"mobi\"},\n    \"application/x-modrinth-modpack+zip\"                                        => {\"mrpack\"},\n    \"application/x-ms-application\"                                              => {\"application\"},\n    \"application/x-ms-asx\"                                                      => {\"asx\", \"wax\", \"wvx\", \"wmx\"},\n    \"application/x-ms-dos-executable\"                                           => {\"exe\", \"dll\", \"cpl\", \"drv\", \"scr\"},\n    \"application/x-ms-ne-executable\"                                            => {\"exe\", \"dll\", \"cpl\", \"drv\", \"scr\"},\n    \"application/x-ms-pdb\"                                                      => {\"pdb\"},\n    \"application/x-ms-shortcut\"                                                 => {\"lnk\"},\n    \"application/x-ms-wim\"                                                      => {\"wim\", \"swm\"},\n    \"application/x-ms-wmd\"                                                      => {\"wmd\"},\n    \"application/x-ms-wmz\"                                                      => {\"wmz\"},\n    \"application/x-ms-xbap\"                                                     => {\"xbap\"},\n    \"application/x-msaccess\"                                                    => {\"mdb\"},\n    \"application/x-msbinder\"                                                    => {\"obd\"},\n    \"application/x-mscardfile\"                                                  => {\"crd\"},\n    \"application/x-msclip\"                                                      => {\"clp\"},\n    \"application/x-msdos-program\"                                               => {\"exe\"},\n    \"application/x-msdownload\"                                                  => {\"exe\", \"dll\", \"com\", \"bat\", \"msi\", \"cpl\", \"drv\", \"scr\"},\n    \"application/x-msexcel\"                                                     => {\"xls\", \"xlc\", \"xll\", \"xlm\", \"xlw\", \"xla\", \"xlt\", \"xld\"},\n    \"application/x-msi\"                                                         => {\"msi\"},\n    \"application/x-msmediaview\"                                                 => {\"mvb\", \"m13\", \"m14\"},\n    \"application/x-msmetafile\"                                                  => {\"wmf\", \"wmz\", \"emf\", \"emz\"},\n    \"application/x-msmoney\"                                                     => {\"mny\"},\n    \"application/x-mspowerpoint\"                                                => {\"ppz\", \"ppt\", \"pps\", \"pot\"},\n    \"application/x-mspublisher\"                                                 => {\"pub\"},\n    \"application/x-msschedule\"                                                  => {\"scd\"},\n    \"application/x-msterminal\"                                                  => {\"trm\"},\n    \"application/x-mswinurl\"                                                    => {\"url\"},\n    \"application/x-msword\"                                                      => {\"doc\"},\n    \"application/x-mswrite\"                                                     => {\"wri\"},\n    \"application/x-msx-rom\"                                                     => {\"msx\"},\n    \"application/x-n64-rom\"                                                     => {\"n64\", \"z64\", \"v64\"},\n    \"application/x-navi-animation\"                                              => {\"ani\"},\n    \"application/x-neo-geo-pocket-color-rom\"                                    => {\"ngc\"},\n    \"application/x-neo-geo-pocket-rom\"                                          => {\"ngp\"},\n    \"application/x-nes-rom\"                                                     => {\"nes\", \"nez\", \"unf\", \"unif\"},\n    \"application/x-netcdf\"                                                      => {\"nc\", \"cdf\"},\n    \"application/x-netshow-channel\"                                             => {\"nsc\"},\n    \"application/x-nintendo-3ds-executable\"                                     => {\"3dsx\"},\n    \"application/x-nintendo-3ds-rom\"                                            => {\"3ds\", \"cci\"},\n    \"application/x-nintendo-ds-rom\"                                             => {\"nds\"},\n    \"application/x-nintendo-switch-xci\"                                         => {\"xci\"},\n    \"application/x-ns-proxy-autoconfig\"                                         => {\"pac\"},\n    \"application/x-nuscript\"                                                    => {\"nu\"},\n    \"application/x-nx-xci\"                                                      => {\"xci\"},\n    \"application/x-nzb\"                                                         => {\"nzb\"},\n    \"application/x-object\"                                                      => {\"o\", \"mod\"},\n    \"application/x-ogg\"                                                         => {\"ogx\"},\n    \"application/x-oleo\"                                                        => {\"oleo\"},\n    \"application/x-openvpn-profile\"                                             => {\"openvpn\", \"ovpn\"},\n    \"application/x-openzim\"                                                     => {\"zim\"},\n    \"application/x-pagemaker\"                                                   => {\"p65\", \"pm\", \"pm6\", \"pmd\"},\n    \"application/x-pak\"                                                         => {\"pak\"},\n    \"application/x-palm-database\"                                               => {\"prc\", \"pdb\", \"pqa\", \"oprc\"},\n    \"application/x-par2\"                                                        => {\"PAR2\", \"par2\"},\n    \"application/x-parquet\"                                                     => {\"parquet\"},\n    \"application/x-partial-download\"                                            => {\"wkdownload\", \"crdownload\", \"part\"},\n    \"application/x-pc-engine-rom\"                                               => {\"pce\"},\n    \"application/x-pcap\"                                                        => {\"pcap\", \"cap\", \"dmp\"},\n    \"application/x-pcapng\"                                                      => {\"pcapng\", \"scap\", \"ntar\"},\n    \"application/x-pdf\"                                                         => {\"pdf\"},\n    \"application/x-perl\"                                                        => {\"pl\", \"pm\", \"PL\", \"al\", \"perl\", \"pod\", \"t\"},\n    \"application/x-photoshop\"                                                   => {\"psd\"},\n    \"application/x-php\"                                                         => {\"php\", \"php3\", \"php4\", \"php5\", \"phps\"},\n    \"application/x-pilot\"                                                       => {\"prc\", \"pdb\"},\n    \"application/x-pkcs12\"                                                      => {\"p12\", \"pfx\"},\n    \"application/x-pkcs7-certificates\"                                          => {\"p7b\", \"spc\"},\n    \"application/x-pkcs7-certreqresp\"                                           => {\"p7r\"},\n    \"application/x-planperfect\"                                                 => {\"pln\"},\n    \"application/x-pocket-word\"                                                 => {\"psw\"},\n    \"application/x-powershell\"                                                  => {\"ps1\"},\n    \"application/x-pw\"                                                          => {\"pw\"},\n    \"application/x-pyspread-bz-spreadsheet\"                                     => {\"pys\"},\n    \"application/x-pyspread-spreadsheet\"                                        => {\"pysu\"},\n    \"application/x-python-bytecode\"                                             => {\"pyc\", \"pyo\"},\n    \"application/x-qbrew\"                                                       => {\"qbrew\"},\n    \"application/x-qed-disk\"                                                    => {\"qed\"},\n    \"application/x-qemu-disk\"                                                   => {\"qcow2\", \"qcow\"},\n    \"application/x-qpress\"                                                      => {\"qp\"},\n    \"application/x-qtiplot\"                                                     => {\"qti\", \"qti.gz\"},\n    \"application/x-quattropro\"                                                  => {\"wb1\", \"wb2\", \"wb3\", \"qpw\"},\n    \"application/x-quicktime-media-link\"                                        => {\"qtl\"},\n    \"application/x-quicktimeplayer\"                                             => {\"qtl\"},\n    \"application/x-qw\"                                                          => {\"qif\"},\n    \"application/x-rar\"                                                         => {\"rar\"},\n    \"application/x-rar-compressed\"                                              => {\"rar\"},\n    \"application/x-raw-disk-image\"                                              => {\"raw-disk-image\", \"img\"},\n    \"application/x-raw-disk-image-xz-compressed\"                                => {\"raw-disk-image.xz\", \"img.xz\"},\n    \"application/x-raw-floppy-disk-image\"                                       => {\"fd\", \"qd\"},\n    \"application/x-redhat-package-manager\"                                      => {\"rpm\"},\n    \"application/x-reject\"                                                      => {\"rej\"},\n    \"application/x-research-info-systems\"                                       => {\"ris\"},\n    \"application/x-rnc\"                                                         => {\"rnc\"},\n    \"application/x-rpm\"                                                         => {\"rpm\"},\n    \"application/x-ruby\"                                                        => {\"rb\"},\n    \"application/x-rzip\"                                                        => {\"rz\"},\n    \"application/x-rzip-compressed-tar\"                                         => {\"tar.rz\", \"trz\"},\n    \"application/x-sami\"                                                        => {\"smi\", \"sami\"},\n    \"application/x-sap-file\"                                                    => {\"sap\"},\n    \"application/x-saturn-rom\"                                                  => {\"iso\"},\n    \"application/x-sdp\"                                                         => {\"sdp\"},\n    \"application/x-sea\"                                                         => {\"sea\"},\n    \"application/x-sega-cd-rom\"                                                 => {\"iso\"},\n    \"application/x-sega-pico-rom\"                                               => {\"iso\"},\n    \"application/x-sg1000-rom\"                                                  => {\"sg\"},\n    \"application/x-sh\"                                                          => {\"sh\"},\n    \"application/x-shar\"                                                        => {\"shar\"},\n    \"application/x-shared-library-la\"                                           => {\"la\"},\n    \"application/x-sharedlib\"                                                   => {\"so\", \"so.[0-9]*\"},\n    \"application/x-shellscript\"                                                 => {\"sh\"},\n    \"application/x-shockwave-flash\"                                             => {\"swf\", \"spl\"},\n    \"application/x-shorten\"                                                     => {\"shn\"},\n    \"application/x-siag\"                                                        => {\"siag\"},\n    \"application/x-silverlight-app\"                                             => {\"xap\"},\n    \"application/x-sit\"                                                         => {\"sit\"},\n    \"application/x-sitx\"                                                        => {\"sitx\"},\n    \"application/x-smaf\"                                                        => {\"mmf\", \"smaf\"},\n    \"application/x-sms-rom\"                                                     => {\"sms\"},\n    \"application/x-snes-rom\"                                                    => {\"sfc\", \"smc\"},\n    \"application/x-sony-bbeb\"                                                   => {\"lrf\"},\n    \"application/x-source-rpm\"                                                  => {\"src.rpm\", \"spm\"},\n    \"application/x-spss-por\"                                                    => {\"por\"},\n    \"application/x-spss-sav\"                                                    => {\"sav\", \"zsav\"},\n    \"application/x-spss-savefile\"                                               => {\"sav\", \"zsav\"},\n    \"application/x-sql\"                                                         => {\"sql\"},\n    \"application/x-sqlite2\"                                                     => {\"sqlite2\"},\n    \"application/x-sqlite3\"                                                     => {\"sqlite3\"},\n    \"application/x-srt\"                                                         => {\"srt\"},\n    \"application/x-starcalc\"                                                    => {\"sdc\"},\n    \"application/x-starchart\"                                                   => {\"sds\"},\n    \"application/x-stardraw\"                                                    => {\"sda\"},\n    \"application/x-starimpress\"                                                 => {\"sdd\"},\n    \"application/x-starmail\"                                                    => {\"smd\"},\n    \"application/x-starmath\"                                                    => {\"smf\"},\n    \"application/x-starwriter\"                                                  => {\"sdw\", \"vor\"},\n    \"application/x-starwriter-global\"                                           => {\"sgl\"},\n    \"application/x-stuffit\"                                                     => {\"sit\"},\n    \"application/x-stuffitx\"                                                    => {\"sitx\"},\n    \"application/x-subrip\"                                                      => {\"srt\"},\n    \"application/x-sv4cpio\"                                                     => {\"sv4cpio\"},\n    \"application/x-sv4crc\"                                                      => {\"sv4crc\"},\n    \"application/x-sylk\"                                                        => {\"sylk\", \"slk\"},\n    \"application/x-t3vm-image\"                                                  => {\"t3\"},\n    \"application/x-t602\"                                                        => {\"602\"},\n    \"application/x-tads\"                                                        => {\"gam\"},\n    \"application/x-tar\"                                                         => {\"tar\", \"gtar\", \"gem\"},\n    \"application/x-targa\"                                                       => {\"tga\", \"icb\", \"tpic\", \"vda\", \"vst\"},\n    \"application/x-tarz\"                                                        => {\"tar.Z\", \"taz\"},\n    \"application/x-tcl\"                                                         => {\"tcl\", \"tk\"},\n    \"application/x-tex\"                                                         => {\"tex\", \"ltx\", \"sty\", \"cls\", \"dtx\", \"ins\", \"latex\"},\n    \"application/x-tex-gf\"                                                      => {\"gf\"},\n    \"application/x-tex-pk\"                                                      => {\"pk\"},\n    \"application/x-tex-tfm\"                                                     => {\"tfm\"},\n    \"application/x-texinfo\"                                                     => {\"texinfo\", \"texi\"},\n    \"application/x-tga\"                                                         => {\"tga\", \"icb\", \"tpic\", \"vda\", \"vst\"},\n    \"application/x-tgif\"                                                        => {\"obj\"},\n    \"application/x-theme\"                                                       => {\"theme\"},\n    \"application/x-thomson-cartridge-memo7\"                                     => {\"m7\"},\n    \"application/x-thomson-cassette\"                                            => {\"k7\"},\n    \"application/x-thomson-sap-image\"                                           => {\"sap\"},\n    \"application/x-tiled-tmx\"                                                   => {\"tmx\"},\n    \"application/x-tiled-tsx\"                                                   => {\"tsx\"},\n    \"application/x-trash\"                                                       => {\"bak\", \"old\", \"sik\"},\n    \"application/x-trig\"                                                        => {\"trig\"},\n    \"application/x-troff\"                                                       => {\"tr\", \"roff\", \"t\"},\n    \"application/x-troff-man\"                                                   => {\"man\", \"[1-9]\"},\n    \"application/x-tzo\"                                                         => {\"tar.lzo\", \"tzo\"},\n    \"application/x-ufraw\"                                                       => {\"ufraw\"},\n    \"application/x-ustar\"                                                       => {\"ustar\"},\n    \"application/x-vdi-disk\"                                                    => {\"vdi\"},\n    \"application/x-vhd-disk\"                                                    => {\"vhd\", \"vpc\"},\n    \"application/x-vhdx-disk\"                                                   => {\"vhdx\"},\n    \"application/x-virtual-boy-rom\"                                             => {\"vb\"},\n    \"application/x-virtualbox-hdd\"                                              => {\"hdd\"},\n    \"application/x-virtualbox-ova\"                                              => {\"ova\"},\n    \"application/x-virtualbox-ovf\"                                              => {\"ovf\"},\n    \"application/x-virtualbox-vbox\"                                             => {\"vbox\"},\n    \"application/x-virtualbox-vbox-extpack\"                                     => {\"vbox-extpack\"},\n    \"application/x-virtualbox-vdi\"                                              => {\"vdi\"},\n    \"application/x-virtualbox-vhd\"                                              => {\"vhd\", \"vpc\"},\n    \"application/x-virtualbox-vhdx\"                                             => {\"vhdx\"},\n    \"application/x-virtualbox-vmdk\"                                             => {\"vmdk\"},\n    \"application/x-vmdk-disk\"                                                   => {\"vmdk\"},\n    \"application/x-vnd.kde.kexi\"                                                => {\"kexi\"},\n    \"application/x-wais-source\"                                                 => {\"src\"},\n    \"application/x-wbfs\"                                                        => {\"iso\"},\n    \"application/x-web-app-manifest+json\"                                       => {\"webapp\"},\n    \"application/x-wia\"                                                         => {\"iso\"},\n    \"application/x-wii-iso-image\"                                               => {\"iso\"},\n    \"application/x-wii-rom\"                                                     => {\"iso\"},\n    \"application/x-wii-wad\"                                                     => {\"wad\"},\n    \"application/x-win-lnk\"                                                     => {\"lnk\"},\n    \"application/x-windows-themepack\"                                           => {\"themepack\"},\n    \"application/x-wmf\"                                                         => {\"wmf\"},\n    \"application/x-wonderswan-color-rom\"                                        => {\"wsc\"},\n    \"application/x-wonderswan-rom\"                                              => {\"ws\"},\n    \"application/x-wordperfect\"                                                 => {\"wp\", \"wp4\", \"wp5\", \"wp6\", \"wpd\", \"wpp\"},\n    \"application/x-wpg\"                                                         => {\"wpg\"},\n    \"application/x-wwf\"                                                         => {\"wwf\"},\n    \"application/x-x509-ca-cert\"                                                => {\"der\", \"crt\", \"pem\", \"cert\"},\n    \"application/x-xar\"                                                         => {\"xar\", \"pkg\"},\n    \"application/x-xbel\"                                                        => {\"xbel\"},\n    \"application/x-xfig\"                                                        => {\"fig\"},\n    \"application/x-xliff\"                                                       => {\"xlf\", \"xliff\"},\n    \"application/x-xliff+xml\"                                                   => {\"xlf\"},\n    \"application/x-xpinstall\"                                                   => {\"xpi\"},\n    \"application/x-xspf+xml\"                                                    => {\"xspf\"},\n    \"application/x-xz\"                                                          => {\"xz\"},\n    \"application/x-xz-compressed-tar\"                                           => {\"tar.xz\", \"txz\"},\n    \"application/x-xzpdf\"                                                       => {\"pdf.xz\"},\n    \"application/x-yaml\"                                                        => {\"yaml\", \"yml\"},\n    \"application/x-zip\"                                                         => {\"zip\", \"zipx\"},\n    \"application/x-zip-compressed\"                                              => {\"zip\", \"zipx\"},\n    \"application/x-zip-compressed-fb2\"                                          => {\"fb2.zip\"},\n    \"application/x-zmachine\"                                                    => {\"z1\", \"z2\", \"z3\", \"z4\", \"z5\", \"z6\", \"z7\", \"z8\"},\n    \"application/x-zoo\"                                                         => {\"zoo\"},\n    \"application/x-zpaq\"                                                        => {\"zpaq\"},\n    \"application/x-zstd-compressed-tar\"                                         => {\"tar.zst\", \"tzst\"},\n    \"application/xaml+xml\"                                                      => {\"xaml\"},\n    \"application/xcap-att+xml\"                                                  => {\"xav\"},\n    \"application/xcap-caps+xml\"                                                 => {\"xca\"},\n    \"application/xcap-diff+xml\"                                                 => {\"xdf\"},\n    \"application/xcap-el+xml\"                                                   => {\"xel\"},\n    \"application/xcap-error+xml\"                                                => {\"xer\"},\n    \"application/xcap-ns+xml\"                                                   => {\"xns\"},\n    \"application/xenc+xml\"                                                      => {\"xenc\"},\n    \"application/xfdf\"                                                          => {\"xfdf\"},\n    \"application/xhtml+xml\"                                                     => {\"xhtml\", \"xht\", \"html\", \"htm\"},\n    \"application/xliff+xml\"                                                     => {\"xlf\", \"xliff\"},\n    \"application/xml\"                                                           => {\"xml\", \"xsl\", \"xsd\", \"rng\", \"xbl\"},\n    \"application/xml-dtd\"                                                       => {\"dtd\"},\n    \"application/xml-external-parsed-entity\"                                    => {\"ent\"},\n    \"application/xop+xml\"                                                       => {\"xop\"},\n    \"application/xproc+xml\"                                                     => {\"xpl\"},\n    \"application/xps\"                                                           => {\"xps\"},\n    \"application/xslt+xml\"                                                      => {\"xsl\", \"xslt\"},\n    \"application/xspf+xml\"                                                      => {\"xspf\"},\n    \"application/xv+xml\"                                                        => {\"mxml\", \"xhvml\", \"xvml\", \"xvm\"},\n    \"application/yaml\"                                                          => {\"yaml\", \"yml\"},\n    \"application/yang\"                                                          => {\"yang\"},\n    \"application/yin+xml\"                                                       => {\"yin\"},\n    \"application/zip\"                                                           => {\"zip\", \"zipx\"},\n    \"application/zlib\"                                                          => {\"zz\"},\n    \"application/zstd\"                                                          => {\"zst\"},\n    \"audio/3gpp\"                                                                => {\"3gpp\", \"3gp\", \"3ga\"},\n    \"audio/3gpp-encrypted\"                                                      => {\"3gp\", \"3gpp\", \"3ga\"},\n    \"audio/3gpp2\"                                                               => {\"3g2\", \"3gp2\", \"3gpp2\"},\n    \"audio/aac\"                                                                 => {\"aac\", \"adts\", \"ass\"},\n    \"audio/ac3\"                                                                 => {\"ac3\"},\n    \"audio/adpcm\"                                                               => {\"adp\"},\n    \"audio/amr\"                                                                 => {\"amr\"},\n    \"audio/amr-encrypted\"                                                       => {\"amr\"},\n    \"audio/amr-wb\"                                                              => {\"awb\"},\n    \"audio/amr-wb-encrypted\"                                                    => {\"awb\"},\n    \"audio/annodex\"                                                             => {\"axa\"},\n    \"audio/basic\"                                                               => {\"au\", \"snd\"},\n    \"audio/dff\"                                                                 => {\"dff\"},\n    \"audio/dsd\"                                                                 => {\"dsf\"},\n    \"audio/dsf\"                                                                 => {\"dsf\"},\n    \"audio/flac\"                                                                => {\"flac\"},\n    \"audio/imelody\"                                                             => {\"imy\", \"ime\"},\n    \"audio/m3u\"                                                                 => {\"m3u\", \"m3u8\", \"vlc\"},\n    \"audio/m4a\"                                                                 => {\"m4a\", \"f4a\"},\n    \"audio/midi\"                                                                => {\"mid\", \"midi\", \"kar\", \"rmi\"},\n    \"audio/mobile-xmf\"                                                          => {\"mxmf\"},\n    \"audio/mp2\"                                                                 => {\"mp2\"},\n    \"audio/mp3\"                                                                 => {\"mp3\", \"mpga\"},\n    \"audio/mp4\"                                                                 => {\"m4a\", \"mp4a\", \"f4a\"},\n    \"audio/mpeg\"                                                                => {\"mp3\", \"mpga\", \"mp2\", \"mp2a\", \"m2a\", \"m3a\"},\n    \"audio/mpegurl\"                                                             => {\"m3u\", \"m3u8\", \"vlc\"},\n    \"audio/ogg\"                                                                 => {\"ogg\", \"oga\", \"spx\", \"opus\"},\n    \"audio/prs.sid\"                                                             => {\"sid\", \"psid\"},\n    \"audio/s3m\"                                                                 => {\"s3m\"},\n    \"audio/scpls\"                                                               => {\"pls\"},\n    \"audio/silk\"                                                                => {\"sil\"},\n    \"audio/tta\"                                                                 => {\"tta\"},\n    \"audio/usac\"                                                                => {\"loas\", \"xhe\"},\n    \"audio/vnd.audible\"                                                         => {\"aa\", \"aax\"},\n    \"audio/vnd.audible.aax\"                                                     => {\"aax\"},\n    \"audio/vnd.audible.aaxc\"                                                    => {\"aaxc\"},\n    \"audio/vnd.dece.audio\"                                                      => {\"uva\", \"uvva\"},\n    \"audio/vnd.digital-winds\"                                                   => {\"eol\"},\n    \"audio/vnd.dra\"                                                             => {\"dra\"},\n    \"audio/vnd.dts\"                                                             => {\"dts\"},\n    \"audio/vnd.dts.hd\"                                                          => {\"dtshd\"},\n    \"audio/vnd.lucent.voice\"                                                    => {\"lvp\"},\n    \"audio/vnd.m-realaudio\"                                                     => {\"ra\", \"rax\"},\n    \"audio/vnd.ms-playready.media.pya\"                                          => {\"pya\"},\n    \"audio/vnd.nokia.mobile-xmf\"                                                => {\"mxmf\"},\n    \"audio/vnd.nuera.ecelp4800\"                                                 => {\"ecelp4800\"},\n    \"audio/vnd.nuera.ecelp7470\"                                                 => {\"ecelp7470\"},\n    \"audio/vnd.nuera.ecelp9600\"                                                 => {\"ecelp9600\"},\n    \"audio/vnd.rip\"                                                             => {\"rip\"},\n    \"audio/vnd.rn-realaudio\"                                                    => {\"ra\", \"rax\"},\n    \"audio/vnd.wave\"                                                            => {\"wav\"},\n    \"audio/vorbis\"                                                              => {\"oga\", \"ogg\"},\n    \"audio/wav\"                                                                 => {\"wav\"},\n    \"audio/wave\"                                                                => {\"wav\"},\n    \"audio/webm\"                                                                => {\"weba\"},\n    \"audio/wma\"                                                                 => {\"wma\"},\n    \"audio/x-aac\"                                                               => {\"aac\", \"adts\", \"ass\"},\n    \"audio/x-aifc\"                                                              => {\"aifc\", \"aiffc\"},\n    \"audio/x-aiff\"                                                              => {\"aif\", \"aiff\", \"aifc\"},\n    \"audio/x-aiffc\"                                                             => {\"aifc\", \"aiffc\"},\n    \"audio/x-amzxml\"                                                            => {\"amz\"},\n    \"audio/x-annodex\"                                                           => {\"axa\"},\n    \"audio/x-ape\"                                                               => {\"ape\"},\n    \"audio/x-caf\"                                                               => {\"caf\"},\n    \"audio/x-dff\"                                                               => {\"dff\"},\n    \"audio/x-dsd\"                                                               => {\"dsf\"},\n    \"audio/x-dsf\"                                                               => {\"dsf\"},\n    \"audio/x-dts\"                                                               => {\"dts\"},\n    \"audio/x-dtshd\"                                                             => {\"dtshd\"},\n    \"audio/x-flac\"                                                              => {\"flac\"},\n    \"audio/x-flac+ogg\"                                                          => {\"oga\", \"ogg\"},\n    \"audio/x-gsm\"                                                               => {\"gsm\"},\n    \"audio/x-hx-aac-adts\"                                                       => {\"aac\", \"adts\", \"ass\"},\n    \"audio/x-imelody\"                                                           => {\"imy\", \"ime\"},\n    \"audio/x-iriver-pla\"                                                        => {\"pla\"},\n    \"audio/x-it\"                                                                => {\"it\"},\n    \"audio/x-m3u\"                                                               => {\"m3u\", \"m3u8\", \"vlc\"},\n    \"audio/x-m4a\"                                                               => {\"m4a\", \"f4a\"},\n    \"audio/x-m4b\"                                                               => {\"m4b\", \"f4b\"},\n    \"audio/x-m4r\"                                                               => {\"m4r\"},\n    \"audio/x-matroska\"                                                          => {\"mka\"},\n    \"audio/x-midi\"                                                              => {\"mid\", \"midi\", \"kar\"},\n    \"audio/x-minipsf\"                                                           => {\"minipsf\"},\n    \"audio/x-mo3\"                                                               => {\"mo3\"},\n    \"audio/x-mod\"                                                               => {\"mod\", \"ult\", \"uni\", \"m15\", \"mtm\", \"669\", \"med\"},\n    \"audio/x-mp2\"                                                               => {\"mp2\"},\n    \"audio/x-mp3\"                                                               => {\"mp3\", \"mpga\"},\n    \"audio/x-mp3-playlist\"                                                      => {\"m3u\", \"m3u8\", \"vlc\"},\n    \"audio/x-mpeg\"                                                              => {\"mp3\", \"mpga\"},\n    \"audio/x-mpegurl\"                                                           => {\"m3u\", \"m3u8\", \"vlc\"},\n    \"audio/x-mpg\"                                                               => {\"mp3\", \"mpga\"},\n    \"audio/x-ms-asx\"                                                            => {\"asx\", \"wax\", \"wvx\", \"wmx\"},\n    \"audio/x-ms-wax\"                                                            => {\"wax\"},\n    \"audio/x-ms-wma\"                                                            => {\"wma\"},\n    \"audio/x-ms-wmv\"                                                            => {\"wmv\"},\n    \"audio/x-musepack\"                                                          => {\"mpc\", \"mpp\", \"mp+\"},\n    \"audio/x-ogg\"                                                               => {\"oga\", \"ogg\", \"opus\"},\n    \"audio/x-oggflac\"                                                           => {\"oga\", \"ogg\"},\n    \"audio/x-opus+ogg\"                                                          => {\"opus\"},\n    \"audio/x-pn-audibleaudio\"                                                   => {\"aa\", \"aax\"},\n    \"audio/x-pn-realaudio\"                                                      => {\"ram\", \"ra\", \"rax\"},\n    \"audio/x-pn-realaudio-plugin\"                                               => {\"rmp\"},\n    \"audio/x-psf\"                                                               => {\"psf\"},\n    \"audio/x-psflib\"                                                            => {\"psflib\"},\n    \"audio/x-realaudio\"                                                         => {\"ra\"},\n    \"audio/x-rn-3gpp-amr\"                                                       => {\"3gp\", \"3gpp\", \"3ga\"},\n    \"audio/x-rn-3gpp-amr-encrypted\"                                             => {\"3gp\", \"3gpp\", \"3ga\"},\n    \"audio/x-rn-3gpp-amr-wb\"                                                    => {\"3gp\", \"3gpp\", \"3ga\"},\n    \"audio/x-rn-3gpp-amr-wb-encrypted\"                                          => {\"3gp\", \"3gpp\", \"3ga\"},\n    \"audio/x-s3m\"                                                               => {\"s3m\"},\n    \"audio/x-scpls\"                                                             => {\"pls\"},\n    \"audio/x-shorten\"                                                           => {\"shn\"},\n    \"audio/x-speex\"                                                             => {\"spx\"},\n    \"audio/x-speex+ogg\"                                                         => {\"oga\", \"ogg\", \"spx\"},\n    \"audio/x-stm\"                                                               => {\"stm\"},\n    \"audio/x-tak\"                                                               => {\"tak\"},\n    \"audio/x-tta\"                                                               => {\"tta\"},\n    \"audio/x-voc\"                                                               => {\"voc\"},\n    \"audio/x-vorbis\"                                                            => {\"oga\", \"ogg\"},\n    \"audio/x-vorbis+ogg\"                                                        => {\"oga\", \"ogg\"},\n    \"audio/x-wav\"                                                               => {\"wav\"},\n    \"audio/x-wavpack\"                                                           => {\"wv\", \"wvp\"},\n    \"audio/x-wavpack-correction\"                                                => {\"wvc\"},\n    \"audio/x-xi\"                                                                => {\"xi\"},\n    \"audio/x-xm\"                                                                => {\"xm\"},\n    \"audio/x-xmf\"                                                               => {\"xmf\"},\n    \"audio/xm\"                                                                  => {\"xm\"},\n    \"audio/xmf\"                                                                 => {\"xmf\"},\n    \"chemical/x-cdx\"                                                            => {\"cdx\"},\n    \"chemical/x-cif\"                                                            => {\"cif\"},\n    \"chemical/x-cmdf\"                                                           => {\"cmdf\"},\n    \"chemical/x-cml\"                                                            => {\"cml\"},\n    \"chemical/x-csml\"                                                           => {\"csml\"},\n    \"chemical/x-pdb\"                                                            => {\"pdb\", \"brk\"},\n    \"chemical/x-xyz\"                                                            => {\"xyz\"},\n    \"flv-application/octet-stream\"                                              => {\"flv\"},\n    \"font/collection\"                                                           => {\"ttc\"},\n    \"font/otf\"                                                                  => {\"otf\"},\n    \"font/ttf\"                                                                  => {\"ttf\"},\n    \"font/woff\"                                                                 => {\"woff\"},\n    \"font/woff2\"                                                                => {\"woff2\"},\n    \"image/aces\"                                                                => {\"exr\"},\n    \"image/apng\"                                                                => {\"apng\", \"png\"},\n    \"image/astc\"                                                                => {\"astc\"},\n    \"image/avci\"                                                                => {\"avci\"},\n    \"image/avcs\"                                                                => {\"avcs\"},\n    \"image/avif\"                                                                => {\"avif\", \"avifs\"},\n    \"image/avif-sequence\"                                                       => {\"avif\", \"avifs\"},\n    \"image/bmp\"                                                                 => {\"bmp\", \"dib\"},\n    \"image/cdr\"                                                                 => {\"cdr\"},\n    \"image/cgm\"                                                                 => {\"cgm\"},\n    \"image/dicom-rle\"                                                           => {\"drle\"},\n    \"image/dpx\"                                                                 => {\"dpx\"},\n    \"image/emf\"                                                                 => {\"emf\"},\n    \"image/fax-g3\"                                                              => {\"g3\"},\n    \"image/fits\"                                                                => {\"fits\", \"fit\", \"fts\"},\n    \"image/g3fax\"                                                               => {\"g3\"},\n    \"image/gif\"                                                                 => {\"gif\"},\n    \"image/heic\"                                                                => {\"heic\", \"heif\", \"hif\"},\n    \"image/heic-sequence\"                                                       => {\"heics\", \"heic\", \"heif\", \"hif\"},\n    \"image/heif\"                                                                => {\"heif\", \"heic\", \"hif\"},\n    \"image/heif-sequence\"                                                       => {\"heifs\", \"heic\", \"heif\", \"hif\"},\n    \"image/hej2k\"                                                               => {\"hej2\"},\n    \"image/hsj2\"                                                                => {\"hsj2\"},\n    \"image/ico\"                                                                 => {\"ico\"},\n    \"image/icon\"                                                                => {\"ico\"},\n    \"image/ief\"                                                                 => {\"ief\"},\n    \"image/jls\"                                                                 => {\"jls\"},\n    \"image/jp2\"                                                                 => {\"jp2\", \"jpg2\"},\n    \"image/jpeg\"                                                                => {\"jpg\", \"jpeg\", \"jpe\", \"jfif\"},\n    \"image/jpeg2000\"                                                            => {\"jp2\", \"jpg2\"},\n    \"image/jpeg2000-image\"                                                      => {\"jp2\", \"jpg2\"},\n    \"image/jph\"                                                                 => {\"jph\"},\n    \"image/jphc\"                                                                => {\"jhc\"},\n    \"image/jpm\"                                                                 => {\"jpm\", \"jpgm\"},\n    \"image/jpx\"                                                                 => {\"jpx\", \"jpf\"},\n    \"image/jxl\"                                                                 => {\"jxl\"},\n    \"image/jxr\"                                                                 => {\"jxr\", \"hdp\", \"wdp\"},\n    \"image/jxra\"                                                                => {\"jxra\"},\n    \"image/jxrs\"                                                                => {\"jxrs\"},\n    \"image/jxs\"                                                                 => {\"jxs\"},\n    \"image/jxsc\"                                                                => {\"jxsc\"},\n    \"image/jxsi\"                                                                => {\"jxsi\"},\n    \"image/jxss\"                                                                => {\"jxss\"},\n    \"image/ktx\"                                                                 => {\"ktx\"},\n    \"image/ktx2\"                                                                => {\"ktx2\"},\n    \"image/openraster\"                                                          => {\"ora\"},\n    \"image/pdf\"                                                                 => {\"pdf\"},\n    \"image/photoshop\"                                                           => {\"psd\"},\n    \"image/pjpeg\"                                                               => {\"jpg\", \"jpeg\", \"jpe\", \"jfif\"},\n    \"image/png\"                                                                 => {\"png\"},\n    \"image/prs.btif\"                                                            => {\"btif\", \"btf\"},\n    \"image/prs.pti\"                                                             => {\"pti\"},\n    \"image/psd\"                                                                 => {\"psd\"},\n    \"image/qoi\"                                                                 => {\"qoi\"},\n    \"image/rle\"                                                                 => {\"rle\"},\n    \"image/sgi\"                                                                 => {\"sgi\"},\n    \"image/svg\"                                                                 => {\"svg\"},\n    \"image/svg+xml\"                                                             => {\"svg\", \"svgz\"},\n    \"image/svg+xml-compressed\"                                                  => {\"svgz\", \"svg.gz\"},\n    \"image/t38\"                                                                 => {\"t38\"},\n    \"image/targa\"                                                               => {\"tga\", \"icb\", \"tpic\", \"vda\", \"vst\"},\n    \"image/tga\"                                                                 => {\"tga\", \"icb\", \"tpic\", \"vda\", \"vst\"},\n    \"image/tiff\"                                                                => {\"tif\", \"tiff\"},\n    \"image/tiff-fx\"                                                             => {\"tfx\"},\n    \"image/vnd.adobe.photoshop\"                                                 => {\"psd\"},\n    \"image/vnd.airzip.accelerator.azv\"                                          => {\"azv\"},\n    \"image/vnd.dece.graphic\"                                                    => {\"uvi\", \"uvvi\", \"uvg\", \"uvvg\"},\n    \"image/vnd.djvu\"                                                            => {\"djvu\", \"djv\"},\n    \"image/vnd.djvu+multipage\"                                                  => {\"djvu\", \"djv\"},\n    \"image/vnd.dvb.subtitle\"                                                    => {\"sub\"},\n    \"image/vnd.dwg\"                                                             => {\"dwg\"},\n    \"image/vnd.dxf\"                                                             => {\"dxf\"},\n    \"image/vnd.fastbidsheet\"                                                    => {\"fbs\"},\n    \"image/vnd.fpx\"                                                             => {\"fpx\"},\n    \"image/vnd.fst\"                                                             => {\"fst\"},\n    \"image/vnd.fujixerox.edmics-mmr\"                                            => {\"mmr\"},\n    \"image/vnd.fujixerox.edmics-rlc\"                                            => {\"rlc\"},\n    \"image/vnd.microsoft.icon\"                                                  => {\"ico\"},\n    \"image/vnd.mozilla.apng\"                                                    => {\"apng\", \"png\"},\n    \"image/vnd.ms-dds\"                                                          => {\"dds\"},\n    \"image/vnd.ms-modi\"                                                         => {\"mdi\"},\n    \"image/vnd.ms-photo\"                                                        => {\"wdp\", \"jxr\", \"hdp\"},\n    \"image/vnd.net-fpx\"                                                         => {\"npx\"},\n    \"image/vnd.pco.b16\"                                                         => {\"b16\"},\n    \"image/vnd.radiance\"                                                        => {\"hdr\", \"pic\", \"rgbe\", \"xyze\"},\n    \"image/vnd.rn-realpix\"                                                      => {\"rp\"},\n    \"image/vnd.tencent.tap\"                                                     => {\"tap\"},\n    \"image/vnd.valve.source.texture\"                                            => {\"vtf\"},\n    \"image/vnd.wap.wbmp\"                                                        => {\"wbmp\"},\n    \"image/vnd.xiff\"                                                            => {\"xif\"},\n    \"image/vnd.zbrush.pcx\"                                                      => {\"pcx\"},\n    \"image/webp\"                                                                => {\"webp\"},\n    \"image/wmf\"                                                                 => {\"wmf\"},\n    \"image/x-3ds\"                                                               => {\"3ds\"},\n    \"image/x-adobe-dng\"                                                         => {\"dng\"},\n    \"image/x-applix-graphics\"                                                   => {\"ag\"},\n    \"image/x-bmp\"                                                               => {\"bmp\", \"dib\"},\n    \"image/x-bzeps\"                                                             => {\"eps.bz2\", \"epsi.bz2\", \"epsf.bz2\"},\n    \"image/x-canon-cr2\"                                                         => {\"cr2\"},\n    \"image/x-canon-cr3\"                                                         => {\"cr3\"},\n    \"image/x-canon-crw\"                                                         => {\"crw\"},\n    \"image/x-cdr\"                                                               => {\"cdr\"},\n    \"image/x-cmu-raster\"                                                        => {\"ras\"},\n    \"image/x-cmx\"                                                               => {\"cmx\"},\n    \"image/x-compressed-xcf\"                                                    => {\"xcf.gz\", \"xcf.bz2\"},\n    \"image/x-dds\"                                                               => {\"dds\"},\n    \"image/x-djvu\"                                                              => {\"djvu\", \"djv\"},\n    \"image/x-emf\"                                                               => {\"emf\"},\n    \"image/x-eps\"                                                               => {\"eps\", \"epsi\", \"epsf\"},\n    \"image/x-exr\"                                                               => {\"exr\"},\n    \"image/x-fits\"                                                              => {\"fits\", \"fit\", \"fts\"},\n    \"image/x-fpx\"                                                               => {\"fpx\"},\n    \"image/x-freehand\"                                                          => {\"fh\", \"fhc\", \"fh4\", \"fh5\", \"fh7\"},\n    \"image/x-fuji-raf\"                                                          => {\"raf\"},\n    \"image/x-gimp-gbr\"                                                          => {\"gbr\"},\n    \"image/x-gimp-gih\"                                                          => {\"gih\"},\n    \"image/x-gimp-pat\"                                                          => {\"pat\"},\n    \"image/x-gzeps\"                                                             => {\"eps.gz\", \"epsi.gz\", \"epsf.gz\"},\n    \"image/x-icb\"                                                               => {\"tga\", \"icb\", \"tpic\", \"vda\", \"vst\"},\n    \"image/x-icns\"                                                              => {\"icns\"},\n    \"image/x-ico\"                                                               => {\"ico\"},\n    \"image/x-icon\"                                                              => {\"ico\"},\n    \"image/x-iff\"                                                               => {\"iff\", \"ilbm\", \"lbm\"},\n    \"image/x-ilbm\"                                                              => {\"iff\", \"ilbm\", \"lbm\"},\n    \"image/x-jng\"                                                               => {\"jng\"},\n    \"image/x-jp2-codestream\"                                                    => {\"j2c\", \"j2k\", \"jpc\"},\n    \"image/x-jpeg2000-image\"                                                    => {\"jp2\", \"jpg2\"},\n    \"image/x-kiss-cel\"                                                          => {\"cel\", \"kcf\"},\n    \"image/x-kodak-dcr\"                                                         => {\"dcr\"},\n    \"image/x-kodak-k25\"                                                         => {\"k25\"},\n    \"image/x-kodak-kdc\"                                                         => {\"kdc\"},\n    \"image/x-lwo\"                                                               => {\"lwo\", \"lwob\"},\n    \"image/x-lws\"                                                               => {\"lws\"},\n    \"image/x-macpaint\"                                                          => {\"pntg\"},\n    \"image/x-minolta-mrw\"                                                       => {\"mrw\"},\n    \"image/x-mrsid-image\"                                                       => {\"sid\"},\n    \"image/x-ms-bmp\"                                                            => {\"bmp\", \"dib\"},\n    \"image/x-msod\"                                                              => {\"msod\"},\n    \"image/x-nikon-nef\"                                                         => {\"nef\"},\n    \"image/x-nikon-nrw\"                                                         => {\"nrw\"},\n    \"image/x-olympus-orf\"                                                       => {\"orf\"},\n    \"image/x-panasonic-raw\"                                                     => {\"raw\"},\n    \"image/x-panasonic-raw2\"                                                    => {\"rw2\"},\n    \"image/x-panasonic-rw\"                                                      => {\"raw\"},\n    \"image/x-panasonic-rw2\"                                                     => {\"rw2\"},\n    \"image/x-pcx\"                                                               => {\"pcx\"},\n    \"image/x-pentax-pef\"                                                        => {\"pef\"},\n    \"image/x-pfm\"                                                               => {\"pfm\"},\n    \"image/x-phm\"                                                               => {\"phm\"},\n    \"image/x-photo-cd\"                                                          => {\"pcd\"},\n    \"image/x-photoshop\"                                                         => {\"psd\"},\n    \"image/x-pict\"                                                              => {\"pic\", \"pct\", \"pict\", \"pict1\", \"pict2\"},\n    \"image/x-portable-anymap\"                                                   => {\"pnm\"},\n    \"image/x-portable-bitmap\"                                                   => {\"pbm\"},\n    \"image/x-portable-graymap\"                                                  => {\"pgm\"},\n    \"image/x-portable-pixmap\"                                                   => {\"ppm\"},\n    \"image/x-psd\"                                                               => {\"psd\"},\n    \"image/x-pxr\"                                                               => {\"pxr\"},\n    \"image/x-quicktime\"                                                         => {\"qtif\", \"qif\"},\n    \"image/x-rgb\"                                                               => {\"rgb\"},\n    \"image/x-sct\"                                                               => {\"sct\"},\n    \"image/x-sgi\"                                                               => {\"sgi\"},\n    \"image/x-sigma-x3f\"                                                         => {\"x3f\"},\n    \"image/x-skencil\"                                                           => {\"sk\", \"sk1\"},\n    \"image/x-sony-arw\"                                                          => {\"arw\"},\n    \"image/x-sony-sr2\"                                                          => {\"sr2\"},\n    \"image/x-sony-srf\"                                                          => {\"srf\"},\n    \"image/x-sun-raster\"                                                        => {\"sun\"},\n    \"image/x-targa\"                                                             => {\"tga\", \"icb\", \"tpic\", \"vda\", \"vst\"},\n    \"image/x-tga\"                                                               => {\"tga\", \"icb\", \"tpic\", \"vda\", \"vst\"},\n    \"image/x-win-bitmap\"                                                        => {\"cur\"},\n    \"image/x-win-metafile\"                                                      => {\"wmf\"},\n    \"image/x-wmf\"                                                               => {\"wmf\"},\n    \"image/x-xbitmap\"                                                           => {\"xbm\"},\n    \"image/x-xcf\"                                                               => {\"xcf\"},\n    \"image/x-xfig\"                                                              => {\"fig\"},\n    \"image/x-xpixmap\"                                                           => {\"xpm\"},\n    \"image/x-xpm\"                                                               => {\"xpm\"},\n    \"image/x-xwindowdump\"                                                       => {\"xwd\"},\n    \"image/x.djvu\"                                                              => {\"djvu\", \"djv\"},\n    \"message/disposition-notification\"                                          => {\"disposition-notification\"},\n    \"message/global\"                                                            => {\"u8msg\"},\n    \"message/global-delivery-status\"                                            => {\"u8dsn\"},\n    \"message/global-disposition-notification\"                                   => {\"u8mdn\"},\n    \"message/global-headers\"                                                    => {\"u8hdr\"},\n    \"message/rfc822\"                                                            => {\"eml\", \"mime\"},\n    \"message/vnd.wfa.wsc\"                                                       => {\"wsc\"},\n    \"model/3mf\"                                                                 => {\"3mf\"},\n    \"model/gltf+json\"                                                           => {\"gltf\"},\n    \"model/gltf-binary\"                                                         => {\"glb\"},\n    \"model/iges\"                                                                => {\"igs\", \"iges\"},\n    \"model/jt\"                                                                  => {\"jt\"},\n    \"model/mesh\"                                                                => {\"msh\", \"mesh\", \"silo\"},\n    \"model/mtl\"                                                                 => {\"mtl\"},\n    \"model/obj\"                                                                 => {\"obj\"},\n    \"model/prc\"                                                                 => {\"prc\"},\n    \"model/step\"                                                                => {\"step\", \"stp\"},\n    \"model/step+xml\"                                                            => {\"stpx\"},\n    \"model/step+zip\"                                                            => {\"stpz\"},\n    \"model/step-xml+zip\"                                                        => {\"stpxz\"},\n    \"model/stl\"                                                                 => {\"stl\"},\n    \"model/u3d\"                                                                 => {\"u3d\"},\n    \"model/vnd.bary\"                                                            => {\"bary\"},\n    \"model/vnd.cld\"                                                             => {\"cld\"},\n    \"model/vnd.collada+xml\"                                                     => {\"dae\"},\n    \"model/vnd.dwf\"                                                             => {\"dwf\"},\n    \"model/vnd.gdl\"                                                             => {\"gdl\"},\n    \"model/vnd.gtw\"                                                             => {\"gtw\"},\n    \"model/vnd.mts\"                                                             => {\"mts\"},\n    \"model/vnd.opengex\"                                                         => {\"ogex\"},\n    \"model/vnd.parasolid.transmit.binary\"                                       => {\"x_b\"},\n    \"model/vnd.parasolid.transmit.text\"                                         => {\"x_t\"},\n    \"model/vnd.pytha.pyox\"                                                      => {\"pyo\", \"pyox\"},\n    \"model/vnd.sap.vds\"                                                         => {\"vds\"},\n    \"model/vnd.usda\"                                                            => {\"usda\"},\n    \"model/vnd.usdz+zip\"                                                        => {\"usdz\"},\n    \"model/vnd.valve.source.compiled-map\"                                       => {\"bsp\"},\n    \"model/vnd.vtu\"                                                             => {\"vtu\"},\n    \"model/vrml\"                                                                => {\"wrl\", \"vrml\", \"vrm\"},\n    \"model/x.stl-ascii\"                                                         => {\"stl\"},\n    \"model/x.stl-binary\"                                                        => {\"stl\"},\n    \"model/x3d+binary\"                                                          => {\"x3db\", \"x3dbz\"},\n    \"model/x3d+fastinfoset\"                                                     => {\"x3db\"},\n    \"model/x3d+vrml\"                                                            => {\"x3dv\", \"x3dvz\"},\n    \"model/x3d+xml\"                                                             => {\"x3d\", \"x3dz\"},\n    \"model/x3d-vrml\"                                                            => {\"x3dv\"},\n    \"text/cache-manifest\"                                                       => {\"appcache\", \"manifest\"},\n    \"text/calendar\"                                                             => {\"ics\", \"ifb\", \"vcs\", \"icalendar\"},\n    \"text/coffeescript\"                                                         => {\"coffee\", \"litcoffee\"},\n    \"text/crystal\"                                                              => {\"cr\"},\n    \"text/css\"                                                                  => {\"css\"},\n    \"text/csv\"                                                                  => {\"csv\"},\n    \"text/csv-schema\"                                                           => {\"csvs\"},\n    \"text/directory\"                                                            => {\"vcard\", \"vcf\", \"vct\", \"gcrd\"},\n    \"text/ecmascript\"                                                           => {\"es\"},\n    \"text/gedcom\"                                                               => {\"ged\", \"gedcom\"},\n    \"text/google-video-pointer\"                                                 => {\"gvp\"},\n    \"text/html\"                                                                 => {\"html\", \"htm\", \"shtml\"},\n    \"text/ico\"                                                                  => {\"ico\"},\n    \"text/jade\"                                                                 => {\"jade\"},\n    \"text/javascript\"                                                           => {\"js\", \"mjs\", \"cjs\", \"jsm\"},\n    \"text/jscript\"                                                              => {\"cjs\", \"js\", \"jsm\", \"mjs\"},\n    \"text/jscript.encode\"                                                       => {\"jse\"},\n    \"text/jsx\"                                                                  => {\"jsx\"},\n    \"text/julia\"                                                                => {\"jl\"},\n    \"text/less\"                                                                 => {\"less\"},\n    \"text/markdown\"                                                             => {\"md\", \"markdown\", \"mkd\"},\n    \"text/mathml\"                                                               => {\"mml\"},\n    \"text/mdx\"                                                                  => {\"mdx\"},\n    \"text/n3\"                                                                   => {\"n3\"},\n    \"text/org\"                                                                  => {\"org\"},\n    \"text/plain\"                                                                => {\"txt\", \"text\", \"conf\", \"def\", \"list\", \"log\", \"in\", \"ini\", \"asc\"},\n    \"text/prs.lines.tag\"                                                        => {\"dsc\"},\n    \"text/rdf\"                                                                  => {\"rdf\", \"rdfs\", \"owl\"},\n    \"text/richtext\"                                                             => {\"rtx\"},\n    \"text/rss\"                                                                  => {\"rss\"},\n    \"text/rtf\"                                                                  => {\"rtf\"},\n    \"text/rust\"                                                                 => {\"rs\"},\n    \"text/sgml\"                                                                 => {\"sgml\", \"sgm\"},\n    \"text/shex\"                                                                 => {\"shex\"},\n    \"text/slim\"                                                                 => {\"slim\", \"slm\"},\n    \"text/spdx\"                                                                 => {\"spdx\"},\n    \"text/spreadsheet\"                                                          => {\"sylk\", \"slk\"},\n    \"text/stylus\"                                                               => {\"stylus\", \"styl\"},\n    \"text/tab-separated-values\"                                                 => {\"tsv\"},\n    \"text/tcl\"                                                                  => {\"tcl\", \"tk\"},\n    \"text/troff\"                                                                => {\"t\", \"tr\", \"roff\", \"man\", \"me\", \"ms\"},\n    \"text/turtle\"                                                               => {\"ttl\"},\n    \"text/uri-list\"                                                             => {\"uri\", \"uris\", \"urls\"},\n    \"text/vbs\"                                                                  => {\"vbs\"},\n    \"text/vbscript\"                                                             => {\"vbs\"},\n    \"text/vbscript.encode\"                                                      => {\"vbe\"},\n    \"text/vcard\"                                                                => {\"vcard\", \"vcf\", \"vct\", \"gcrd\"},\n    \"text/vnd.curl\"                                                             => {\"curl\"},\n    \"text/vnd.curl.dcurl\"                                                       => {\"dcurl\"},\n    \"text/vnd.curl.mcurl\"                                                       => {\"mcurl\"},\n    \"text/vnd.curl.scurl\"                                                       => {\"scurl\"},\n    \"text/vnd.dvb.subtitle\"                                                     => {\"sub\"},\n    \"text/vnd.familysearch.gedcom\"                                              => {\"ged\", \"gedcom\"},\n    \"text/vnd.fly\"                                                              => {\"fly\"},\n    \"text/vnd.fmi.flexstor\"                                                     => {\"flx\"},\n    \"text/vnd.graphviz\"                                                         => {\"gv\", \"dot\"},\n    \"text/vnd.in3d.3dml\"                                                        => {\"3dml\"},\n    \"text/vnd.in3d.spot\"                                                        => {\"spot\"},\n    \"text/vnd.qt.linguist\"                                                      => {\"ts\"},\n    \"text/vnd.rn-realtext\"                                                      => {\"rt\"},\n    \"text/vnd.senx.warpscript\"                                                  => {\"mc2\"},\n    \"text/vnd.sun.j2me.app-descriptor\"                                          => {\"jad\"},\n    \"text/vnd.trolltech.linguist\"                                               => {\"ts\"},\n    \"text/vnd.typst\"                                                            => {\"typ\"},\n    \"text/vnd.wap.wml\"                                                          => {\"wml\"},\n    \"text/vnd.wap.wmlscript\"                                                    => {\"wmls\"},\n    \"text/vtt\"                                                                  => {\"vtt\"},\n    \"text/wgsl\"                                                                 => {\"wgsl\"},\n    \"text/x-adasrc\"                                                             => {\"adb\", \"ads\"},\n    \"text/x-asm\"                                                                => {\"s\", \"asm\"},\n    \"text/x-basic\"                                                              => {\"bas\"},\n    \"text/x-bibtex\"                                                             => {\"bib\"},\n    \"text/x-blueprint\"                                                          => {\"blp\"},\n    \"text/x-c\"                                                                  => {\"c\", \"cc\", \"cxx\", \"cpp\", \"h\", \"hh\", \"dic\"},\n    \"text/x-c++hdr\"                                                             => {\"hh\", \"hp\", \"hpp\", \"h++\", \"hxx\"},\n    \"text/x-c++src\"                                                             => {\"cpp\", \"cxx\", \"cc\", \"C\", \"c++\"},\n    \"text/x-chdr\"                                                               => {\"h\"},\n    \"text/x-cmake\"                                                              => {\"cmake\"},\n    \"text/x-cobol\"                                                              => {\"cbl\", \"cob\"},\n    \"text/x-comma-separated-values\"                                             => {\"csv\"},\n    \"text/x-common-lisp\"                                                        => {\"asd\", \"fasl\", \"lisp\", \"ros\"},\n    \"text/x-component\"                                                          => {\"htc\"},\n    \"text/x-crystal\"                                                            => {\"cr\"},\n    \"text/x-csharp\"                                                             => {\"cs\"},\n    \"text/x-csrc\"                                                               => {\"c\"},\n    \"text/x-csv\"                                                                => {\"csv\"},\n    \"text/x-cython\"                                                             => {\"pxd\", \"pxi\", \"pyx\"},\n    \"text/x-dart\"                                                               => {\"dart\"},\n    \"text/x-dbus-service\"                                                       => {\"service\"},\n    \"text/x-dcl\"                                                                => {\"dcl\"},\n    \"text/x-devicetree-binary\"                                                  => {\"dtb\"},\n    \"text/x-devicetree-source\"                                                  => {\"dts\", \"dtsi\"},\n    \"text/x-diff\"                                                               => {\"diff\", \"patch\"},\n    \"text/x-dockerfile\"                                                         => {\"Dockerfile\"},\n    \"text/x-dsl\"                                                                => {\"dsl\"},\n    \"text/x-dsrc\"                                                               => {\"d\", \"di\"},\n    \"text/x-dtd\"                                                                => {\"dtd\"},\n    \"text/x-eiffel\"                                                             => {\"e\", \"eif\"},\n    \"text/x-elixir\"                                                             => {\"ex\", \"exs\"},\n    \"text/x-emacs-lisp\"                                                         => {\"el\"},\n    \"text/x-erlang\"                                                             => {\"erl\"},\n    \"text/x-fish\"                                                               => {\"fish\"},\n    \"text/x-fortran\"                                                            => {\"f\", \"for\", \"f77\", \"f90\", \"f95\"},\n    \"text/x-gcode-gx\"                                                           => {\"gx\"},\n    \"text/x-genie\"                                                              => {\"gs\"},\n    \"text/x-gettext-translation\"                                                => {\"po\"},\n    \"text/x-gettext-translation-template\"                                       => {\"pot\"},\n    \"text/x-gherkin\"                                                            => {\"feature\"},\n    \"text/x-go\"                                                                 => {\"go\"},\n    \"text/x-google-video-pointer\"                                               => {\"gvp\"},\n    \"text/x-gradle\"                                                             => {\"gradle\"},\n    \"text/x-groovy\"                                                             => {\"groovy\", \"gvy\", \"gy\", \"gsh\"},\n    \"text/x-handlebars-template\"                                                => {\"hbs\"},\n    \"text/x-haskell\"                                                            => {\"hs\"},\n    \"text/x-idl\"                                                                => {\"idl\"},\n    \"text/x-imelody\"                                                            => {\"imy\", \"ime\"},\n    \"text/x-iptables\"                                                           => {\"iptables\"},\n    \"text/x-java\"                                                               => {\"java\"},\n    \"text/x-java-source\"                                                        => {\"java\"},\n    \"text/x-kaitai-struct\"                                                      => {\"ksy\"},\n    \"text/x-kotlin\"                                                             => {\"kt\"},\n    \"text/x-ldif\"                                                               => {\"ldif\"},\n    \"text/x-lilypond\"                                                           => {\"ly\"},\n    \"text/x-literate-haskell\"                                                   => {\"lhs\"},\n    \"text/x-log\"                                                                => {\"log\"},\n    \"text/x-lua\"                                                                => {\"lua\"},\n    \"text/x-lyx\"                                                                => {\"lyx\"},\n    \"text/x-makefile\"                                                           => {\"mk\", \"mak\"},\n    \"text/x-markdown\"                                                           => {\"md\", \"mkd\", \"markdown\"},\n    \"text/x-matlab\"                                                             => {\"m\"},\n    \"text/x-microdvd\"                                                           => {\"sub\"},\n    \"text/x-moc\"                                                                => {\"moc\"},\n    \"text/x-modelica\"                                                           => {\"mo\"},\n    \"text/x-mof\"                                                                => {\"mof\"},\n    \"text/x-mpl2\"                                                               => {\"mpl\"},\n    \"text/x-mpsub\"                                                              => {\"sub\"},\n    \"text/x-mrml\"                                                               => {\"mrml\", \"mrl\"},\n    \"text/x-ms-regedit\"                                                         => {\"reg\"},\n    \"text/x-mup\"                                                                => {\"mup\", \"not\"},\n    \"text/x-nfo\"                                                                => {\"nfo\"},\n    \"text/x-nim\"                                                                => {\"nim\"},\n    \"text/x-nimscript\"                                                          => {\"nims\", \"nimble\"},\n    \"text/x-nix\"                                                                => {\"nix\"},\n    \"text/x-nu\"                                                                 => {\"nu\"},\n    \"text/x-nushell\"                                                            => {\"nu\"},\n    \"text/x-objc++src\"                                                          => {\"mm\"},\n    \"text/x-objcsrc\"                                                            => {\"m\"},\n    \"text/x-ocaml\"                                                              => {\"ml\", \"mli\"},\n    \"text/x-ocl\"                                                                => {\"ocl\"},\n    \"text/x-octave\"                                                             => {\"m\"},\n    \"text/x-ooc\"                                                                => {\"ooc\"},\n    \"text/x-opencl-src\"                                                         => {\"cl\"},\n    \"text/x-opml\"                                                               => {\"opml\"},\n    \"text/x-opml+xml\"                                                           => {\"opml\"},\n    \"text/x-org\"                                                                => {\"org\"},\n    \"text/x-pascal\"                                                             => {\"p\", \"pas\"},\n    \"text/x-patch\"                                                              => {\"diff\", \"patch\"},\n    \"text/x-perl\"                                                               => {\"pl\", \"PL\", \"pm\", \"al\", \"perl\", \"pod\", \"t\"},\n    \"text/x-po\"                                                                 => {\"po\"},\n    \"text/x-pot\"                                                                => {\"pot\"},\n    \"text/x-processing\"                                                         => {\"pde\"},\n    \"text/x-python\"                                                             => {\"py\", \"wsgi\"},\n    \"text/x-python2\"                                                            => {\"py\", \"py2\"},\n    \"text/x-python3\"                                                            => {\"py\", \"py3\", \"pyi\"},\n    \"text/x-qml\"                                                                => {\"qml\", \"qmltypes\", \"qmlproject\"},\n    \"text/x-reject\"                                                             => {\"rej\"},\n    \"text/x-rpm-spec\"                                                           => {\"spec\"},\n    \"text/x-rst\"                                                                => {\"rst\"},\n    \"text/x-sagemath\"                                                           => {\"sage\"},\n    \"text/x-sass\"                                                               => {\"sass\"},\n    \"text/x-scala\"                                                              => {\"scala\", \"sc\"},\n    \"text/x-scheme\"                                                             => {\"scm\", \"ss\"},\n    \"text/x-scss\"                                                               => {\"scss\"},\n    \"text/x-setext\"                                                             => {\"etx\"},\n    \"text/x-sfv\"                                                                => {\"sfv\"},\n    \"text/x-sh\"                                                                 => {\"sh\"},\n    \"text/x-sql\"                                                                => {\"sql\"},\n    \"text/x-ssa\"                                                                => {\"ssa\", \"ass\"},\n    \"text/x-ssh-public-key\"                                                     => {\"pub\"},\n    \"text/x-subviewer\"                                                          => {\"sub\"},\n    \"text/x-suse-ymp\"                                                           => {\"ymp\"},\n    \"text/x-svhdr\"                                                              => {\"svh\"},\n    \"text/x-svsrc\"                                                              => {\"sv\"},\n    \"text/x-systemd-unit\"                                                       => {\"automount\", \"device\", \"mount\", \"path\", \"scope\", \"service\", \"slice\", \"socket\", \"swap\", \"target\", \"timer\"},\n    \"text/x-tcl\"                                                                => {\"tcl\", \"tk\"},\n    \"text/x-tex\"                                                                => {\"tex\", \"ltx\", \"sty\", \"cls\", \"dtx\", \"ins\", \"latex\"},\n    \"text/x-texinfo\"                                                            => {\"texi\", \"texinfo\"},\n    \"text/x-troff\"                                                              => {\"tr\", \"roff\", \"t\"},\n    \"text/x-troff-me\"                                                           => {\"me\"},\n    \"text/x-troff-mm\"                                                           => {\"mm\"},\n    \"text/x-troff-ms\"                                                           => {\"ms\"},\n    \"text/x-twig\"                                                               => {\"twig\"},\n    \"text/x-txt2tags\"                                                           => {\"t2t\"},\n    \"text/x-typst\"                                                              => {\"typ\"},\n    \"text/x-uil\"                                                                => {\"uil\"},\n    \"text/x-uuencode\"                                                           => {\"uu\", \"uue\"},\n    \"text/x-vala\"                                                               => {\"vala\", \"vapi\"},\n    \"text/x-vb\"                                                                 => {\"vb\"},\n    \"text/x-vcalendar\"                                                          => {\"vcs\", \"ics\", \"ifb\", \"icalendar\"},\n    \"text/x-vcard\"                                                              => {\"vcf\", \"vcard\", \"vct\", \"gcrd\"},\n    \"text/x-verilog\"                                                            => {\"v\"},\n    \"text/x-vhdl\"                                                               => {\"vhd\", \"vhdl\"},\n    \"text/x-xmi\"                                                                => {\"xmi\"},\n    \"text/x-xslfo\"                                                              => {\"fo\", \"xslfo\"},\n    \"text/x-yaml\"                                                               => {\"yaml\", \"yml\"},\n    \"text/x.gcode\"                                                              => {\"gcode\"},\n    \"text/xml\"                                                                  => {\"xml\", \"xbl\", \"xsd\", \"rng\"},\n    \"text/xml-external-parsed-entity\"                                           => {\"ent\"},\n    \"text/yaml\"                                                                 => {\"yaml\", \"yml\"},\n    \"video/3gp\"                                                                 => {\"3gp\", \"3gpp\", \"3ga\"},\n    \"video/3gpp\"                                                                => {\"3gp\", \"3gpp\", \"3ga\"},\n    \"video/3gpp-encrypted\"                                                      => {\"3gp\", \"3gpp\", \"3ga\"},\n    \"video/3gpp2\"                                                               => {\"3g2\", \"3gp2\", \"3gpp2\"},\n    \"video/annodex\"                                                             => {\"axv\"},\n    \"video/avi\"                                                                 => {\"avi\", \"avf\", \"divx\"},\n    \"video/divx\"                                                                => {\"avi\", \"avf\", \"divx\"},\n    \"video/dv\"                                                                  => {\"dv\"},\n    \"video/fli\"                                                                 => {\"fli\", \"flc\"},\n    \"video/flv\"                                                                 => {\"flv\"},\n    \"video/h261\"                                                                => {\"h261\"},\n    \"video/h263\"                                                                => {\"h263\"},\n    \"video/h264\"                                                                => {\"h264\"},\n    \"video/iso.segment\"                                                         => {\"m4s\"},\n    \"video/jpeg\"                                                                => {\"jpgv\"},\n    \"video/jpm\"                                                                 => {\"jpm\", \"jpgm\"},\n    \"video/mj2\"                                                                 => {\"mj2\", \"mjp2\"},\n    \"video/mp2t\"                                                                => {\"ts\", \"m2t\", \"m2ts\", \"mts\", \"cpi\", \"clpi\", \"mpl\", \"mpls\", \"bdm\", \"bdmv\"},\n    \"video/mp4\"                                                                 => {\"mp4\", \"mp4v\", \"mpg4\", \"m4v\", \"f4v\", \"lrv\", \"lrf\"},\n    \"video/mp4v-es\"                                                             => {\"mp4\", \"m4v\", \"f4v\", \"lrv\", \"lrf\"},\n    \"video/mpeg\"                                                                => {\"mpeg\", \"mpg\", \"mpe\", \"m1v\", \"m2v\", \"mp2\", \"vob\"},\n    \"video/mpeg-system\"                                                         => {\"mpeg\", \"mpg\", \"mp2\", \"mpe\", \"vob\"},\n    \"video/mpg4\"                                                                => {\"mpg4\"},\n    \"video/msvideo\"                                                             => {\"avi\", \"avf\", \"divx\"},\n    \"video/ogg\"                                                                 => {\"ogv\", \"ogg\"},\n    \"video/quicktime\"                                                           => {\"mov\", \"qt\", \"moov\", \"qtvr\"},\n    \"video/vivo\"                                                                => {\"viv\", \"vivo\"},\n    \"video/vnd.avi\"                                                             => {\"avi\", \"avf\", \"divx\"},\n    \"video/vnd.dece.hd\"                                                         => {\"uvh\", \"uvvh\"},\n    \"video/vnd.dece.mobile\"                                                     => {\"uvm\", \"uvvm\"},\n    \"video/vnd.dece.pd\"                                                         => {\"uvp\", \"uvvp\"},\n    \"video/vnd.dece.sd\"                                                         => {\"uvs\", \"uvvs\"},\n    \"video/vnd.dece.video\"                                                      => {\"uvv\", \"uvvv\"},\n    \"video/vnd.divx\"                                                            => {\"avi\", \"avf\", \"divx\"},\n    \"video/vnd.dvb.file\"                                                        => {\"dvb\"},\n    \"video/vnd.fvt\"                                                             => {\"fvt\"},\n    \"video/vnd.mpegurl\"                                                         => {\"mxu\", \"m4u\", \"m1u\"},\n    \"video/vnd.ms-playready.media.pyv\"                                          => {\"pyv\"},\n    \"video/vnd.radgamettools.bink\"                                              => {\"bik\", \"bk2\"},\n    \"video/vnd.radgamettools.smacker\"                                           => {\"smk\"},\n    \"video/vnd.rn-realvideo\"                                                    => {\"rv\", \"rvx\"},\n    \"video/vnd.uvvu.mp4\"                                                        => {\"uvu\", \"uvvu\"},\n    \"video/vnd.vivo\"                                                            => {\"viv\", \"vivo\"},\n    \"video/vnd.youtube.yt\"                                                      => {\"yt\"},\n    \"video/webm\"                                                                => {\"webm\"},\n    \"video/x-anim\"                                                              => {\"anim[1-9j]\", \"anim2\", \"anim3\", \"anim4\", \"anim5\", \"anim6\", \"anim7\", \"anim8\", \"anim9\", \"animj\"},\n    \"video/x-annodex\"                                                           => {\"axv\"},\n    \"video/x-avi\"                                                               => {\"avi\", \"avf\", \"divx\"},\n    \"video/x-f4v\"                                                               => {\"f4v\"},\n    \"video/x-fli\"                                                               => {\"fli\", \"flc\"},\n    \"video/x-flic\"                                                              => {\"fli\", \"flc\"},\n    \"video/x-flv\"                                                               => {\"flv\"},\n    \"video/x-javafx\"                                                            => {\"fxm\"},\n    \"video/x-m4v\"                                                               => {\"m4v\", \"mp4\", \"f4v\", \"lrv\", \"lrf\"},\n    \"video/x-matroska\"                                                          => {\"mkv\", \"mk3d\", \"mks\"},\n    \"video/x-matroska-3d\"                                                       => {\"mk3d\"},\n    \"video/x-mjpeg\"                                                             => {\"mjpeg\", \"mjpg\"},\n    \"video/x-mng\"                                                               => {\"mng\"},\n    \"video/x-mpeg\"                                                              => {\"mpeg\", \"mpg\", \"mp2\", \"mpe\", \"vob\"},\n    \"video/x-mpeg-system\"                                                       => {\"mpeg\", \"mpg\", \"mp2\", \"mpe\", \"vob\"},\n    \"video/x-mpeg2\"                                                             => {\"mpeg\", \"mpg\", \"mp2\", \"mpe\", \"vob\"},\n    \"video/x-mpegurl\"                                                           => {\"m1u\", \"m4u\", \"mxu\"},\n    \"video/x-ms-asf\"                                                            => {\"asf\", \"asx\"},\n    \"video/x-ms-asf-plugin\"                                                     => {\"asf\"},\n    \"video/x-ms-vob\"                                                            => {\"vob\"},\n    \"video/x-ms-wax\"                                                            => {\"asx\", \"wax\", \"wvx\", \"wmx\"},\n    \"video/x-ms-wm\"                                                             => {\"wm\", \"asf\"},\n    \"video/x-ms-wmv\"                                                            => {\"wmv\"},\n    \"video/x-ms-wmx\"                                                            => {\"wmx\", \"asx\", \"wax\", \"wvx\"},\n    \"video/x-ms-wvx\"                                                            => {\"wvx\", \"asx\", \"wax\", \"wmx\"},\n    \"video/x-msvideo\"                                                           => {\"avi\", \"avf\", \"divx\"},\n    \"video/x-nsv\"                                                               => {\"nsv\"},\n    \"video/x-ogg\"                                                               => {\"ogv\", \"ogg\"},\n    \"video/x-ogm\"                                                               => {\"ogm\"},\n    \"video/x-ogm+ogg\"                                                           => {\"ogm\"},\n    \"video/x-real-video\"                                                        => {\"rv\", \"rvx\"},\n    \"video/x-sgi-movie\"                                                         => {\"movie\"},\n    \"video/x-smv\"                                                               => {\"smv\"},\n    \"video/x-theora\"                                                            => {\"ogg\"},\n    \"video/x-theora+ogg\"                                                        => {\"ogg\"},\n    \"x-conference/x-cooltalk\"                                                   => {\"ice\"},\n    \"x-epoc/x-sisx-app\"                                                         => {\"sisx\"},\n    \"zz-application/zz-winassoc-123\"                                            => {\"123\", \"wk1\", \"wk3\", \"wk4\", \"wks\"},\n    \"zz-application/zz-winassoc-cab\"                                            => {\"cab\"},\n    \"zz-application/zz-winassoc-cdr\"                                            => {\"cdr\"},\n    \"zz-application/zz-winassoc-doc\"                                            => {\"doc\"},\n    \"zz-application/zz-winassoc-hlp\"                                            => {\"hlp\"},\n    \"zz-application/zz-winassoc-mdb\"                                            => {\"mdb\"},\n    \"zz-application/zz-winassoc-uu\"                                             => {\"uue\"},\n    \"zz-application/zz-winassoc-xls\"                                            => {\"xls\", \"xlc\", \"xll\", \"xlm\", \"xlw\", \"xla\", \"xlt\", \"xld\"},\n  }\n\n  private REVERSE_MAP = {\n    \"123\"                      => {\"application/lotus123\", \"application/vnd.lotus-1-2-3\", \"application/wk1\", \"application/x-123\", \"application/x-lotus123\", \"zz-application/zz-winassoc-123\"},\n    \"1km\"                      => {\"application/vnd.1000minds.decision-model+xml\"},\n    \"32x\"                      => {\"application/x-genesis-32x-rom\"},\n    \"3dml\"                     => {\"text/vnd.in3d.3dml\"},\n    \"3ds\"                      => {\"application/x-nintendo-3ds-rom\", \"image/x-3ds\"},\n    \"3dsx\"                     => {\"application/x-nintendo-3ds-executable\"},\n    \"3g2\"                      => {\"audio/3gpp2\", \"video/3gpp2\"},\n    \"3ga\"                      => {\"audio/3gpp\", \"audio/3gpp-encrypted\", \"audio/x-rn-3gpp-amr\", \"audio/x-rn-3gpp-amr-encrypted\", \"audio/x-rn-3gpp-amr-wb\", \"audio/x-rn-3gpp-amr-wb-encrypted\", \"video/3gp\", \"video/3gpp\", \"video/3gpp-encrypted\"},\n    \"3gp\"                      => {\"audio/3gpp\", \"audio/3gpp-encrypted\", \"audio/x-rn-3gpp-amr\", \"audio/x-rn-3gpp-amr-encrypted\", \"audio/x-rn-3gpp-amr-wb\", \"audio/x-rn-3gpp-amr-wb-encrypted\", \"video/3gp\", \"video/3gpp\", \"video/3gpp-encrypted\"},\n    \"3gp2\"                     => {\"audio/3gpp2\", \"video/3gpp2\"},\n    \"3gpp\"                     => {\"audio/3gpp\", \"audio/3gpp-encrypted\", \"audio/x-rn-3gpp-amr\", \"audio/x-rn-3gpp-amr-encrypted\", \"audio/x-rn-3gpp-amr-wb\", \"audio/x-rn-3gpp-amr-wb-encrypted\", \"video/3gp\", \"video/3gpp\", \"video/3gpp-encrypted\"},\n    \"3gpp2\"                    => {\"audio/3gpp2\", \"video/3gpp2\"},\n    \"3mf\"                      => {\"application/vnd.ms-3mfdocument\", \"model/3mf\"},\n    \"602\"                      => {\"application/x-t602\"},\n    \"669\"                      => {\"audio/x-mod\"},\n    \"7z\"                       => {\"application/x-7z-compressed\"},\n    \"7z.001\"                   => {\"application/x-7z-compressed\"},\n    \"C\"                        => {\"text/x-c++src\"},\n    \"Dockerfile\"               => {\"text/x-dockerfile\"},\n    \"PAR2\"                     => {\"application/x-par2\"},\n    \"PL\"                       => {\"application/x-perl\", \"text/x-perl\"},\n    \"Z\"                        => {\"application/x-compress\"},\n    \"[1-9]\"                    => {\"application/x-troff-man\"},\n    \"a\"                        => {\"application/x-archive\"},\n    \"a26\"                      => {\"application/x-atari-2600-rom\"},\n    \"a78\"                      => {\"application/x-atari-7800-rom\"},\n    \"aa\"                       => {\"audio/vnd.audible\", \"audio/x-pn-audibleaudio\"},\n    \"aab\"                      => {\"application/x-authorware-bin\"},\n    \"aac\"                      => {\"audio/aac\", \"audio/x-aac\", \"audio/x-hx-aac-adts\"},\n    \"aam\"                      => {\"application/x-authorware-map\"},\n    \"aas\"                      => {\"application/x-authorware-seg\"},\n    \"aax\"                      => {\"audio/vnd.audible\", \"audio/vnd.audible.aax\", \"audio/x-pn-audibleaudio\"},\n    \"aaxc\"                     => {\"audio/vnd.audible.aaxc\"},\n    \"abw\"                      => {\"application/x-abiword\"},\n    \"abw.CRASHED\"              => {\"application/x-abiword\"},\n    \"abw.gz\"                   => {\"application/x-abiword\"},\n    \"ac\"                       => {\"application/pkix-attr-cert\", \"application/vnd.nokia.n-gage.ac+xml\"},\n    \"ac3\"                      => {\"audio/ac3\"},\n    \"acc\"                      => {\"application/vnd.americandynamics.acc\"},\n    \"ace\"                      => {\"application/x-ace\", \"application/x-ace-compressed\"},\n    \"acu\"                      => {\"application/vnd.acucobol\"},\n    \"acutc\"                    => {\"application/vnd.acucorp\"},\n    \"adb\"                      => {\"text/x-adasrc\"},\n    \"adf\"                      => {\"application/x-amiga-disk-format\"},\n    \"adp\"                      => {\"audio/adpcm\"},\n    \"ads\"                      => {\"text/x-adasrc\"},\n    \"adts\"                     => {\"audio/aac\", \"audio/x-aac\", \"audio/x-hx-aac-adts\"},\n    \"aep\"                      => {\"application/vnd.audiograph\"},\n    \"afm\"                      => {\"application/x-font-afm\", \"application/x-font-type1\"},\n    \"afp\"                      => {\"application/vnd.ibm.modcap\"},\n    \"ag\"                       => {\"image/x-applix-graphics\"},\n    \"agb\"                      => {\"application/x-gba-rom\"},\n    \"age\"                      => {\"application/vnd.age\"},\n    \"ahead\"                    => {\"application/vnd.ahead.space\"},\n    \"ai\"                       => {\"application/illustrator\", \"application/postscript\", \"application/vnd.adobe.illustrator\"},\n    \"aif\"                      => {\"audio/x-aiff\"},\n    \"aifc\"                     => {\"audio/x-aifc\", \"audio/x-aiff\", \"audio/x-aiffc\"},\n    \"aiff\"                     => {\"audio/x-aiff\"},\n    \"aiffc\"                    => {\"audio/x-aifc\", \"audio/x-aiffc\"},\n    \"air\"                      => {\"application/vnd.adobe.air-application-installer-package+zip\"},\n    \"ait\"                      => {\"application/vnd.dvb.ait\"},\n    \"al\"                       => {\"application/x-perl\", \"text/x-perl\"},\n    \"alz\"                      => {\"application/x-alz\"},\n    \"ami\"                      => {\"application/vnd.amiga.ami\"},\n    \"aml\"                      => {\"application/automationml-aml+xml\"},\n    \"amlx\"                     => {\"application/automationml-amlx+zip\"},\n    \"amr\"                      => {\"audio/amr\", \"audio/amr-encrypted\"},\n    \"amz\"                      => {\"audio/x-amzxml\"},\n    \"ani\"                      => {\"application/x-navi-animation\"},\n    \"anim2\"                    => {\"video/x-anim\"},\n    \"anim3\"                    => {\"video/x-anim\"},\n    \"anim4\"                    => {\"video/x-anim\"},\n    \"anim5\"                    => {\"video/x-anim\"},\n    \"anim6\"                    => {\"video/x-anim\"},\n    \"anim7\"                    => {\"video/x-anim\"},\n    \"anim8\"                    => {\"video/x-anim\"},\n    \"anim9\"                    => {\"video/x-anim\"},\n    \"anim[1-9j]\"               => {\"video/x-anim\"},\n    \"animj\"                    => {\"video/x-anim\"},\n    \"anx\"                      => {\"application/annodex\", \"application/x-annodex\"},\n    \"ape\"                      => {\"audio/x-ape\"},\n    \"apk\"                      => {\"application/vnd.android.package-archive\"},\n    \"apng\"                     => {\"image/apng\", \"image/vnd.mozilla.apng\"},\n    \"appcache\"                 => {\"text/cache-manifest\"},\n    \"appimage\"                 => {\"application/vnd.appimage\", \"application/x-iso9660-appimage\"},\n    \"appinstaller\"             => {\"application/appinstaller\"},\n    \"application\"              => {\"application/x-ms-application\"},\n    \"appx\"                     => {\"application/appx\"},\n    \"appxbundle\"               => {\"application/appxbundle\"},\n    \"apr\"                      => {\"application/vnd.lotus-approach\"},\n    \"ar\"                       => {\"application/x-archive\"},\n    \"arc\"                      => {\"application/x-freearc\"},\n    \"arj\"                      => {\"application/x-arj\"},\n    \"arw\"                      => {\"image/x-sony-arw\"},\n    \"as\"                       => {\"application/x-applix-spreadsheet\"},\n    \"asar\"                     => {\"application/x-asar\"},\n    \"asc\"                      => {\"application/pgp\", \"application/pgp-encrypted\", \"application/pgp-keys\", \"application/pgp-signature\", \"text/plain\"},\n    \"asd\"                      => {\"text/x-common-lisp\"},\n    \"asf\"                      => {\"application/vnd.ms-asf\", \"video/x-ms-asf\", \"video/x-ms-asf-plugin\", \"video/x-ms-wm\"},\n    \"asice\"                    => {\"application/vnd.etsi.asic-e+zip\"},\n    \"asm\"                      => {\"text/x-asm\"},\n    \"aso\"                      => {\"application/vnd.accpac.simply.aso\"},\n    \"asp\"                      => {\"application/x-asp\"},\n    \"ass\"                      => {\"audio/aac\", \"audio/x-aac\", \"audio/x-hx-aac-adts\", \"text/x-ssa\"},\n    \"astc\"                     => {\"image/astc\"},\n    \"asx\"                      => {\"application/x-ms-asx\", \"audio/x-ms-asx\", \"video/x-ms-asf\", \"video/x-ms-wax\", \"video/x-ms-wmx\", \"video/x-ms-wvx\"},\n    \"atc\"                      => {\"application/vnd.acucorp\"},\n    \"atom\"                     => {\"application/atom+xml\"},\n    \"atomcat\"                  => {\"application/atomcat+xml\"},\n    \"atomdeleted\"              => {\"application/atomdeleted+xml\"},\n    \"atomsvc\"                  => {\"application/atomsvc+xml\"},\n    \"atx\"                      => {\"application/vnd.antix.game-component\"},\n    \"au\"                       => {\"audio/basic\"},\n    \"automount\"                => {\"text/x-systemd-unit\"},\n    \"avci\"                     => {\"image/avci\"},\n    \"avcs\"                     => {\"image/avcs\"},\n    \"avf\"                      => {\"video/avi\", \"video/divx\", \"video/msvideo\", \"video/vnd.avi\", \"video/vnd.divx\", \"video/x-avi\", \"video/x-msvideo\"},\n    \"avi\"                      => {\"video/avi\", \"video/divx\", \"video/msvideo\", \"video/vnd.avi\", \"video/vnd.divx\", \"video/x-avi\", \"video/x-msvideo\"},\n    \"avif\"                     => {\"image/avif\", \"image/avif-sequence\"},\n    \"avifs\"                    => {\"image/avif\", \"image/avif-sequence\"},\n    \"aw\"                       => {\"application/applixware\", \"application/x-applix-word\"},\n    \"awb\"                      => {\"audio/amr-wb\", \"audio/amr-wb-encrypted\"},\n    \"awk\"                      => {\"application/x-awk\"},\n    \"axa\"                      => {\"audio/annodex\", \"audio/x-annodex\"},\n    \"axv\"                      => {\"video/annodex\", \"video/x-annodex\"},\n    \"azf\"                      => {\"application/vnd.airzip.filesecure.azf\"},\n    \"azs\"                      => {\"application/vnd.airzip.filesecure.azs\"},\n    \"azv\"                      => {\"image/vnd.airzip.accelerator.azv\"},\n    \"azw\"                      => {\"application/vnd.amazon.ebook\"},\n    \"azw3\"                     => {\"application/vnd.amazon.mobi8-ebook\", \"application/x-mobi8-ebook\"},\n    \"b16\"                      => {\"image/vnd.pco.b16\"},\n    \"bak\"                      => {\"application/x-trash\"},\n    \"bary\"                     => {\"model/vnd.bary\"},\n    \"bas\"                      => {\"text/x-basic\"},\n    \"bat\"                      => {\"application/bat\", \"application/x-bat\", \"application/x-msdownload\"},\n    \"bcpio\"                    => {\"application/x-bcpio\"},\n    \"bdf\"                      => {\"application/x-font-bdf\"},\n    \"bdm\"                      => {\"application/vnd.syncml.dm+wbxml\", \"video/mp2t\"},\n    \"bdmv\"                     => {\"video/mp2t\"},\n    \"bdo\"                      => {\"application/vnd.nato.bindingdataobject+xml\"},\n    \"bdoc\"                     => {\"application/bdoc\", \"application/x-bdoc\"},\n    \"bed\"                      => {\"application/vnd.realvnc.bed\"},\n    \"bh2\"                      => {\"application/vnd.fujitsu.oasysprs\"},\n    \"bib\"                      => {\"text/x-bibtex\"},\n    \"bik\"                      => {\"video/vnd.radgamettools.bink\"},\n    \"bin\"                      => {\"application/octet-stream\"},\n    \"bk2\"                      => {\"video/vnd.radgamettools.bink\"},\n    \"blb\"                      => {\"application/x-blorb\"},\n    \"blend\"                    => {\"application/x-blender\"},\n    \"blender\"                  => {\"application/x-blender\"},\n    \"blorb\"                    => {\"application/x-blorb\"},\n    \"blp\"                      => {\"text/x-blueprint\"},\n    \"bmi\"                      => {\"application/vnd.bmi\"},\n    \"bmml\"                     => {\"application/vnd.balsamiq.bmml+xml\"},\n    \"bmp\"                      => {\"image/bmp\", \"image/x-bmp\", \"image/x-ms-bmp\"},\n    \"book\"                     => {\"application/vnd.framemaker\"},\n    \"box\"                      => {\"application/vnd.previewsystems.box\"},\n    \"boz\"                      => {\"application/x-bzip2\"},\n    \"bps\"                      => {\"application/x-bps-patch\"},\n    \"brk\"                      => {\"chemical/x-pdb\"},\n    \"bsdiff\"                   => {\"application/x-bsdiff\"},\n    \"bsp\"                      => {\"model/vnd.valve.source.compiled-map\"},\n    \"bst\"                      => {\"application/buildstream+yaml\"},\n    \"btf\"                      => {\"image/prs.btif\"},\n    \"btif\"                     => {\"image/prs.btif\"},\n    \"bz\"                       => {\"application/bzip2\", \"application/x-bzip\", \"application/x-bzip1\"},\n    \"bz2\"                      => {\"application/x-bz2\", \"application/bzip2\", \"application/x-bzip\", \"application/x-bzip2\"},\n    \"bz3\"                      => {\"application/x-bzip3\"},\n    \"c\"                        => {\"text/x-c\", \"text/x-csrc\"},\n    \"c++\"                      => {\"text/x-c++src\"},\n    \"c11amc\"                   => {\"application/vnd.cluetrust.cartomobile-config\"},\n    \"c11amz\"                   => {\"application/vnd.cluetrust.cartomobile-config-pkg\"},\n    \"c4d\"                      => {\"application/vnd.clonk.c4group\"},\n    \"c4f\"                      => {\"application/vnd.clonk.c4group\"},\n    \"c4g\"                      => {\"application/vnd.clonk.c4group\"},\n    \"c4p\"                      => {\"application/vnd.clonk.c4group\"},\n    \"c4u\"                      => {\"application/vnd.clonk.c4group\"},\n    \"cab\"                      => {\"application/vnd.ms-cab-compressed\", \"zz-application/zz-winassoc-cab\"},\n    \"caf\"                      => {\"audio/x-caf\"},\n    \"cap\"                      => {\"application/pcap\", \"application/vnd.tcpdump.pcap\", \"application/x-pcap\"},\n    \"car\"                      => {\"application/vnd.curl.car\"},\n    \"cat\"                      => {\"application/vnd.ms-pki.seccat\"},\n    \"cb7\"                      => {\"application/x-cb7\", \"application/x-cbr\"},\n    \"cba\"                      => {\"application/x-cbr\"},\n    \"cbl\"                      => {\"text/x-cobol\"},\n    \"cbor\"                     => {\"application/cbor\"},\n    \"cbr\"                      => {\"application/vnd.comicbook-rar\", \"application/x-cbr\"},\n    \"cbt\"                      => {\"application/x-cbr\", \"application/x-cbt\"},\n    \"cbz\"                      => {\"application/vnd.comicbook+zip\", \"application/x-cbr\", \"application/x-cbz\"},\n    \"cc\"                       => {\"text/x-c\", \"text/x-c++src\"},\n    \"cci\"                      => {\"application/x-nintendo-3ds-rom\"},\n    \"ccmx\"                     => {\"application/x-ccmx\"},\n    \"cco\"                      => {\"application/x-cocoa\"},\n    \"cct\"                      => {\"application/x-director\"},\n    \"ccxml\"                    => {\"application/ccxml+xml\"},\n    \"cdbcmsg\"                  => {\"application/vnd.contact.cmsg\"},\n    \"cdf\"                      => {\"application/x-netcdf\"},\n    \"cdfx\"                     => {\"application/cdfx+xml\"},\n    \"cdi\"                      => {\"application/x-discjuggler-cd-image\"},\n    \"cdkey\"                    => {\"application/vnd.mediastation.cdkey\"},\n    \"cdmia\"                    => {\"application/cdmi-capability\"},\n    \"cdmic\"                    => {\"application/cdmi-container\"},\n    \"cdmid\"                    => {\"application/cdmi-domain\"},\n    \"cdmio\"                    => {\"application/cdmi-object\"},\n    \"cdmiq\"                    => {\"application/cdmi-queue\"},\n    \"cdr\"                      => {\"application/cdr\", \"application/coreldraw\", \"application/vnd.corel-draw\", \"application/x-cdr\", \"application/x-coreldraw\", \"image/cdr\", \"image/x-cdr\", \"zz-application/zz-winassoc-cdr\"},\n    \"cdx\"                      => {\"chemical/x-cdx\"},\n    \"cdxml\"                    => {\"application/vnd.chemdraw+xml\"},\n    \"cdy\"                      => {\"application/vnd.cinderella\"},\n    \"cel\"                      => {\"image/x-kiss-cel\"},\n    \"cer\"                      => {\"application/pkix-cert\"},\n    \"cert\"                     => {\"application/x-x509-ca-cert\"},\n    \"cfs\"                      => {\"application/x-cfs-compressed\"},\n    \"cgb\"                      => {\"application/x-gameboy-color-rom\"},\n    \"cgm\"                      => {\"image/cgm\"},\n    \"chat\"                     => {\"application/x-chat\"},\n    \"chd\"                      => {\"application/x-mame-chd\"},\n    \"chm\"                      => {\"application/vnd.ms-htmlhelp\", \"application/x-chm\"},\n    \"chrt\"                     => {\"application/vnd.kde.kchart\", \"application/x-kchart\"},\n    \"cif\"                      => {\"chemical/x-cif\"},\n    \"cii\"                      => {\"application/vnd.anser-web-certificate-issue-initiation\"},\n    \"cil\"                      => {\"application/vnd.ms-artgalry\"},\n    \"cjs\"                      => {\"application/javascript\", \"application/node\", \"application/x-javascript\", \"text/javascript\", \"text/jscript\"},\n    \"cl\"                       => {\"text/x-opencl-src\"},\n    \"cla\"                      => {\"application/vnd.claymore\"},\n    \"class\"                    => {\"application/java\", \"application/java-byte-code\", \"application/java-vm\", \"application/x-java\", \"application/x-java-class\", \"application/x-java-vm\"},\n    \"cld\"                      => {\"model/vnd.cld\"},\n    \"clkk\"                     => {\"application/vnd.crick.clicker.keyboard\"},\n    \"clkp\"                     => {\"application/vnd.crick.clicker.palette\"},\n    \"clkt\"                     => {\"application/vnd.crick.clicker.template\"},\n    \"clkw\"                     => {\"application/vnd.crick.clicker.wordbank\"},\n    \"clkx\"                     => {\"application/vnd.crick.clicker\"},\n    \"clp\"                      => {\"application/x-msclip\"},\n    \"clpi\"                     => {\"video/mp2t\"},\n    \"cls\"                      => {\"application/x-tex\", \"text/x-tex\"},\n    \"cmake\"                    => {\"text/x-cmake\"},\n    \"cmc\"                      => {\"application/vnd.cosmocaller\"},\n    \"cmdf\"                     => {\"chemical/x-cmdf\"},\n    \"cml\"                      => {\"chemical/x-cml\"},\n    \"cmp\"                      => {\"application/vnd.yellowriver-custom-menu\"},\n    \"cmx\"                      => {\"image/x-cmx\"},\n    \"cob\"                      => {\"text/x-cobol\"},\n    \"cod\"                      => {\"application/vnd.rim.cod\"},\n    \"coffee\"                   => {\"application/vnd.coffeescript\", \"text/coffeescript\"},\n    \"com\"                      => {\"application/x-msdownload\"},\n    \"conf\"                     => {\"text/plain\"},\n    \"cpi\"                      => {\"video/mp2t\"},\n    \"cpio\"                     => {\"application/x-cpio\"},\n    \"cpio.gz\"                  => {\"application/x-cpio-compressed\"},\n    \"cpl\"                      => {\"application/cpl+xml\", \"application/vnd.microsoft.portable-executable\", \"application/x-ms-dos-executable\", \"application/x-ms-ne-executable\", \"application/x-msdownload\"},\n    \"cpp\"                      => {\"text/x-c\", \"text/x-c++src\"},\n    \"cpt\"                      => {\"application/mac-compactpro\"},\n    \"cr\"                       => {\"text/crystal\", \"text/x-crystal\"},\n    \"cr2\"                      => {\"image/x-canon-cr2\"},\n    \"cr3\"                      => {\"image/x-canon-cr3\"},\n    \"crd\"                      => {\"application/x-mscardfile\"},\n    \"crdownload\"               => {\"application/x-partial-download\"},\n    \"crl\"                      => {\"application/pkix-crl\"},\n    \"crt\"                      => {\"application/x-x509-ca-cert\"},\n    \"crw\"                      => {\"image/x-canon-crw\"},\n    \"crx\"                      => {\"application/x-chrome-extension\"},\n    \"cryptonote\"               => {\"application/vnd.rig.cryptonote\"},\n    \"cs\"                       => {\"text/x-csharp\"},\n    \"csh\"                      => {\"application/x-csh\"},\n    \"csl\"                      => {\"application/vnd.citationstyles.style+xml\"},\n    \"csml\"                     => {\"chemical/x-csml\"},\n    \"cso\"                      => {\"application/x-compressed-iso\"},\n    \"csp\"                      => {\"application/vnd.commonspace\"},\n    \"css\"                      => {\"text/css\"},\n    \"cst\"                      => {\"application/x-director\"},\n    \"csv\"                      => {\"text/csv\", \"application/csv\", \"text/x-comma-separated-values\", \"text/x-csv\"},\n    \"csvs\"                     => {\"text/csv-schema\"},\n    \"cts\"                      => {\"application/typescript\"},\n    \"cu\"                       => {\"application/cu-seeme\"},\n    \"cue\"                      => {\"application/x-cue\"},\n    \"cur\"                      => {\"image/x-win-bitmap\"},\n    \"curl\"                     => {\"text/vnd.curl\"},\n    \"cwk\"                      => {\"application/x-appleworks-document\"},\n    \"cwl\"                      => {\"application/cwl\"},\n    \"cww\"                      => {\"application/prs.cww\"},\n    \"cxt\"                      => {\"application/x-director\"},\n    \"cxx\"                      => {\"text/x-c\", \"text/x-c++src\"},\n    \"d\"                        => {\"text/x-dsrc\"},\n    \"dae\"                      => {\"model/vnd.collada+xml\"},\n    \"daf\"                      => {\"application/vnd.mobius.daf\"},\n    \"dar\"                      => {\"application/x-dar\"},\n    \"dart\"                     => {\"application/vnd.dart\", \"text/x-dart\"},\n    \"dataless\"                 => {\"application/vnd.fdsn.seed\"},\n    \"davmount\"                 => {\"application/davmount+xml\"},\n    \"dbf\"                      => {\"application/dbase\", \"application/dbf\", \"application/vnd.dbf\", \"application/x-dbase\", \"application/x-dbf\"},\n    \"dbk\"                      => {\"application/docbook+xml\", \"application/vnd.oasis.docbook+xml\", \"application/x-docbook+xml\"},\n    \"dc\"                       => {\"application/x-dc-rom\"},\n    \"dcl\"                      => {\"text/x-dcl\"},\n    \"dcm\"                      => {\"application/dicom\"},\n    \"dcr\"                      => {\"application/x-director\", \"image/x-kodak-dcr\"},\n    \"dcurl\"                    => {\"text/vnd.curl.dcurl\"},\n    \"dd2\"                      => {\"application/vnd.oma.dd2+xml\"},\n    \"ddd\"                      => {\"application/vnd.fujixerox.ddd\"},\n    \"ddf\"                      => {\"application/vnd.syncml.dmddf+xml\"},\n    \"dds\"                      => {\"image/vnd.ms-dds\", \"image/x-dds\"},\n    \"deb\"                      => {\"application/vnd.debian.binary-package\", \"application/x-deb\", \"application/x-debian-package\"},\n    \"def\"                      => {\"text/plain\"},\n    \"der\"                      => {\"application/x-x509-ca-cert\"},\n    \"desktop\"                  => {\"application/x-desktop\", \"application/x-gnome-app-info\"},\n    \"device\"                   => {\"text/x-systemd-unit\"},\n    \"dfac\"                     => {\"application/vnd.dreamfactory\"},\n    \"dff\"                      => {\"audio/dff\", \"audio/x-dff\"},\n    \"dgc\"                      => {\"application/x-dgc-compressed\"},\n    \"di\"                       => {\"text/x-dsrc\"},\n    \"dia\"                      => {\"application/x-dia-diagram\"},\n    \"dib\"                      => {\"image/bmp\", \"image/x-bmp\", \"image/x-ms-bmp\"},\n    \"dic\"                      => {\"text/x-c\"},\n    \"diff\"                     => {\"text/x-diff\", \"text/x-patch\"},\n    \"dir\"                      => {\"application/x-director\"},\n    \"dis\"                      => {\"application/vnd.mobius.dis\"},\n    \"disposition-notification\" => {\"message/disposition-notification\"},\n    \"divx\"                     => {\"video/avi\", \"video/divx\", \"video/msvideo\", \"video/vnd.avi\", \"video/vnd.divx\", \"video/x-avi\", \"video/x-msvideo\"},\n    \"djv\"                      => {\"image/vnd.djvu\", \"image/vnd.djvu+multipage\", \"image/x-djvu\", \"image/x.djvu\"},\n    \"djvu\"                     => {\"image/vnd.djvu\", \"image/vnd.djvu+multipage\", \"image/x-djvu\", \"image/x.djvu\"},\n    \"dll\"                      => {\"application/vnd.microsoft.portable-executable\", \"application/x-ms-dos-executable\", \"application/x-ms-ne-executable\", \"application/x-msdownload\"},\n    \"dmg\"                      => {\"application/x-apple-diskimage\"},\n    \"dmp\"                      => {\"application/pcap\", \"application/vnd.tcpdump.pcap\", \"application/x-pcap\"},\n    \"dna\"                      => {\"application/vnd.dna\"},\n    \"dng\"                      => {\"image/x-adobe-dng\"},\n    \"doc\"                      => {\"application/msword\", \"application/vnd.ms-word\", \"application/x-msword\", \"zz-application/zz-winassoc-doc\"},\n    \"docbook\"                  => {\"application/docbook+xml\", \"application/vnd.oasis.docbook+xml\", \"application/x-docbook+xml\"},\n    \"docm\"                     => {\"application/vnd.ms-word.document.macroenabled.12\"},\n    \"docx\"                     => {\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"},\n    \"dot\"                      => {\"application/msword\", \"application/msword-template\", \"text/vnd.graphviz\"},\n    \"dotm\"                     => {\"application/vnd.ms-word.template.macroenabled.12\"},\n    \"dotx\"                     => {\"application/vnd.openxmlformats-officedocument.wordprocessingml.template\"},\n    \"dp\"                       => {\"application/vnd.osgi.dp\"},\n    \"dpg\"                      => {\"application/vnd.dpgraph\"},\n    \"dpx\"                      => {\"image/dpx\"},\n    \"dra\"                      => {\"audio/vnd.dra\"},\n    \"drl\"                      => {\"application/x-excellon\"},\n    \"drle\"                     => {\"image/dicom-rle\"},\n    \"drv\"                      => {\"application/vnd.microsoft.portable-executable\", \"application/x-ms-dos-executable\", \"application/x-ms-ne-executable\", \"application/x-msdownload\"},\n    \"dsc\"                      => {\"text/prs.lines.tag\"},\n    \"dsf\"                      => {\"audio/dsd\", \"audio/dsf\", \"audio/x-dsd\", \"audio/x-dsf\"},\n    \"dsl\"                      => {\"text/x-dsl\"},\n    \"dssc\"                     => {\"application/dssc+der\"},\n    \"dtb\"                      => {\"application/x-dtbook+xml\", \"text/x-devicetree-binary\"},\n    \"dtd\"                      => {\"application/xml-dtd\", \"text/x-dtd\"},\n    \"dts\"                      => {\"audio/vnd.dts\", \"audio/x-dts\", \"text/x-devicetree-source\"},\n    \"dtshd\"                    => {\"audio/vnd.dts.hd\", \"audio/x-dtshd\"},\n    \"dtsi\"                     => {\"text/x-devicetree-source\"},\n    \"dtx\"                      => {\"application/x-tex\", \"text/x-tex\"},\n    \"dv\"                       => {\"video/dv\"},\n    \"dvb\"                      => {\"video/vnd.dvb.file\"},\n    \"dvi\"                      => {\"application/x-dvi\"},\n    \"dvi.bz2\"                  => {\"application/x-bzdvi\"},\n    \"dvi.gz\"                   => {\"application/x-gzdvi\"},\n    \"dwd\"                      => {\"application/atsc-dwd+xml\"},\n    \"dwf\"                      => {\"model/vnd.dwf\"},\n    \"dwg\"                      => {\"image/vnd.dwg\"},\n    \"dxf\"                      => {\"image/vnd.dxf\"},\n    \"dxp\"                      => {\"application/vnd.spotfire.dxp\"},\n    \"dxr\"                      => {\"application/x-director\"},\n    \"e\"                        => {\"text/x-eiffel\"},\n    \"ear\"                      => {\"application/java-archive\"},\n    \"ecelp4800\"                => {\"audio/vnd.nuera.ecelp4800\"},\n    \"ecelp7470\"                => {\"audio/vnd.nuera.ecelp7470\"},\n    \"ecelp9600\"                => {\"audio/vnd.nuera.ecelp9600\"},\n    \"ecma\"                     => {\"application/ecmascript\"},\n    \"edm\"                      => {\"application/vnd.novadigm.edm\"},\n    \"edx\"                      => {\"application/vnd.novadigm.edx\"},\n    \"efi\"                      => {\"application/vnd.microsoft.portable-executable\"},\n    \"efif\"                     => {\"application/vnd.picsel\"},\n    \"egon\"                     => {\"application/x-egon\"},\n    \"ei6\"                      => {\"application/vnd.pg.osasli\"},\n    \"eif\"                      => {\"text/x-eiffel\"},\n    \"el\"                       => {\"text/x-emacs-lisp\"},\n    \"emf\"                      => {\"application/emf\", \"application/x-emf\", \"application/x-msmetafile\", \"image/emf\", \"image/x-emf\"},\n    \"eml\"                      => {\"message/rfc822\"},\n    \"emma\"                     => {\"application/emma+xml\"},\n    \"emotionml\"                => {\"application/emotionml+xml\"},\n    \"emp\"                      => {\"application/vnd.emusic-emusic_package\"},\n    \"emz\"                      => {\"application/x-msmetafile\"},\n    \"ent\"                      => {\"application/xml-external-parsed-entity\", \"text/xml-external-parsed-entity\"},\n    \"eol\"                      => {\"audio/vnd.digital-winds\"},\n    \"eot\"                      => {\"application/vnd.ms-fontobject\"},\n    \"eps\"                      => {\"application/postscript\", \"image/x-eps\"},\n    \"eps.bz2\"                  => {\"image/x-bzeps\"},\n    \"eps.gz\"                   => {\"image/x-gzeps\"},\n    \"epsf\"                     => {\"image/x-eps\"},\n    \"epsf.bz2\"                 => {\"image/x-bzeps\"},\n    \"epsf.gz\"                  => {\"image/x-gzeps\"},\n    \"epsi\"                     => {\"image/x-eps\"},\n    \"epsi.bz2\"                 => {\"image/x-bzeps\"},\n    \"epsi.gz\"                  => {\"image/x-gzeps\"},\n    \"epub\"                     => {\"application/epub+zip\"},\n    \"eris\"                     => {\"application/x-eris-link+cbor\"},\n    \"erl\"                      => {\"text/x-erlang\"},\n    \"es\"                       => {\"application/ecmascript\", \"text/ecmascript\"},\n    \"es3\"                      => {\"application/vnd.eszigno3+xml\"},\n    \"esa\"                      => {\"application/vnd.osgi.subsystem\"},\n    \"escn\"                     => {\"application/x-godot-scene\"},\n    \"esf\"                      => {\"application/vnd.epson.esf\"},\n    \"et3\"                      => {\"application/vnd.eszigno3+xml\"},\n    \"etheme\"                   => {\"application/x-e-theme\"},\n    \"etx\"                      => {\"text/x-setext\"},\n    \"eva\"                      => {\"application/x-eva\"},\n    \"evy\"                      => {\"application/x-envoy\"},\n    \"ex\"                       => {\"text/x-elixir\"},\n    \"exe\"                      => {\"application/vnd.microsoft.portable-executable\", \"application/x-dosexec\", \"application/x-ms-dos-executable\", \"application/x-ms-ne-executable\", \"application/x-msdos-program\", \"application/x-msdownload\"},\n    \"exi\"                      => {\"application/exi\"},\n    \"exp\"                      => {\"application/express\"},\n    \"exr\"                      => {\"image/aces\", \"image/x-exr\"},\n    \"exs\"                      => {\"text/x-elixir\"},\n    \"ext\"                      => {\"application/vnd.novadigm.ext\"},\n    \"ez\"                       => {\"application/andrew-inset\"},\n    \"ez2\"                      => {\"application/vnd.ezpix-album\"},\n    \"ez3\"                      => {\"application/vnd.ezpix-package\"},\n    \"f\"                        => {\"text/x-fortran\"},\n    \"f4a\"                      => {\"audio/m4a\", \"audio/mp4\", \"audio/x-m4a\"},\n    \"f4b\"                      => {\"audio/x-m4b\"},\n    \"f4v\"                      => {\"video/mp4\", \"video/mp4v-es\", \"video/x-f4v\", \"video/x-m4v\"},\n    \"f77\"                      => {\"text/x-fortran\"},\n    \"f90\"                      => {\"text/x-fortran\"},\n    \"f95\"                      => {\"text/x-fortran\"},\n    \"fasl\"                     => {\"text/x-common-lisp\"},\n    \"fb2\"                      => {\"application/x-fictionbook\", \"application/x-fictionbook+xml\"},\n    \"fb2.zip\"                  => {\"application/x-zip-compressed-fb2\"},\n    \"fbs\"                      => {\"image/vnd.fastbidsheet\"},\n    \"fcdt\"                     => {\"application/vnd.adobe.formscentral.fcdt\"},\n    \"fcs\"                      => {\"application/vnd.isac.fcs\"},\n    \"fd\"                       => {\"application/x-fd-file\", \"application/x-raw-floppy-disk-image\"},\n    \"fdf\"                      => {\"application/fdf\", \"application/vnd.fdf\"},\n    \"fds\"                      => {\"application/x-fds-disk\"},\n    \"fdt\"                      => {\"application/fdt+xml\"},\n    \"fe_launch\"                => {\"application/vnd.denovo.fcselayout-link\"},\n    \"feature\"                  => {\"text/x-gherkin\"},\n    \"fg5\"                      => {\"application/vnd.fujitsu.oasysgp\"},\n    \"fgd\"                      => {\"application/x-director\"},\n    \"fh\"                       => {\"image/x-freehand\"},\n    \"fh4\"                      => {\"image/x-freehand\"},\n    \"fh5\"                      => {\"image/x-freehand\"},\n    \"fh7\"                      => {\"image/x-freehand\"},\n    \"fhc\"                      => {\"image/x-freehand\"},\n    \"fig\"                      => {\"application/x-xfig\", \"image/x-xfig\"},\n    \"fish\"                     => {\"application/x-fishscript\", \"text/x-fish\"},\n    \"fit\"                      => {\"application/fits\", \"image/fits\", \"image/x-fits\"},\n    \"fits\"                     => {\"application/fits\", \"image/fits\", \"image/x-fits\"},\n    \"fl\"                       => {\"application/x-fluid\"},\n    \"flac\"                     => {\"audio/flac\", \"audio/x-flac\"},\n    \"flatpak\"                  => {\"application/vnd.flatpak\", \"application/vnd.xdgapp\"},\n    \"flatpakref\"               => {\"application/vnd.flatpak.ref\"},\n    \"flatpakrepo\"              => {\"application/vnd.flatpak.repo\"},\n    \"flc\"                      => {\"video/fli\", \"video/x-fli\", \"video/x-flic\"},\n    \"fli\"                      => {\"video/fli\", \"video/x-fli\", \"video/x-flic\"},\n    \"flo\"                      => {\"application/vnd.micrografx.flo\"},\n    \"flv\"                      => {\"video/x-flv\", \"application/x-flash-video\", \"flv-application/octet-stream\", \"video/flv\"},\n    \"flw\"                      => {\"application/vnd.kde.kivio\", \"application/x-kivio\"},\n    \"flx\"                      => {\"text/vnd.fmi.flexstor\"},\n    \"fly\"                      => {\"text/vnd.fly\"},\n    \"fm\"                       => {\"application/vnd.framemaker\", \"application/x-frame\"},\n    \"fnc\"                      => {\"application/vnd.frogans.fnc\"},\n    \"fo\"                       => {\"application/vnd.software602.filler.form+xml\", \"text/x-xslfo\"},\n    \"fodg\"                     => {\"application/vnd.oasis.opendocument.graphics-flat-xml\"},\n    \"fodp\"                     => {\"application/vnd.oasis.opendocument.presentation-flat-xml\"},\n    \"fods\"                     => {\"application/vnd.oasis.opendocument.spreadsheet-flat-xml\"},\n    \"fodt\"                     => {\"application/vnd.oasis.opendocument.text-flat-xml\"},\n    \"for\"                      => {\"text/x-fortran\"},\n    \"fpx\"                      => {\"image/vnd.fpx\", \"image/x-fpx\"},\n    \"frame\"                    => {\"application/vnd.framemaker\"},\n    \"fsc\"                      => {\"application/vnd.fsc.weblaunch\"},\n    \"fst\"                      => {\"image/vnd.fst\"},\n    \"ftc\"                      => {\"application/vnd.fluxtime.clip\"},\n    \"fti\"                      => {\"application/vnd.anser-web-funds-transfer-initiation\"},\n    \"fts\"                      => {\"application/fits\", \"image/fits\", \"image/x-fits\"},\n    \"fvt\"                      => {\"video/vnd.fvt\"},\n    \"fxm\"                      => {\"video/x-javafx\"},\n    \"fxp\"                      => {\"application/vnd.adobe.fxp\"},\n    \"fxpl\"                     => {\"application/vnd.adobe.fxp\"},\n    \"fzs\"                      => {\"application/vnd.fuzzysheet\"},\n    \"g2w\"                      => {\"application/vnd.geoplan\"},\n    \"g3\"                       => {\"image/fax-g3\", \"image/g3fax\"},\n    \"g3w\"                      => {\"application/vnd.geospace\"},\n    \"gac\"                      => {\"application/vnd.groove-account\"},\n    \"gam\"                      => {\"application/x-tads\"},\n    \"gb\"                       => {\"application/x-gameboy-rom\"},\n    \"gba\"                      => {\"application/x-gba-rom\"},\n    \"gbc\"                      => {\"application/x-gameboy-color-rom\"},\n    \"gbr\"                      => {\"application/rpki-ghostbusters\", \"application/vnd.gerber\", \"application/x-gerber\", \"image/x-gimp-gbr\"},\n    \"gbrjob\"                   => {\"application/x-gerber-job\"},\n    \"gca\"                      => {\"application/x-gca-compressed\"},\n    \"gcode\"                    => {\"text/x.gcode\"},\n    \"gcrd\"                     => {\"text/directory\", \"text/vcard\", \"text/x-vcard\"},\n    \"gd\"                       => {\"application/x-gdscript\"},\n    \"gdi\"                      => {\"application/x-gd-rom-cue\"},\n    \"gdl\"                      => {\"model/vnd.gdl\"},\n    \"gdoc\"                     => {\"application/vnd.google-apps.document\"},\n    \"gdshader\"                 => {\"application/x-godot-shader\"},\n    \"ged\"                      => {\"application/x-gedcom\", \"text/gedcom\", \"text/vnd.familysearch.gedcom\"},\n    \"gedcom\"                   => {\"application/x-gedcom\", \"text/gedcom\", \"text/vnd.familysearch.gedcom\"},\n    \"gem\"                      => {\"application/x-gtar\", \"application/x-tar\"},\n    \"gen\"                      => {\"application/x-genesis-rom\"},\n    \"geo\"                      => {\"application/vnd.dynageo\"},\n    \"geo.json\"                 => {\"application/geo+json\", \"application/vnd.geo+json\"},\n    \"geojson\"                  => {\"application/geo+json\", \"application/vnd.geo+json\"},\n    \"gex\"                      => {\"application/vnd.geometry-explorer\"},\n    \"gf\"                       => {\"application/x-tex-gf\"},\n    \"gg\"                       => {\"application/x-gamegear-rom\"},\n    \"ggb\"                      => {\"application/vnd.geogebra.file\"},\n    \"ggs\"                      => {\"application/vnd.geogebra.slides\"},\n    \"ggt\"                      => {\"application/vnd.geogebra.tool\"},\n    \"ghf\"                      => {\"application/vnd.groove-help\"},\n    \"gif\"                      => {\"image/gif\"},\n    \"gih\"                      => {\"image/x-gimp-gih\"},\n    \"gim\"                      => {\"application/vnd.groove-identity-message\"},\n    \"glade\"                    => {\"application/x-glade\"},\n    \"glb\"                      => {\"model/gltf-binary\"},\n    \"gltf\"                     => {\"model/gltf+json\"},\n    \"gml\"                      => {\"application/gml+xml\"},\n    \"gmo\"                      => {\"application/x-gettext-translation\"},\n    \"gmx\"                      => {\"application/vnd.gmx\"},\n    \"gnc\"                      => {\"application/x-gnucash\"},\n    \"gnd\"                      => {\"application/gnunet-directory\"},\n    \"gnucash\"                  => {\"application/x-gnucash\"},\n    \"gnumeric\"                 => {\"application/x-gnumeric\"},\n    \"gnuplot\"                  => {\"application/x-gnuplot\"},\n    \"go\"                       => {\"text/x-go\"},\n    \"gp\"                       => {\"application/x-gnuplot\"},\n    \"gpg\"                      => {\"application/pgp\", \"application/pgp-encrypted\", \"application/pgp-keys\", \"application/pgp-signature\"},\n    \"gph\"                      => {\"application/vnd.flographit\"},\n    \"gplt\"                     => {\"application/x-gnuplot\"},\n    \"gpx\"                      => {\"application/gpx\", \"application/gpx+xml\", \"application/x-gpx\", \"application/x-gpx+xml\"},\n    \"gqf\"                      => {\"application/vnd.grafeq\"},\n    \"gqs\"                      => {\"application/vnd.grafeq\"},\n    \"gra\"                      => {\"application/x-graphite\"},\n    \"gradle\"                   => {\"text/x-gradle\"},\n    \"gram\"                     => {\"application/srgs\"},\n    \"gramps\"                   => {\"application/x-gramps-xml\"},\n    \"gre\"                      => {\"application/vnd.geometry-explorer\"},\n    \"groovy\"                   => {\"text/x-groovy\"},\n    \"grv\"                      => {\"application/vnd.groove-injector\"},\n    \"grxml\"                    => {\"application/srgs+xml\"},\n    \"gs\"                       => {\"text/x-genie\"},\n    \"gsf\"                      => {\"application/x-font-ghostscript\", \"application/x-font-type1\"},\n    \"gsh\"                      => {\"text/x-groovy\"},\n    \"gsheet\"                   => {\"application/vnd.google-apps.spreadsheet\"},\n    \"gslides\"                  => {\"application/vnd.google-apps.presentation\"},\n    \"gsm\"                      => {\"audio/x-gsm\"},\n    \"gtar\"                     => {\"application/x-gtar\", \"application/x-tar\"},\n    \"gtm\"                      => {\"application/vnd.groove-tool-message\"},\n    \"gtw\"                      => {\"model/vnd.gtw\"},\n    \"gv\"                       => {\"text/vnd.graphviz\"},\n    \"gvp\"                      => {\"text/google-video-pointer\", \"text/x-google-video-pointer\"},\n    \"gvy\"                      => {\"text/x-groovy\"},\n    \"gx\"                       => {\"text/x-gcode-gx\"},\n    \"gxf\"                      => {\"application/gxf\"},\n    \"gxt\"                      => {\"application/vnd.geonext\"},\n    \"gy\"                       => {\"text/x-groovy\"},\n    \"gz\"                       => {\"application/x-gzip\", \"application/gzip\"},\n    \"h\"                        => {\"text/x-c\", \"text/x-chdr\"},\n    \"h++\"                      => {\"text/x-c++hdr\"},\n    \"h261\"                     => {\"video/h261\"},\n    \"h263\"                     => {\"video/h263\"},\n    \"h264\"                     => {\"video/h264\"},\n    \"h4\"                       => {\"application/x-hdf\"},\n    \"h5\"                       => {\"application/x-hdf\"},\n    \"hal\"                      => {\"application/vnd.hal+xml\"},\n    \"hbci\"                     => {\"application/vnd.hbci\"},\n    \"hbs\"                      => {\"text/x-handlebars-template\"},\n    \"hdd\"                      => {\"application/x-virtualbox-hdd\"},\n    \"hdf\"                      => {\"application/x-hdf\"},\n    \"hdf4\"                     => {\"application/x-hdf\"},\n    \"hdf5\"                     => {\"application/x-hdf\"},\n    \"hdp\"                      => {\"image/jxr\", \"image/vnd.ms-photo\"},\n    \"hdr\"                      => {\"image/vnd.radiance\"},\n    \"heic\"                     => {\"image/heic\", \"image/heic-sequence\", \"image/heif\", \"image/heif-sequence\"},\n    \"heics\"                    => {\"image/heic-sequence\"},\n    \"heif\"                     => {\"image/heic\", \"image/heic-sequence\", \"image/heif\", \"image/heif-sequence\"},\n    \"heifs\"                    => {\"image/heif-sequence\"},\n    \"hej2\"                     => {\"image/hej2k\"},\n    \"held\"                     => {\"application/atsc-held+xml\"},\n    \"hfe\"                      => {\"application/x-hfe-file\", \"application/x-hfe-floppy-image\"},\n    \"hh\"                       => {\"text/x-c\", \"text/x-c++hdr\"},\n    \"hif\"                      => {\"image/heic\", \"image/heic-sequence\", \"image/heif\", \"image/heif-sequence\"},\n    \"hjson\"                    => {\"application/hjson\"},\n    \"hlp\"                      => {\"application/winhlp\", \"zz-application/zz-winassoc-hlp\"},\n    \"hp\"                       => {\"text/x-c++hdr\"},\n    \"hpgl\"                     => {\"application/vnd.hp-hpgl\"},\n    \"hpid\"                     => {\"application/vnd.hp-hpid\"},\n    \"hpp\"                      => {\"text/x-c++hdr\"},\n    \"hps\"                      => {\"application/vnd.hp-hps\"},\n    \"hqx\"                      => {\"application/stuffit\", \"application/mac-binhex40\"},\n    \"hs\"                       => {\"text/x-haskell\"},\n    \"hsj2\"                     => {\"image/hsj2\"},\n    \"hta\"                      => {\"application/hta\"},\n    \"htc\"                      => {\"text/x-component\"},\n    \"htke\"                     => {\"application/vnd.kenameaapp\"},\n    \"htm\"                      => {\"text/html\", \"application/xhtml+xml\"},\n    \"html\"                     => {\"text/html\", \"application/xhtml+xml\"},\n    \"hvd\"                      => {\"application/vnd.yamaha.hv-dic\"},\n    \"hvp\"                      => {\"application/vnd.yamaha.hv-voice\"},\n    \"hvs\"                      => {\"application/vnd.yamaha.hv-script\"},\n    \"hwp\"                      => {\"application/vnd.haansoft-hwp\", \"application/x-hwp\"},\n    \"hwt\"                      => {\"application/vnd.haansoft-hwt\", \"application/x-hwt\"},\n    \"hxx\"                      => {\"text/x-c++hdr\"},\n    \"i2g\"                      => {\"application/vnd.intergeo\"},\n    \"ica\"                      => {\"application/x-ica\"},\n    \"icalendar\"                => {\"application/ics\", \"text/calendar\", \"text/x-vcalendar\"},\n    \"icb\"                      => {\"application/tga\", \"application/x-targa\", \"application/x-tga\", \"image/targa\", \"image/tga\", \"image/x-icb\", \"image/x-targa\", \"image/x-tga\"},\n    \"icc\"                      => {\"application/vnd.iccprofile\"},\n    \"ice\"                      => {\"x-conference/x-cooltalk\"},\n    \"icm\"                      => {\"application/vnd.iccprofile\"},\n    \"icns\"                     => {\"image/x-icns\"},\n    \"ico\"                      => {\"application/ico\", \"image/ico\", \"image/icon\", \"image/vnd.microsoft.icon\", \"image/x-ico\", \"image/x-icon\", \"text/ico\"},\n    \"ics\"                      => {\"application/ics\", \"text/calendar\", \"text/x-vcalendar\"},\n    \"idl\"                      => {\"text/x-idl\"},\n    \"ief\"                      => {\"image/ief\"},\n    \"ifb\"                      => {\"application/ics\", \"text/calendar\", \"text/x-vcalendar\"},\n    \"iff\"                      => {\"image/x-iff\", \"image/x-ilbm\"},\n    \"ifm\"                      => {\"application/vnd.shana.informed.formdata\"},\n    \"iges\"                     => {\"model/iges\"},\n    \"igl\"                      => {\"application/vnd.igloader\"},\n    \"igm\"                      => {\"application/vnd.insors.igm\"},\n    \"igs\"                      => {\"model/iges\"},\n    \"igx\"                      => {\"application/vnd.micrografx.igx\"},\n    \"iif\"                      => {\"application/vnd.shana.informed.interchange\"},\n    \"ilbm\"                     => {\"image/x-iff\", \"image/x-ilbm\"},\n    \"ime\"                      => {\"audio/imelody\", \"audio/x-imelody\", \"text/x-imelody\"},\n    \"img\"                      => {\"application/vnd.efi.img\", \"application/x-raw-disk-image\"},\n    \"img.xz\"                   => {\"application/x-raw-disk-image-xz-compressed\"},\n    \"imp\"                      => {\"application/vnd.accpac.simply.imp\"},\n    \"ims\"                      => {\"application/vnd.ms-ims\"},\n    \"imy\"                      => {\"audio/imelody\", \"audio/x-imelody\", \"text/x-imelody\"},\n    \"in\"                       => {\"text/plain\"},\n    \"ini\"                      => {\"text/plain\"},\n    \"ink\"                      => {\"application/inkml+xml\"},\n    \"inkml\"                    => {\"application/inkml+xml\"},\n    \"ins\"                      => {\"application/x-tex\", \"text/x-tex\"},\n    \"install\"                  => {\"application/x-install-instructions\"},\n    \"iota\"                     => {\"application/vnd.astraea-software.iota\"},\n    \"ipfix\"                    => {\"application/ipfix\"},\n    \"ipk\"                      => {\"application/vnd.shana.informed.package\"},\n    \"ips\"                      => {\"application/x-ips-patch\"},\n    \"iptables\"                 => {\"text/x-iptables\"},\n    \"ipynb\"                    => {\"application/x-ipynb+json\"},\n    \"irm\"                      => {\"application/vnd.ibm.rights-management\"},\n    \"irp\"                      => {\"application/vnd.irepository.package+xml\"},\n    \"iso\"                      => {\"application/vnd.efi.iso\", \"application/x-cd-image\", \"application/x-dreamcast-rom\", \"application/x-gamecube-iso-image\", \"application/x-gamecube-rom\", \"application/x-iso9660-image\", \"application/x-saturn-rom\", \"application/x-sega-cd-rom\", \"application/x-sega-pico-rom\", \"application/x-wbfs\", \"application/x-wia\", \"application/x-wii-iso-image\", \"application/x-wii-rom\"},\n    \"iso9660\"                  => {\"application/vnd.efi.iso\", \"application/x-cd-image\", \"application/x-iso9660-image\"},\n    \"it\"                       => {\"audio/x-it\"},\n    \"it87\"                     => {\"application/x-it87\"},\n    \"itp\"                      => {\"application/vnd.shana.informed.formtemplate\"},\n    \"its\"                      => {\"application/its+xml\"},\n    \"ivp\"                      => {\"application/vnd.immervision-ivp\"},\n    \"ivu\"                      => {\"application/vnd.immervision-ivu\"},\n    \"j2c\"                      => {\"image/x-jp2-codestream\"},\n    \"j2k\"                      => {\"image/x-jp2-codestream\"},\n    \"jad\"                      => {\"text/vnd.sun.j2me.app-descriptor\"},\n    \"jade\"                     => {\"text/jade\"},\n    \"jam\"                      => {\"application/vnd.jam\"},\n    \"jar\"                      => {\"application/x-java-archive\", \"application/java-archive\", \"application/x-jar\"},\n    \"jardiff\"                  => {\"application/x-java-archive-diff\"},\n    \"java\"                     => {\"text/x-java\", \"text/x-java-source\"},\n    \"jceks\"                    => {\"application/x-java-jce-keystore\"},\n    \"jfif\"                     => {\"image/jpeg\", \"image/pjpeg\"},\n    \"jhc\"                      => {\"image/jphc\"},\n    \"jisp\"                     => {\"application/vnd.jisp\"},\n    \"jks\"                      => {\"application/x-java-keystore\"},\n    \"jl\"                       => {\"text/julia\"},\n    \"jls\"                      => {\"image/jls\"},\n    \"jlt\"                      => {\"application/vnd.hp-jlyt\"},\n    \"jng\"                      => {\"image/x-jng\"},\n    \"jnlp\"                     => {\"application/x-java-jnlp-file\"},\n    \"joda\"                     => {\"application/vnd.joost.joda-archive\"},\n    \"jp2\"                      => {\"image/jp2\", \"image/jpeg2000\", \"image/jpeg2000-image\", \"image/x-jpeg2000-image\"},\n    \"jpc\"                      => {\"image/x-jp2-codestream\"},\n    \"jpe\"                      => {\"image/jpeg\", \"image/pjpeg\"},\n    \"jpeg\"                     => {\"image/jpeg\", \"image/pjpeg\"},\n    \"jpf\"                      => {\"image/jpx\"},\n    \"jpg\"                      => {\"image/jpeg\", \"image/pjpeg\"},\n    \"jpg2\"                     => {\"image/jp2\", \"image/jpeg2000\", \"image/jpeg2000-image\", \"image/x-jpeg2000-image\"},\n    \"jpgm\"                     => {\"image/jpm\", \"video/jpm\"},\n    \"jpgv\"                     => {\"video/jpeg\"},\n    \"jph\"                      => {\"image/jph\"},\n    \"jpm\"                      => {\"image/jpm\", \"video/jpm\"},\n    \"jpr\"                      => {\"application/x-jbuilder-project\"},\n    \"jpx\"                      => {\"application/x-jbuilder-project\", \"image/jpx\"},\n    \"jrd\"                      => {\"application/jrd+json\"},\n    \"js\"                       => {\"text/javascript\", \"application/javascript\", \"application/x-javascript\", \"text/jscript\"},\n    \"jse\"                      => {\"text/jscript.encode\"},\n    \"jsm\"                      => {\"application/javascript\", \"application/x-javascript\", \"text/javascript\", \"text/jscript\"},\n    \"json\"                     => {\"application/json\", \"application/schema+json\"},\n    \"json-patch\"               => {\"application/json-patch+json\"},\n    \"json5\"                    => {\"application/json5\"},\n    \"jsonld\"                   => {\"application/ld+json\"},\n    \"jsonml\"                   => {\"application/jsonml+json\"},\n    \"jsx\"                      => {\"text/jsx\"},\n    \"jt\"                       => {\"model/jt\"},\n    \"jxl\"                      => {\"image/jxl\"},\n    \"jxr\"                      => {\"image/jxr\", \"image/vnd.ms-photo\"},\n    \"jxra\"                     => {\"image/jxra\"},\n    \"jxrs\"                     => {\"image/jxrs\"},\n    \"jxs\"                      => {\"image/jxs\"},\n    \"jxsc\"                     => {\"image/jxsc\"},\n    \"jxsi\"                     => {\"image/jxsi\"},\n    \"jxss\"                     => {\"image/jxss\"},\n    \"k25\"                      => {\"image/x-kodak-k25\"},\n    \"k7\"                       => {\"application/x-thomson-cassette\"},\n    \"kar\"                      => {\"audio/midi\", \"audio/x-midi\"},\n    \"karbon\"                   => {\"application/vnd.kde.karbon\", \"application/x-karbon\"},\n    \"kcf\"                      => {\"image/x-kiss-cel\"},\n    \"kdbx\"                     => {\"application/x-keepass2\"},\n    \"kdc\"                      => {\"image/x-kodak-kdc\"},\n    \"kdelnk\"                   => {\"application/x-desktop\", \"application/x-gnome-app-info\"},\n    \"kexi\"                     => {\"application/x-kexiproject-sqlite\", \"application/x-kexiproject-sqlite2\", \"application/x-kexiproject-sqlite3\", \"application/x-vnd.kde.kexi\"},\n    \"kexic\"                    => {\"application/x-kexi-connectiondata\"},\n    \"kexis\"                    => {\"application/x-kexiproject-shortcut\"},\n    \"key\"                      => {\"application/vnd.apple.keynote\", \"application/pgp-keys\", \"application/x-iwork-keynote-sffkey\"},\n    \"keynote\"                  => {\"application/vnd.apple.keynote\"},\n    \"kfo\"                      => {\"application/vnd.kde.kformula\", \"application/x-kformula\"},\n    \"kfx\"                      => {\"application/vnd.amazon.mobi8-ebook\", \"application/x-mobi8-ebook\"},\n    \"kia\"                      => {\"application/vnd.kidspiration\"},\n    \"kil\"                      => {\"application/x-killustrator\"},\n    \"kino\"                     => {\"application/smil\", \"application/smil+xml\"},\n    \"kml\"                      => {\"application/vnd.google-earth.kml+xml\"},\n    \"kmz\"                      => {\"application/vnd.google-earth.kmz\"},\n    \"kne\"                      => {\"application/vnd.kinar\"},\n    \"knp\"                      => {\"application/vnd.kinar\"},\n    \"kon\"                      => {\"application/vnd.kde.kontour\", \"application/x-kontour\"},\n    \"kpm\"                      => {\"application/x-kpovmodeler\"},\n    \"kpr\"                      => {\"application/vnd.kde.kpresenter\", \"application/x-kpresenter\"},\n    \"kpt\"                      => {\"application/vnd.kde.kpresenter\", \"application/x-kpresenter\"},\n    \"kpxx\"                     => {\"application/vnd.ds-keypoint\"},\n    \"kra\"                      => {\"application/x-krita\"},\n    \"krz\"                      => {\"application/x-krita\"},\n    \"ks\"                       => {\"application/x-java-keystore\"},\n    \"ksp\"                      => {\"application/vnd.kde.kspread\", \"application/x-kspread\"},\n    \"ksy\"                      => {\"text/x-kaitai-struct\"},\n    \"kt\"                       => {\"text/x-kotlin\"},\n    \"ktr\"                      => {\"application/vnd.kahootz\"},\n    \"ktx\"                      => {\"image/ktx\"},\n    \"ktx2\"                     => {\"image/ktx2\"},\n    \"ktz\"                      => {\"application/vnd.kahootz\"},\n    \"kud\"                      => {\"application/x-kugar\"},\n    \"kwd\"                      => {\"application/vnd.kde.kword\", \"application/x-kword\"},\n    \"kwt\"                      => {\"application/vnd.kde.kword\", \"application/x-kword\"},\n    \"la\"                       => {\"application/x-shared-library-la\"},\n    \"lasxml\"                   => {\"application/vnd.las.las+xml\"},\n    \"latex\"                    => {\"application/x-latex\", \"application/x-tex\", \"text/x-tex\"},\n    \"lbd\"                      => {\"application/vnd.llamagraphics.life-balance.desktop\"},\n    \"lbe\"                      => {\"application/vnd.llamagraphics.life-balance.exchange+xml\"},\n    \"lbm\"                      => {\"image/x-iff\", \"image/x-ilbm\"},\n    \"ldif\"                     => {\"text/x-ldif\"},\n    \"les\"                      => {\"application/vnd.hhe.lesson-player\"},\n    \"less\"                     => {\"text/less\"},\n    \"lgr\"                      => {\"application/lgr+xml\"},\n    \"lha\"                      => {\"application/x-lha\", \"application/x-lzh-compressed\"},\n    \"lhs\"                      => {\"text/x-literate-haskell\"},\n    \"lhz\"                      => {\"application/x-lhz\"},\n    \"lib\"                      => {\"application/vnd.microsoft.portable-executable\", \"application/x-archive\"},\n    \"link66\"                   => {\"application/vnd.route66.link66+xml\"},\n    \"lisp\"                     => {\"text/x-common-lisp\"},\n    \"list\"                     => {\"text/plain\"},\n    \"list3820\"                 => {\"application/vnd.ibm.modcap\"},\n    \"listafp\"                  => {\"application/vnd.ibm.modcap\"},\n    \"litcoffee\"                => {\"text/coffeescript\"},\n    \"lmdb\"                     => {\"application/x-lmdb\"},\n    \"lnk\"                      => {\"application/x-ms-shortcut\", \"application/x-win-lnk\"},\n    \"lnx\"                      => {\"application/x-atari-lynx-rom\"},\n    \"loas\"                     => {\"audio/usac\"},\n    \"log\"                      => {\"text/plain\", \"text/x-log\"},\n    \"lostxml\"                  => {\"application/lost+xml\"},\n    \"lrf\"                      => {\"application/x-sony-bbeb\", \"video/mp4\", \"video/mp4v-es\", \"video/x-m4v\"},\n    \"lrm\"                      => {\"application/vnd.ms-lrm\"},\n    \"lrv\"                      => {\"video/mp4\", \"video/mp4v-es\", \"video/x-m4v\"},\n    \"lrz\"                      => {\"application/x-lrzip\"},\n    \"ltf\"                      => {\"application/vnd.frogans.ltf\"},\n    \"ltx\"                      => {\"application/x-tex\", \"text/x-tex\"},\n    \"lua\"                      => {\"text/x-lua\"},\n    \"luac\"                     => {\"application/x-lua-bytecode\"},\n    \"lvp\"                      => {\"audio/vnd.lucent.voice\"},\n    \"lwo\"                      => {\"image/x-lwo\"},\n    \"lwob\"                     => {\"image/x-lwo\"},\n    \"lwp\"                      => {\"application/vnd.lotus-wordpro\"},\n    \"lws\"                      => {\"image/x-lws\"},\n    \"ly\"                       => {\"text/x-lilypond\"},\n    \"lyx\"                      => {\"application/x-lyx\", \"text/x-lyx\"},\n    \"lz\"                       => {\"application/x-lzip\"},\n    \"lz4\"                      => {\"application/x-lz4\"},\n    \"lzh\"                      => {\"application/x-lha\", \"application/x-lzh-compressed\"},\n    \"lzma\"                     => {\"application/x-lzma\"},\n    \"lzo\"                      => {\"application/x-lzop\"},\n    \"m\"                        => {\"text/x-matlab\", \"text/x-objcsrc\", \"text/x-octave\"},\n    \"m13\"                      => {\"application/x-msmediaview\"},\n    \"m14\"                      => {\"application/x-msmediaview\"},\n    \"m15\"                      => {\"audio/x-mod\"},\n    \"m1u\"                      => {\"video/vnd.mpegurl\", \"video/x-mpegurl\"},\n    \"m1v\"                      => {\"video/mpeg\"},\n    \"m21\"                      => {\"application/mp21\"},\n    \"m2a\"                      => {\"audio/mpeg\"},\n    \"m2t\"                      => {\"video/mp2t\"},\n    \"m2ts\"                     => {\"video/mp2t\"},\n    \"m2v\"                      => {\"video/mpeg\"},\n    \"m3a\"                      => {\"audio/mpeg\"},\n    \"m3u\"                      => {\"audio/x-mpegurl\", \"application/m3u\", \"application/vnd.apple.mpegurl\", \"audio/m3u\", \"audio/mpegurl\", \"audio/x-m3u\", \"audio/x-mp3-playlist\"},\n    \"m3u8\"                     => {\"application/m3u\", \"application/vnd.apple.mpegurl\", \"audio/m3u\", \"audio/mpegurl\", \"audio/x-m3u\", \"audio/x-mp3-playlist\", \"audio/x-mpegurl\"},\n    \"m4\"                       => {\"application/x-m4\"},\n    \"m4a\"                      => {\"audio/mp4\", \"audio/m4a\", \"audio/x-m4a\"},\n    \"m4b\"                      => {\"audio/x-m4b\"},\n    \"m4p\"                      => {\"application/mp4\"},\n    \"m4r\"                      => {\"audio/x-m4r\"},\n    \"m4s\"                      => {\"video/iso.segment\"},\n    \"m4u\"                      => {\"video/vnd.mpegurl\", \"video/x-mpegurl\"},\n    \"m4v\"                      => {\"video/mp4\", \"video/mp4v-es\", \"video/x-m4v\"},\n    \"m7\"                       => {\"application/x-thomson-cartridge-memo7\"},\n    \"ma\"                       => {\"application/mathematica\"},\n    \"mab\"                      => {\"application/x-markaby\"},\n    \"mads\"                     => {\"application/mads+xml\"},\n    \"maei\"                     => {\"application/mmt-aei+xml\"},\n    \"mag\"                      => {\"application/vnd.ecowin.chart\"},\n    \"mak\"                      => {\"text/x-makefile\"},\n    \"maker\"                    => {\"application/vnd.framemaker\"},\n    \"man\"                      => {\"application/x-troff-man\", \"text/troff\"},\n    \"manifest\"                 => {\"text/cache-manifest\"},\n    \"map\"                      => {\"application/json\"},\n    \"markdown\"                 => {\"text/markdown\", \"text/x-markdown\"},\n    \"mathml\"                   => {\"application/mathml+xml\"},\n    \"mb\"                       => {\"application/mathematica\"},\n    \"mbk\"                      => {\"application/vnd.mobius.mbk\"},\n    \"mbox\"                     => {\"application/mbox\"},\n    \"mc1\"                      => {\"application/vnd.medcalcdata\"},\n    \"mc2\"                      => {\"text/vnd.senx.warpscript\"},\n    \"mcd\"                      => {\"application/vnd.mcd\"},\n    \"mcurl\"                    => {\"text/vnd.curl.mcurl\"},\n    \"md\"                       => {\"text/markdown\", \"text/x-markdown\", \"application/x-genesis-rom\"},\n    \"mdb\"                      => {\"application/x-msaccess\", \"application/mdb\", \"application/msaccess\", \"application/vnd.ms-access\", \"application/vnd.msaccess\", \"application/x-lmdb\", \"application/x-mdb\", \"zz-application/zz-winassoc-mdb\"},\n    \"mdi\"                      => {\"image/vnd.ms-modi\"},\n    \"mdx\"                      => {\"application/x-genesis-32x-rom\", \"text/mdx\"},\n    \"me\"                       => {\"text/troff\", \"text/x-troff-me\"},\n    \"med\"                      => {\"audio/x-mod\"},\n    \"mesh\"                     => {\"model/mesh\"},\n    \"meta4\"                    => {\"application/metalink4+xml\"},\n    \"metalink\"                 => {\"application/metalink+xml\"},\n    \"mets\"                     => {\"application/mets+xml\"},\n    \"mfm\"                      => {\"application/vnd.mfmp\"},\n    \"mft\"                      => {\"application/rpki-manifest\"},\n    \"mgp\"                      => {\"application/vnd.osgeo.mapguide.package\", \"application/x-magicpoint\"},\n    \"mgz\"                      => {\"application/vnd.proteus.magazine\"},\n    \"mht\"                      => {\"application/x-mimearchive\"},\n    \"mhtml\"                    => {\"application/x-mimearchive\"},\n    \"mid\"                      => {\"audio/midi\", \"audio/x-midi\"},\n    \"midi\"                     => {\"audio/midi\", \"audio/x-midi\"},\n    \"mie\"                      => {\"application/x-mie\"},\n    \"mif\"                      => {\"application/vnd.mif\", \"application/x-mif\"},\n    \"mime\"                     => {\"message/rfc822\"},\n    \"minipsf\"                  => {\"audio/x-minipsf\"},\n    \"mj2\"                      => {\"video/mj2\"},\n    \"mjp2\"                     => {\"video/mj2\"},\n    \"mjpeg\"                    => {\"video/x-mjpeg\"},\n    \"mjpg\"                     => {\"video/x-mjpeg\"},\n    \"mjs\"                      => {\"application/javascript\", \"application/x-javascript\", \"text/javascript\", \"text/jscript\"},\n    \"mk\"                       => {\"text/x-makefile\"},\n    \"mk3d\"                     => {\"video/x-matroska\", \"video/x-matroska-3d\"},\n    \"mka\"                      => {\"audio/x-matroska\"},\n    \"mkd\"                      => {\"text/markdown\", \"text/x-markdown\"},\n    \"mks\"                      => {\"video/x-matroska\"},\n    \"mkv\"                      => {\"video/x-matroska\"},\n    \"ml\"                       => {\"text/x-ocaml\"},\n    \"mli\"                      => {\"text/x-ocaml\"},\n    \"mlp\"                      => {\"application/vnd.dolby.mlp\"},\n    \"mm\"                       => {\"text/x-objc++src\", \"text/x-troff-mm\"},\n    \"mmd\"                      => {\"application/vnd.chipnuts.karaoke-mmd\"},\n    \"mmf\"                      => {\"application/vnd.smaf\", \"application/x-smaf\"},\n    \"mml\"                      => {\"application/mathml+xml\", \"text/mathml\"},\n    \"mmr\"                      => {\"image/vnd.fujixerox.edmics-mmr\"},\n    \"mng\"                      => {\"video/x-mng\"},\n    \"mny\"                      => {\"application/x-msmoney\"},\n    \"mo\"                       => {\"application/x-gettext-translation\", \"text/x-modelica\"},\n    \"mo3\"                      => {\"audio/x-mo3\"},\n    \"mobi\"                     => {\"application/x-mobipocket-ebook\"},\n    \"moc\"                      => {\"text/x-moc\"},\n    \"mod\"                      => {\"application/x-object\", \"audio/x-mod\"},\n    \"mods\"                     => {\"application/mods+xml\"},\n    \"mof\"                      => {\"text/x-mof\"},\n    \"moov\"                     => {\"video/quicktime\"},\n    \"mount\"                    => {\"text/x-systemd-unit\"},\n    \"mov\"                      => {\"video/quicktime\"},\n    \"movie\"                    => {\"video/x-sgi-movie\"},\n    \"mp+\"                      => {\"audio/x-musepack\"},\n    \"mp2\"                      => {\"audio/mp2\", \"audio/mpeg\", \"audio/x-mp2\", \"video/mpeg\", \"video/mpeg-system\", \"video/x-mpeg\", \"video/x-mpeg-system\", \"video/x-mpeg2\"},\n    \"mp21\"                     => {\"application/mp21\"},\n    \"mp2a\"                     => {\"audio/mpeg\"},\n    \"mp3\"                      => {\"audio/mpeg\", \"audio/mp3\", \"audio/x-mp3\", \"audio/x-mpeg\", \"audio/x-mpg\"},\n    \"mp4\"                      => {\"video/mp4\", \"application/mp4\", \"video/mp4v-es\", \"video/x-m4v\"},\n    \"mp4a\"                     => {\"audio/mp4\"},\n    \"mp4s\"                     => {\"application/mp4\"},\n    \"mp4v\"                     => {\"video/mp4\"},\n    \"mpc\"                      => {\"application/vnd.mophun.certificate\", \"audio/x-musepack\"},\n    \"mpd\"                      => {\"application/dash+xml\"},\n    \"mpe\"                      => {\"video/mpeg\", \"video/mpeg-system\", \"video/x-mpeg\", \"video/x-mpeg-system\", \"video/x-mpeg2\"},\n    \"mpeg\"                     => {\"video/mpeg\", \"video/mpeg-system\", \"video/x-mpeg\", \"video/x-mpeg-system\", \"video/x-mpeg2\"},\n    \"mpf\"                      => {\"application/media-policy-dataset+xml\"},\n    \"mpg\"                      => {\"video/mpeg\", \"video/mpeg-system\", \"video/x-mpeg\", \"video/x-mpeg-system\", \"video/x-mpeg2\"},\n    \"mpg4\"                     => {\"video/mpg4\", \"application/mp4\", \"video/mp4\"},\n    \"mpga\"                     => {\"audio/mp3\", \"audio/mpeg\", \"audio/x-mp3\", \"audio/x-mpeg\", \"audio/x-mpg\"},\n    \"mpkg\"                     => {\"application/vnd.apple.installer+xml\"},\n    \"mpl\"                      => {\"text/x-mpl2\", \"video/mp2t\"},\n    \"mpls\"                     => {\"video/mp2t\"},\n    \"mpm\"                      => {\"application/vnd.blueice.multipass\"},\n    \"mpn\"                      => {\"application/vnd.mophun.application\"},\n    \"mpp\"                      => {\"application/dash-patch+xml\", \"application/vnd.ms-project\", \"audio/x-musepack\"},\n    \"mpt\"                      => {\"application/vnd.ms-project\"},\n    \"mpy\"                      => {\"application/vnd.ibm.minipay\"},\n    \"mqy\"                      => {\"application/vnd.mobius.mqy\"},\n    \"mrc\"                      => {\"application/marc\"},\n    \"mrcx\"                     => {\"application/marcxml+xml\"},\n    \"mrl\"                      => {\"text/x-mrml\"},\n    \"mrml\"                     => {\"text/x-mrml\"},\n    \"mrpack\"                   => {\"application/x-modrinth-modpack+zip\"},\n    \"mrw\"                      => {\"image/x-minolta-mrw\"},\n    \"ms\"                       => {\"text/troff\", \"text/x-troff-ms\"},\n    \"mscml\"                    => {\"application/mediaservercontrol+xml\"},\n    \"mseed\"                    => {\"application/vnd.fdsn.mseed\"},\n    \"mseq\"                     => {\"application/vnd.mseq\"},\n    \"msf\"                      => {\"application/vnd.epson.msf\"},\n    \"msg\"                      => {\"application/vnd.ms-outlook\"},\n    \"msh\"                      => {\"model/mesh\"},\n    \"msi\"                      => {\"application/x-msdownload\", \"application/x-msi\"},\n    \"msix\"                     => {\"application/msix\"},\n    \"msixbundle\"               => {\"application/msixbundle\"},\n    \"msl\"                      => {\"application/vnd.mobius.msl\"},\n    \"msod\"                     => {\"image/x-msod\"},\n    \"msp\"                      => {\"application/microsoftpatch\"},\n    \"msty\"                     => {\"application/vnd.muvee.style\"},\n    \"msu\"                      => {\"application/microsoftupdate\"},\n    \"msx\"                      => {\"application/x-msx-rom\"},\n    \"mtl\"                      => {\"model/mtl\"},\n    \"mtm\"                      => {\"audio/x-mod\"},\n    \"mts\"                      => {\"application/typescript\", \"model/vnd.mts\", \"video/mp2t\"},\n    \"mup\"                      => {\"text/x-mup\"},\n    \"mus\"                      => {\"application/vnd.musician\"},\n    \"musd\"                     => {\"application/mmt-usd+xml\"},\n    \"musicxml\"                 => {\"application/vnd.recordare.musicxml+xml\"},\n    \"mvb\"                      => {\"application/x-msmediaview\"},\n    \"mvt\"                      => {\"application/vnd.mapbox-vector-tile\"},\n    \"mwf\"                      => {\"application/vnd.mfer\"},\n    \"mxf\"                      => {\"application/mxf\"},\n    \"mxl\"                      => {\"application/vnd.recordare.musicxml\"},\n    \"mxmf\"                     => {\"audio/mobile-xmf\", \"audio/vnd.nokia.mobile-xmf\"},\n    \"mxml\"                     => {\"application/xv+xml\"},\n    \"mxs\"                      => {\"application/vnd.triscape.mxs\"},\n    \"mxu\"                      => {\"video/vnd.mpegurl\", \"video/x-mpegurl\"},\n    \"n-gage\"                   => {\"application/vnd.nokia.n-gage.symbian.install\"},\n    \"n3\"                       => {\"text/n3\"},\n    \"n64\"                      => {\"application/x-n64-rom\"},\n    \"nb\"                       => {\"application/mathematica\", \"application/x-mathematica\"},\n    \"nbp\"                      => {\"application/vnd.wolfram.player\"},\n    \"nc\"                       => {\"application/x-netcdf\"},\n    \"ncx\"                      => {\"application/x-dtbncx+xml\"},\n    \"nds\"                      => {\"application/x-nintendo-ds-rom\"},\n    \"nef\"                      => {\"image/x-nikon-nef\"},\n    \"nes\"                      => {\"application/x-nes-rom\"},\n    \"nez\"                      => {\"application/x-nes-rom\"},\n    \"nfo\"                      => {\"text/x-nfo\"},\n    \"ngc\"                      => {\"application/x-neo-geo-pocket-color-rom\"},\n    \"ngdat\"                    => {\"application/vnd.nokia.n-gage.data\"},\n    \"ngp\"                      => {\"application/x-neo-geo-pocket-rom\"},\n    \"nim\"                      => {\"text/x-nim\"},\n    \"nimble\"                   => {\"text/x-nimscript\"},\n    \"nims\"                     => {\"text/x-nimscript\"},\n    \"nitf\"                     => {\"application/vnd.nitf\"},\n    \"nix\"                      => {\"text/x-nix\"},\n    \"nlu\"                      => {\"application/vnd.neurolanguage.nlu\"},\n    \"nml\"                      => {\"application/vnd.enliven\"},\n    \"nnd\"                      => {\"application/vnd.noblenet-directory\"},\n    \"nns\"                      => {\"application/vnd.noblenet-sealer\"},\n    \"nnw\"                      => {\"application/vnd.noblenet-web\"},\n    \"not\"                      => {\"text/x-mup\"},\n    \"npx\"                      => {\"image/vnd.net-fpx\"},\n    \"nq\"                       => {\"application/n-quads\"},\n    \"nrw\"                      => {\"image/x-nikon-nrw\"},\n    \"nsc\"                      => {\"application/x-conference\", \"application/x-netshow-channel\"},\n    \"nsf\"                      => {\"application/vnd.lotus-notes\"},\n    \"nsv\"                      => {\"video/x-nsv\"},\n    \"nt\"                       => {\"application/n-triples\"},\n    \"ntar\"                     => {\"application/x-pcapng\"},\n    \"ntf\"                      => {\"application/vnd.nitf\"},\n    \"nu\"                       => {\"application/x-nuscript\", \"text/x-nu\", \"text/x-nushell\"},\n    \"numbers\"                  => {\"application/vnd.apple.numbers\", \"application/x-iwork-numbers-sffnumbers\"},\n    \"nzb\"                      => {\"application/x-nzb\"},\n    \"o\"                        => {\"application/x-object\"},\n    \"oa2\"                      => {\"application/vnd.fujitsu.oasys2\"},\n    \"oa3\"                      => {\"application/vnd.fujitsu.oasys3\"},\n    \"oas\"                      => {\"application/vnd.fujitsu.oasys\"},\n    \"obd\"                      => {\"application/x-msbinder\"},\n    \"obgx\"                     => {\"application/vnd.openblox.game+xml\"},\n    \"obj\"                      => {\"application/prs.wavefront-obj\", \"application/x-tgif\", \"model/obj\"},\n    \"ocl\"                      => {\"text/x-ocl\"},\n    \"ocx\"                      => {\"application/vnd.microsoft.portable-executable\"},\n    \"oda\"                      => {\"application/oda\"},\n    \"odb\"                      => {\"application/vnd.oasis.opendocument.base\", \"application/vnd.oasis.opendocument.database\", \"application/vnd.sun.xml.base\"},\n    \"odc\"                      => {\"application/vnd.oasis.opendocument.chart\"},\n    \"odf\"                      => {\"application/vnd.oasis.opendocument.formula\"},\n    \"odft\"                     => {\"application/vnd.oasis.opendocument.formula-template\"},\n    \"odg\"                      => {\"application/vnd.oasis.opendocument.graphics\"},\n    \"odi\"                      => {\"application/vnd.oasis.opendocument.image\"},\n    \"odm\"                      => {\"application/vnd.oasis.opendocument.text-master\"},\n    \"odp\"                      => {\"application/vnd.oasis.opendocument.presentation\"},\n    \"ods\"                      => {\"application/vnd.oasis.opendocument.spreadsheet\"},\n    \"odt\"                      => {\"application/vnd.oasis.opendocument.text\"},\n    \"oga\"                      => {\"audio/ogg\", \"audio/vorbis\", \"audio/x-flac+ogg\", \"audio/x-ogg\", \"audio/x-oggflac\", \"audio/x-speex+ogg\", \"audio/x-vorbis\", \"audio/x-vorbis+ogg\"},\n    \"ogex\"                     => {\"model/vnd.opengex\"},\n    \"ogg\"                      => {\"audio/ogg\", \"audio/vorbis\", \"audio/x-flac+ogg\", \"audio/x-ogg\", \"audio/x-oggflac\", \"audio/x-speex+ogg\", \"audio/x-vorbis\", \"audio/x-vorbis+ogg\", \"video/ogg\", \"video/x-ogg\", \"video/x-theora\", \"video/x-theora+ogg\"},\n    \"ogm\"                      => {\"video/x-ogm\", \"video/x-ogm+ogg\"},\n    \"ogv\"                      => {\"video/ogg\", \"video/x-ogg\"},\n    \"ogx\"                      => {\"application/ogg\", \"application/x-ogg\"},\n    \"old\"                      => {\"application/x-trash\"},\n    \"oleo\"                     => {\"application/x-oleo\"},\n    \"omdoc\"                    => {\"application/omdoc+xml\"},\n    \"onepkg\"                   => {\"application/onenote\"},\n    \"onetmp\"                   => {\"application/onenote\"},\n    \"onetoc\"                   => {\"application/onenote\"},\n    \"onetoc2\"                  => {\"application/onenote\"},\n    \"ooc\"                      => {\"text/x-ooc\"},\n    \"openvpn\"                  => {\"application/x-openvpn-profile\"},\n    \"opf\"                      => {\"application/oebps-package+xml\"},\n    \"opml\"                     => {\"text/x-opml\", \"text/x-opml+xml\"},\n    \"oprc\"                     => {\"application/vnd.palm\", \"application/x-palm-database\"},\n    \"opus\"                     => {\"audio/ogg\", \"audio/x-ogg\", \"audio/x-opus+ogg\"},\n    \"ora\"                      => {\"image/openraster\"},\n    \"orf\"                      => {\"image/x-olympus-orf\"},\n    \"org\"                      => {\"application/vnd.lotus-organizer\", \"text/org\", \"text/x-org\"},\n    \"osf\"                      => {\"application/vnd.yamaha.openscoreformat\"},\n    \"osfpvg\"                   => {\"application/vnd.yamaha.openscoreformat.osfpvg+xml\"},\n    \"osm\"                      => {\"application/vnd.openstreetmap.data+xml\"},\n    \"otc\"                      => {\"application/vnd.oasis.opendocument.chart-template\"},\n    \"otf\"                      => {\"application/vnd.oasis.opendocument.formula-template\", \"application/x-font-otf\", \"font/otf\"},\n    \"otg\"                      => {\"application/vnd.oasis.opendocument.graphics-template\"},\n    \"oth\"                      => {\"application/vnd.oasis.opendocument.text-web\"},\n    \"oti\"                      => {\"application/vnd.oasis.opendocument.image-template\"},\n    \"otm\"                      => {\"application/vnd.oasis.opendocument.text-master-template\"},\n    \"otp\"                      => {\"application/vnd.oasis.opendocument.presentation-template\"},\n    \"ots\"                      => {\"application/vnd.oasis.opendocument.spreadsheet-template\"},\n    \"ott\"                      => {\"application/vnd.oasis.opendocument.text-template\"},\n    \"ova\"                      => {\"application/ovf\", \"application/x-virtualbox-ova\"},\n    \"ovf\"                      => {\"application/x-virtualbox-ovf\"},\n    \"ovpn\"                     => {\"application/x-openvpn-profile\"},\n    \"owl\"                      => {\"application/rdf+xml\", \"text/rdf\"},\n    \"owx\"                      => {\"application/owl+xml\"},\n    \"oxps\"                     => {\"application/oxps\"},\n    \"oxt\"                      => {\"application/vnd.openofficeorg.extension\"},\n    \"p\"                        => {\"text/x-pascal\"},\n    \"p10\"                      => {\"application/pkcs10\"},\n    \"p12\"                      => {\"application/pkcs12\", \"application/x-pkcs12\"},\n    \"p65\"                      => {\"application/x-pagemaker\"},\n    \"p7b\"                      => {\"application/x-pkcs7-certificates\"},\n    \"p7c\"                      => {\"application/pkcs7-mime\"},\n    \"p7m\"                      => {\"application/pkcs7-mime\"},\n    \"p7r\"                      => {\"application/x-pkcs7-certreqresp\"},\n    \"p7s\"                      => {\"application/pkcs7-signature\"},\n    \"p8\"                       => {\"application/pkcs8\"},\n    \"p8e\"                      => {\"application/pkcs8-encrypted\"},\n    \"pac\"                      => {\"application/x-ns-proxy-autoconfig\"},\n    \"pack\"                     => {\"application/x-java-pack200\"},\n    \"pages\"                    => {\"application/vnd.apple.pages\", \"application/x-iwork-pages-sffpages\"},\n    \"pak\"                      => {\"application/x-pak\"},\n    \"par2\"                     => {\"application/x-par2\"},\n    \"parquet\"                  => {\"application/vnd.apache.parquet\", \"application/x-parquet\"},\n    \"part\"                     => {\"application/x-partial-download\"},\n    \"pas\"                      => {\"text/x-pascal\"},\n    \"pat\"                      => {\"image/x-gimp-pat\"},\n    \"patch\"                    => {\"text/x-diff\", \"text/x-patch\"},\n    \"path\"                     => {\"text/x-systemd-unit\"},\n    \"paw\"                      => {\"application/vnd.pawaafile\"},\n    \"pbd\"                      => {\"application/vnd.powerbuilder6\"},\n    \"pbm\"                      => {\"image/x-portable-bitmap\"},\n    \"pcap\"                     => {\"application/pcap\", \"application/vnd.tcpdump.pcap\", \"application/x-pcap\"},\n    \"pcapng\"                   => {\"application/x-pcapng\"},\n    \"pcd\"                      => {\"image/x-photo-cd\"},\n    \"pce\"                      => {\"application/x-pc-engine-rom\"},\n    \"pcf\"                      => {\"application/x-cisco-vpn-settings\", \"application/x-font-pcf\"},\n    \"pcf.Z\"                    => {\"application/x-font-pcf\"},\n    \"pcf.gz\"                   => {\"application/x-font-pcf\"},\n    \"pcl\"                      => {\"application/vnd.hp-pcl\"},\n    \"pclxl\"                    => {\"application/vnd.hp-pclxl\"},\n    \"pct\"                      => {\"image/x-pict\"},\n    \"pcurl\"                    => {\"application/vnd.curl.pcurl\"},\n    \"pcx\"                      => {\"image/vnd.zbrush.pcx\", \"image/x-pcx\"},\n    \"pdb\"                      => {\"application/vnd.palm\", \"application/x-aportisdoc\", \"application/x-ms-pdb\", \"application/x-palm-database\", \"application/x-pilot\", \"chemical/x-pdb\"},\n    \"pdc\"                      => {\"application/x-aportisdoc\"},\n    \"pde\"                      => {\"text/x-processing\"},\n    \"pdf\"                      => {\"application/pdf\", \"application/acrobat\", \"application/nappdf\", \"application/x-pdf\", \"image/pdf\"},\n    \"pdf.bz2\"                  => {\"application/x-bzpdf\"},\n    \"pdf.gz\"                   => {\"application/x-gzpdf\"},\n    \"pdf.lz\"                   => {\"application/x-lzpdf\"},\n    \"pdf.xz\"                   => {\"application/x-xzpdf\"},\n    \"pef\"                      => {\"image/x-pentax-pef\"},\n    \"pem\"                      => {\"application/x-x509-ca-cert\"},\n    \"perl\"                     => {\"application/x-perl\", \"text/x-perl\"},\n    \"pfa\"                      => {\"application/x-font-type1\"},\n    \"pfb\"                      => {\"application/x-font-type1\"},\n    \"pfm\"                      => {\"application/x-font-type1\", \"image/x-pfm\"},\n    \"pfr\"                      => {\"application/font-tdpfr\", \"application/vnd.truedoc\"},\n    \"pfx\"                      => {\"application/pkcs12\", \"application/x-pkcs12\"},\n    \"pgm\"                      => {\"image/x-portable-graymap\"},\n    \"pgn\"                      => {\"application/vnd.chess-pgn\", \"application/x-chess-pgn\"},\n    \"pgp\"                      => {\"application/pgp\", \"application/pgp-encrypted\", \"application/pgp-keys\", \"application/pgp-signature\"},\n    \"phm\"                      => {\"image/x-phm\"},\n    \"php\"                      => {\"application/x-php\", \"application/x-httpd-php\"},\n    \"php3\"                     => {\"application/x-php\"},\n    \"php4\"                     => {\"application/x-php\"},\n    \"php5\"                     => {\"application/x-php\"},\n    \"phps\"                     => {\"application/x-php\"},\n    \"pic\"                      => {\"image/vnd.radiance\", \"image/x-pict\"},\n    \"pict\"                     => {\"image/x-pict\"},\n    \"pict1\"                    => {\"image/x-pict\"},\n    \"pict2\"                    => {\"image/x-pict\"},\n    \"pk\"                       => {\"application/x-tex-pk\"},\n    \"pkg\"                      => {\"application/x-xar\"},\n    \"pki\"                      => {\"application/pkixcmp\"},\n    \"pkipath\"                  => {\"application/pkix-pkipath\"},\n    \"pkpass\"                   => {\"application/vnd.apple.pkpass\"},\n    \"pkpasses\"                 => {\"application/vnd.apple.pkpasses\"},\n    \"pkr\"                      => {\"application/pgp-keys\"},\n    \"pl\"                       => {\"application/x-perl\", \"text/x-perl\"},\n    \"pla\"                      => {\"audio/x-iriver-pla\"},\n    \"plb\"                      => {\"application/vnd.3gpp.pic-bw-large\"},\n    \"plc\"                      => {\"application/vnd.mobius.plc\"},\n    \"plf\"                      => {\"application/vnd.pocketlearn\"},\n    \"pln\"                      => {\"application/x-planperfect\"},\n    \"pls\"                      => {\"application/pls\", \"application/pls+xml\", \"audio/scpls\", \"audio/x-scpls\"},\n    \"pm\"                       => {\"application/x-pagemaker\", \"application/x-perl\", \"text/x-perl\"},\n    \"pm6\"                      => {\"application/x-pagemaker\"},\n    \"pmd\"                      => {\"application/x-pagemaker\"},\n    \"pml\"                      => {\"application/vnd.ctc-posml\"},\n    \"png\"                      => {\"image/png\", \"image/apng\", \"image/vnd.mozilla.apng\"},\n    \"pnm\"                      => {\"image/x-portable-anymap\"},\n    \"pntg\"                     => {\"image/x-macpaint\"},\n    \"po\"                       => {\"application/x-gettext\", \"text/x-gettext-translation\", \"text/x-po\"},\n    \"pod\"                      => {\"application/x-perl\", \"text/x-perl\"},\n    \"por\"                      => {\"application/x-spss-por\"},\n    \"portpkg\"                  => {\"application/vnd.macports.portpkg\"},\n    \"pot\"                      => {\"application/mspowerpoint\", \"application/powerpoint\", \"application/vnd.ms-powerpoint\", \"application/x-mspowerpoint\", \"text/x-gettext-translation-template\", \"text/x-pot\"},\n    \"potm\"                     => {\"application/vnd.ms-powerpoint.template.macroenabled.12\"},\n    \"potx\"                     => {\"application/vnd.openxmlformats-officedocument.presentationml.template\"},\n    \"ppam\"                     => {\"application/vnd.ms-powerpoint.addin.macroenabled.12\"},\n    \"ppd\"                      => {\"application/vnd.cups-ppd\"},\n    \"ppm\"                      => {\"image/x-portable-pixmap\"},\n    \"pps\"                      => {\"application/mspowerpoint\", \"application/powerpoint\", \"application/vnd.ms-powerpoint\", \"application/x-mspowerpoint\"},\n    \"ppsm\"                     => {\"application/vnd.ms-powerpoint.slideshow.macroenabled.12\"},\n    \"ppsx\"                     => {\"application/vnd.openxmlformats-officedocument.presentationml.slideshow\"},\n    \"ppt\"                      => {\"application/vnd.ms-powerpoint\", \"application/mspowerpoint\", \"application/powerpoint\", \"application/x-mspowerpoint\"},\n    \"pptm\"                     => {\"application/vnd.ms-powerpoint.presentation.macroenabled.12\"},\n    \"pptx\"                     => {\"application/vnd.openxmlformats-officedocument.presentationml.presentation\"},\n    \"ppz\"                      => {\"application/mspowerpoint\", \"application/powerpoint\", \"application/vnd.ms-powerpoint\", \"application/x-mspowerpoint\"},\n    \"pqa\"                      => {\"application/vnd.palm\", \"application/x-palm-database\"},\n    \"prc\"                      => {\"application/vnd.palm\", \"application/x-mobipocket-ebook\", \"application/x-palm-database\", \"application/x-pilot\", \"model/prc\"},\n    \"pre\"                      => {\"application/vnd.lotus-freelance\"},\n    \"prf\"                      => {\"application/pics-rules\"},\n    \"provx\"                    => {\"application/provenance+xml\"},\n    \"ps\"                       => {\"application/postscript\"},\n    \"ps.bz2\"                   => {\"application/x-bzpostscript\"},\n    \"ps.gz\"                    => {\"application/x-gzpostscript\"},\n    \"ps1\"                      => {\"application/x-powershell\"},\n    \"psb\"                      => {\"application/vnd.3gpp.pic-bw-small\"},\n    \"psd\"                      => {\"application/photoshop\", \"application/x-photoshop\", \"image/photoshop\", \"image/psd\", \"image/vnd.adobe.photoshop\", \"image/x-photoshop\", \"image/x-psd\"},\n    \"psf\"                      => {\"application/x-font-linux-psf\", \"audio/x-psf\"},\n    \"psf.gz\"                   => {\"application/x-gz-font-linux-psf\"},\n    \"psflib\"                   => {\"audio/x-psflib\"},\n    \"psid\"                     => {\"audio/prs.sid\"},\n    \"pskcxml\"                  => {\"application/pskc+xml\"},\n    \"psw\"                      => {\"application/x-pocket-word\"},\n    \"pti\"                      => {\"image/prs.pti\"},\n    \"ptid\"                     => {\"application/vnd.pvi.ptid1\"},\n    \"pub\"                      => {\"application/vnd.ms-publisher\", \"application/x-mspublisher\", \"text/x-ssh-public-key\"},\n    \"pvb\"                      => {\"application/vnd.3gpp.pic-bw-var\"},\n    \"pw\"                       => {\"application/x-pw\"},\n    \"pwn\"                      => {\"application/vnd.3m.post-it-notes\"},\n    \"pxd\"                      => {\"text/x-cython\"},\n    \"pxi\"                      => {\"text/x-cython\"},\n    \"pxr\"                      => {\"image/x-pxr\"},\n    \"py\"                       => {\"text/x-python\", \"text/x-python2\", \"text/x-python3\"},\n    \"py2\"                      => {\"text/x-python2\"},\n    \"py3\"                      => {\"text/x-python3\"},\n    \"pya\"                      => {\"audio/vnd.ms-playready.media.pya\"},\n    \"pyc\"                      => {\"application/x-python-bytecode\"},\n    \"pyi\"                      => {\"text/x-python3\"},\n    \"pyo\"                      => {\"application/x-python-bytecode\", \"model/vnd.pytha.pyox\"},\n    \"pyox\"                     => {\"model/vnd.pytha.pyox\"},\n    \"pys\"                      => {\"application/x-pyspread-bz-spreadsheet\"},\n    \"pysu\"                     => {\"application/x-pyspread-spreadsheet\"},\n    \"pyv\"                      => {\"video/vnd.ms-playready.media.pyv\"},\n    \"pyx\"                      => {\"text/x-cython\"},\n    \"qam\"                      => {\"application/vnd.epson.quickanime\"},\n    \"qbo\"                      => {\"application/vnd.intu.qbo\"},\n    \"qbrew\"                    => {\"application/x-qbrew\"},\n    \"qcow\"                     => {\"application/x-qemu-disk\"},\n    \"qcow2\"                    => {\"application/x-qemu-disk\"},\n    \"qd\"                       => {\"application/x-fd-file\", \"application/x-raw-floppy-disk-image\"},\n    \"qed\"                      => {\"application/x-qed-disk\"},\n    \"qfx\"                      => {\"application/vnd.intu.qfx\"},\n    \"qif\"                      => {\"application/x-qw\", \"image/x-quicktime\"},\n    \"qml\"                      => {\"text/x-qml\"},\n    \"qmlproject\"               => {\"text/x-qml\"},\n    \"qmltypes\"                 => {\"text/x-qml\"},\n    \"qoi\"                      => {\"image/qoi\"},\n    \"qp\"                       => {\"application/x-qpress\"},\n    \"qps\"                      => {\"application/vnd.publishare-delta-tree\"},\n    \"qpw\"                      => {\"application/x-quattropro\"},\n    \"qs\"                       => {\"application/sparql-query\"},\n    \"qt\"                       => {\"video/quicktime\"},\n    \"qti\"                      => {\"application/x-qtiplot\"},\n    \"qti.gz\"                   => {\"application/x-qtiplot\"},\n    \"qtif\"                     => {\"image/x-quicktime\"},\n    \"qtl\"                      => {\"application/x-quicktime-media-link\", \"application/x-quicktimeplayer\"},\n    \"qtvr\"                     => {\"video/quicktime\"},\n    \"qwd\"                      => {\"application/vnd.quark.quarkxpress\"},\n    \"qwt\"                      => {\"application/vnd.quark.quarkxpress\"},\n    \"qxb\"                      => {\"application/vnd.quark.quarkxpress\"},\n    \"qxd\"                      => {\"application/vnd.quark.quarkxpress\"},\n    \"qxl\"                      => {\"application/vnd.quark.quarkxpress\"},\n    \"qxp\"                      => {\"application/vnd.quark.quarkxpress\"},\n    \"qxt\"                      => {\"application/vnd.quark.quarkxpress\"},\n    \"ra\"                       => {\"audio/vnd.m-realaudio\", \"audio/vnd.rn-realaudio\", \"audio/x-pn-realaudio\", \"audio/x-realaudio\"},\n    \"raf\"                      => {\"image/x-fuji-raf\"},\n    \"ram\"                      => {\"application/ram\", \"audio/x-pn-realaudio\"},\n    \"raml\"                     => {\"application/raml+yaml\"},\n    \"rapd\"                     => {\"application/route-apd+xml\"},\n    \"rar\"                      => {\"application/x-rar-compressed\", \"application/vnd.rar\", \"application/x-rar\"},\n    \"ras\"                      => {\"image/x-cmu-raster\"},\n    \"raw\"                      => {\"image/x-panasonic-raw\", \"image/x-panasonic-rw\"},\n    \"raw-disk-image\"           => {\"application/vnd.efi.img\", \"application/x-raw-disk-image\"},\n    \"raw-disk-image.xz\"        => {\"application/x-raw-disk-image-xz-compressed\"},\n    \"rax\"                      => {\"audio/vnd.m-realaudio\", \"audio/vnd.rn-realaudio\", \"audio/x-pn-realaudio\"},\n    \"rb\"                       => {\"application/x-ruby\"},\n    \"rcprofile\"                => {\"application/vnd.ipunplugged.rcprofile\"},\n    \"rdf\"                      => {\"application/rdf+xml\", \"text/rdf\"},\n    \"rdfs\"                     => {\"application/rdf+xml\", \"text/rdf\"},\n    \"rdz\"                      => {\"application/vnd.data-vision.rdz\"},\n    \"reg\"                      => {\"text/x-ms-regedit\"},\n    \"rej\"                      => {\"application/x-reject\", \"text/x-reject\"},\n    \"relo\"                     => {\"application/p2p-overlay+xml\"},\n    \"rep\"                      => {\"application/vnd.businessobjects\"},\n    \"res\"                      => {\"application/x-dtbresource+xml\", \"application/x-godot-resource\"},\n    \"rgb\"                      => {\"image/x-rgb\"},\n    \"rgbe\"                     => {\"image/vnd.radiance\"},\n    \"rif\"                      => {\"application/reginfo+xml\"},\n    \"rip\"                      => {\"audio/vnd.rip\"},\n    \"ris\"                      => {\"application/x-research-info-systems\"},\n    \"rl\"                       => {\"application/resource-lists+xml\"},\n    \"rlc\"                      => {\"image/vnd.fujixerox.edmics-rlc\"},\n    \"rld\"                      => {\"application/resource-lists-diff+xml\"},\n    \"rle\"                      => {\"image/rle\"},\n    \"rm\"                       => {\"application/vnd.rn-realmedia\", \"application/vnd.rn-realmedia-vbr\"},\n    \"rmi\"                      => {\"audio/midi\"},\n    \"rmj\"                      => {\"application/vnd.rn-realmedia\", \"application/vnd.rn-realmedia-vbr\"},\n    \"rmm\"                      => {\"application/vnd.rn-realmedia\", \"application/vnd.rn-realmedia-vbr\"},\n    \"rmp\"                      => {\"audio/x-pn-realaudio-plugin\"},\n    \"rms\"                      => {\"application/vnd.jcp.javame.midlet-rms\", \"application/vnd.rn-realmedia\", \"application/vnd.rn-realmedia-vbr\"},\n    \"rmvb\"                     => {\"application/vnd.rn-realmedia\", \"application/vnd.rn-realmedia-vbr\"},\n    \"rmx\"                      => {\"application/vnd.rn-realmedia\", \"application/vnd.rn-realmedia-vbr\"},\n    \"rnc\"                      => {\"application/relax-ng-compact-syntax\", \"application/x-rnc\"},\n    \"rng\"                      => {\"application/xml\", \"text/xml\"},\n    \"roa\"                      => {\"application/rpki-roa\"},\n    \"roff\"                     => {\"application/x-troff\", \"text/troff\", \"text/x-troff\"},\n    \"ros\"                      => {\"text/x-common-lisp\"},\n    \"rp\"                       => {\"image/vnd.rn-realpix\"},\n    \"rp9\"                      => {\"application/vnd.cloanto.rp9\"},\n    \"rpm\"                      => {\"application/x-redhat-package-manager\", \"application/x-rpm\"},\n    \"rpss\"                     => {\"application/vnd.nokia.radio-presets\"},\n    \"rpst\"                     => {\"application/vnd.nokia.radio-preset\"},\n    \"rq\"                       => {\"application/sparql-query\"},\n    \"rs\"                       => {\"application/rls-services+xml\", \"text/rust\"},\n    \"rsat\"                     => {\"application/atsc-rsat+xml\"},\n    \"rsd\"                      => {\"application/rsd+xml\"},\n    \"rsheet\"                   => {\"application/urc-ressheet+xml\"},\n    \"rss\"                      => {\"application/rss+xml\", \"text/rss\"},\n    \"rst\"                      => {\"text/x-rst\"},\n    \"rt\"                       => {\"text/vnd.rn-realtext\"},\n    \"rtf\"                      => {\"application/rtf\", \"text/rtf\"},\n    \"rtx\"                      => {\"text/richtext\"},\n    \"run\"                      => {\"application/x-makeself\"},\n    \"rusd\"                     => {\"application/route-usd+xml\"},\n    \"rv\"                       => {\"video/vnd.rn-realvideo\", \"video/x-real-video\"},\n    \"rvx\"                      => {\"video/vnd.rn-realvideo\", \"video/x-real-video\"},\n    \"rw2\"                      => {\"image/x-panasonic-raw2\", \"image/x-panasonic-rw2\"},\n    \"rz\"                       => {\"application/x-rzip\"},\n    \"s\"                        => {\"text/x-asm\"},\n    \"s3m\"                      => {\"audio/s3m\", \"audio/x-s3m\"},\n    \"saf\"                      => {\"application/vnd.yamaha.smaf-audio\"},\n    \"sage\"                     => {\"text/x-sagemath\"},\n    \"sam\"                      => {\"application/x-amipro\"},\n    \"sami\"                     => {\"application/x-sami\"},\n    \"sap\"                      => {\"application/x-sap-file\", \"application/x-thomson-sap-image\"},\n    \"sass\"                     => {\"text/x-sass\"},\n    \"sav\"                      => {\"application/x-spss-sav\", \"application/x-spss-savefile\"},\n    \"sbml\"                     => {\"application/sbml+xml\"},\n    \"sc\"                       => {\"application/vnd.ibm.secure-container\", \"text/x-scala\"},\n    \"scala\"                    => {\"text/x-scala\"},\n    \"scap\"                     => {\"application/x-pcapng\"},\n    \"scd\"                      => {\"application/x-msschedule\"},\n    \"scm\"                      => {\"application/vnd.lotus-screencam\", \"text/x-scheme\"},\n    \"scn\"                      => {\"application/x-godot-scene\"},\n    \"scope\"                    => {\"text/x-systemd-unit\"},\n    \"scq\"                      => {\"application/scvp-cv-request\"},\n    \"scr\"                      => {\"application/vnd.microsoft.portable-executable\", \"application/x-ms-dos-executable\", \"application/x-ms-ne-executable\", \"application/x-msdownload\"},\n    \"scs\"                      => {\"application/scvp-cv-response\"},\n    \"scss\"                     => {\"text/x-scss\"},\n    \"sct\"                      => {\"image/x-sct\"},\n    \"scurl\"                    => {\"text/vnd.curl.scurl\"},\n    \"sda\"                      => {\"application/vnd.stardivision.draw\", \"application/x-stardraw\"},\n    \"sdc\"                      => {\"application/vnd.stardivision.calc\", \"application/x-starcalc\"},\n    \"sdd\"                      => {\"application/vnd.stardivision.impress\", \"application/x-starimpress\"},\n    \"sdkd\"                     => {\"application/vnd.solent.sdkm+xml\"},\n    \"sdkm\"                     => {\"application/vnd.solent.sdkm+xml\"},\n    \"sdm\"                      => {\"application/vnd.stardivision.mail\"},\n    \"sdp\"                      => {\"application/sdp\", \"application/vnd.sdp\", \"application/vnd.stardivision.impress-packed\", \"application/x-sdp\"},\n    \"sds\"                      => {\"application/vnd.stardivision.chart\", \"application/x-starchart\"},\n    \"sdw\"                      => {\"application/vnd.stardivision.writer\", \"application/x-starwriter\"},\n    \"sea\"                      => {\"application/x-sea\"},\n    \"see\"                      => {\"application/vnd.seemail\"},\n    \"seed\"                     => {\"application/vnd.fdsn.seed\"},\n    \"sema\"                     => {\"application/vnd.sema\"},\n    \"semd\"                     => {\"application/vnd.semd\"},\n    \"semf\"                     => {\"application/vnd.semf\"},\n    \"senmlx\"                   => {\"application/senml+xml\"},\n    \"sensmlx\"                  => {\"application/sensml+xml\"},\n    \"ser\"                      => {\"application/java-serialized-object\"},\n    \"service\"                  => {\"text/x-dbus-service\", \"text/x-systemd-unit\"},\n    \"setpay\"                   => {\"application/set-payment-initiation\"},\n    \"setreg\"                   => {\"application/set-registration-initiation\"},\n    \"sfc\"                      => {\"application/vnd.nintendo.snes.rom\", \"application/x-snes-rom\"},\n    \"sfd-hdstx\"                => {\"application/vnd.hydrostatix.sof-data\"},\n    \"sfs\"                      => {\"application/vnd.spotfire.sfs\", \"application/vnd.squashfs\"},\n    \"sfv\"                      => {\"text/x-sfv\"},\n    \"sg\"                       => {\"application/x-sg1000-rom\"},\n    \"sgb\"                      => {\"application/x-gameboy-rom\"},\n    \"sgd\"                      => {\"application/x-genesis-rom\"},\n    \"sgf\"                      => {\"application/x-go-sgf\"},\n    \"sgi\"                      => {\"image/sgi\", \"image/x-sgi\"},\n    \"sgl\"                      => {\"application/vnd.stardivision.writer-global\", \"application/x-starwriter-global\"},\n    \"sgm\"                      => {\"text/sgml\"},\n    \"sgml\"                     => {\"text/sgml\"},\n    \"sh\"                       => {\"application/x-sh\", \"application/x-shellscript\", \"text/x-sh\"},\n    \"shape\"                    => {\"application/x-dia-shape\"},\n    \"shar\"                     => {\"application/x-shar\"},\n    \"shex\"                     => {\"text/shex\"},\n    \"shf\"                      => {\"application/shf+xml\"},\n    \"shn\"                      => {\"application/x-shorten\", \"audio/x-shorten\"},\n    \"shtml\"                    => {\"text/html\"},\n    \"siag\"                     => {\"application/x-siag\"},\n    \"sid\"                      => {\"audio/prs.sid\", \"image/x-mrsid-image\"},\n    \"sieve\"                    => {\"application/sieve\"},\n    \"sig\"                      => {\"application/pgp-signature\"},\n    \"sik\"                      => {\"application/x-trash\"},\n    \"sil\"                      => {\"audio/silk\"},\n    \"silo\"                     => {\"model/mesh\"},\n    \"sis\"                      => {\"application/vnd.symbian.install\"},\n    \"sisx\"                     => {\"application/vnd.symbian.install\", \"x-epoc/x-sisx-app\"},\n    \"sit\"                      => {\"application/x-stuffit\", \"application/stuffit\", \"application/x-sit\"},\n    \"sitx\"                     => {\"application/x-sitx\", \"application/x-stuffitx\"},\n    \"siv\"                      => {\"application/sieve\"},\n    \"sk\"                       => {\"image/x-skencil\"},\n    \"sk1\"                      => {\"image/x-skencil\"},\n    \"skd\"                      => {\"application/vnd.koan\"},\n    \"skm\"                      => {\"application/vnd.koan\"},\n    \"skp\"                      => {\"application/vnd.koan\"},\n    \"skr\"                      => {\"application/pgp-keys\"},\n    \"skt\"                      => {\"application/vnd.koan\"},\n    \"sldm\"                     => {\"application/vnd.ms-powerpoint.slide.macroenabled.12\"},\n    \"sldx\"                     => {\"application/vnd.openxmlformats-officedocument.presentationml.slide\"},\n    \"slice\"                    => {\"text/x-systemd-unit\"},\n    \"slim\"                     => {\"text/slim\"},\n    \"slk\"                      => {\"application/x-sylk\", \"text/spreadsheet\"},\n    \"slm\"                      => {\"text/slim\"},\n    \"sls\"                      => {\"application/route-s-tsid+xml\"},\n    \"slt\"                      => {\"application/vnd.epson.salt\"},\n    \"sm\"                       => {\"application/vnd.stepmania.stepchart\"},\n    \"smaf\"                     => {\"application/vnd.smaf\", \"application/x-smaf\"},\n    \"smc\"                      => {\"application/vnd.nintendo.snes.rom\", \"application/x-snes-rom\"},\n    \"smd\"                      => {\"application/x-genesis-rom\", \"application/x-starmail\"},\n    \"smf\"                      => {\"application/vnd.stardivision.math\", \"application/x-starmath\"},\n    \"smi\"                      => {\"application/smil\", \"application/smil+xml\", \"application/x-sami\"},\n    \"smil\"                     => {\"application/smil\", \"application/smil+xml\"},\n    \"smk\"                      => {\"video/vnd.radgamettools.smacker\"},\n    \"sml\"                      => {\"application/smil\", \"application/smil+xml\"},\n    \"sms\"                      => {\"application/x-sms-rom\"},\n    \"smv\"                      => {\"video/x-smv\"},\n    \"smzip\"                    => {\"application/vnd.stepmania.package\"},\n    \"snap\"                     => {\"application/vnd.snap\"},\n    \"snd\"                      => {\"audio/basic\"},\n    \"snf\"                      => {\"application/x-font-snf\"},\n    \"so\"                       => {\"application/x-sharedlib\"},\n    \"so.[0-9]*\"                => {\"application/x-sharedlib\"},\n    \"socket\"                   => {\"text/x-systemd-unit\"},\n    \"spc\"                      => {\"application/x-pkcs7-certificates\"},\n    \"spd\"                      => {\"application/x-font-speedo\"},\n    \"spdx\"                     => {\"text/spdx\"},\n    \"spec\"                     => {\"text/x-rpm-spec\"},\n    \"spf\"                      => {\"application/vnd.yamaha.smaf-phrase\"},\n    \"spl\"                      => {\"application/futuresplash\", \"application/vnd.adobe.flash.movie\", \"application/x-futuresplash\", \"application/x-shockwave-flash\"},\n    \"spm\"                      => {\"application/x-source-rpm\"},\n    \"spot\"                     => {\"text/vnd.in3d.spot\"},\n    \"spp\"                      => {\"application/scvp-vp-response\"},\n    \"spq\"                      => {\"application/scvp-vp-request\"},\n    \"spx\"                      => {\"application/x-apple-systemprofiler+xml\", \"audio/ogg\", \"audio/x-speex\", \"audio/x-speex+ogg\"},\n    \"sqfs\"                     => {\"application/vnd.squashfs\"},\n    \"sql\"                      => {\"application/sql\", \"application/x-sql\", \"text/x-sql\"},\n    \"sqlite2\"                  => {\"application/x-sqlite2\"},\n    \"sqlite3\"                  => {\"application/vnd.sqlite3\", \"application/x-sqlite3\"},\n    \"sqsh\"                     => {\"application/vnd.squashfs\"},\n    \"squashfs\"                 => {\"application/vnd.squashfs\"},\n    \"sr2\"                      => {\"image/x-sony-sr2\"},\n    \"src\"                      => {\"application/x-wais-source\"},\n    \"src.rpm\"                  => {\"application/x-source-rpm\"},\n    \"srf\"                      => {\"image/x-sony-srf\"},\n    \"srt\"                      => {\"application/x-srt\", \"application/x-subrip\"},\n    \"sru\"                      => {\"application/sru+xml\"},\n    \"srx\"                      => {\"application/sparql-results+xml\"},\n    \"ss\"                       => {\"text/x-scheme\"},\n    \"ssa\"                      => {\"text/x-ssa\"},\n    \"ssdl\"                     => {\"application/ssdl+xml\"},\n    \"sse\"                      => {\"application/vnd.kodak-descriptor\"},\n    \"ssf\"                      => {\"application/vnd.epson.ssf\"},\n    \"ssml\"                     => {\"application/ssml+xml\"},\n    \"st\"                       => {\"application/vnd.sailingtracker.track\"},\n    \"stc\"                      => {\"application/vnd.sun.xml.calc.template\"},\n    \"std\"                      => {\"application/vnd.sun.xml.draw.template\"},\n    \"step\"                     => {\"model/step\"},\n    \"stf\"                      => {\"application/vnd.wt.stf\"},\n    \"sti\"                      => {\"application/vnd.sun.xml.impress.template\"},\n    \"stk\"                      => {\"application/hyperstudio\"},\n    \"stl\"                      => {\"application/vnd.ms-pki.stl\", \"model/stl\", \"model/x.stl-ascii\", \"model/x.stl-binary\"},\n    \"stm\"                      => {\"audio/x-stm\"},\n    \"stp\"                      => {\"model/step\"},\n    \"stpx\"                     => {\"model/step+xml\"},\n    \"stpxz\"                    => {\"model/step-xml+zip\"},\n    \"stpz\"                     => {\"model/step+zip\"},\n    \"str\"                      => {\"application/vnd.pg.format\"},\n    \"stw\"                      => {\"application/vnd.sun.xml.writer.template\"},\n    \"sty\"                      => {\"application/x-tex\", \"text/x-tex\"},\n    \"styl\"                     => {\"text/stylus\"},\n    \"stylus\"                   => {\"text/stylus\"},\n    \"sub\"                      => {\"image/vnd.dvb.subtitle\", \"text/vnd.dvb.subtitle\", \"text/x-microdvd\", \"text/x-mpsub\", \"text/x-subviewer\"},\n    \"sun\"                      => {\"image/x-sun-raster\"},\n    \"sus\"                      => {\"application/vnd.sus-calendar\"},\n    \"susp\"                     => {\"application/vnd.sus-calendar\"},\n    \"sv\"                       => {\"text/x-svsrc\"},\n    \"sv4cpio\"                  => {\"application/x-sv4cpio\"},\n    \"sv4crc\"                   => {\"application/x-sv4crc\"},\n    \"svc\"                      => {\"application/vnd.dvb.service\"},\n    \"svd\"                      => {\"application/vnd.svd\"},\n    \"svg\"                      => {\"image/svg+xml\", \"image/svg\"},\n    \"svg.gz\"                   => {\"image/svg+xml-compressed\"},\n    \"svgz\"                     => {\"image/svg+xml\", \"image/svg+xml-compressed\"},\n    \"svh\"                      => {\"text/x-svhdr\"},\n    \"swa\"                      => {\"application/x-director\"},\n    \"swap\"                     => {\"text/x-systemd-unit\"},\n    \"swf\"                      => {\"application/futuresplash\", \"application/vnd.adobe.flash.movie\", \"application/x-shockwave-flash\"},\n    \"swi\"                      => {\"application/vnd.aristanetworks.swi\"},\n    \"swidtag\"                  => {\"application/swid+xml\"},\n    \"swm\"                      => {\"application/x-ms-wim\"},\n    \"sxc\"                      => {\"application/vnd.sun.xml.calc\"},\n    \"sxd\"                      => {\"application/vnd.sun.xml.draw\"},\n    \"sxg\"                      => {\"application/vnd.sun.xml.writer.global\"},\n    \"sxi\"                      => {\"application/vnd.sun.xml.impress\"},\n    \"sxm\"                      => {\"application/vnd.sun.xml.math\"},\n    \"sxw\"                      => {\"application/vnd.sun.xml.writer\"},\n    \"sylk\"                     => {\"application/x-sylk\", \"text/spreadsheet\"},\n    \"sys\"                      => {\"application/vnd.microsoft.portable-executable\"},\n    \"t\"                        => {\"application/x-perl\", \"application/x-troff\", \"text/troff\", \"text/x-perl\", \"text/x-troff\"},\n    \"t2t\"                      => {\"text/x-txt2tags\"},\n    \"t3\"                       => {\"application/x-t3vm-image\"},\n    \"t38\"                      => {\"image/t38\"},\n    \"taglet\"                   => {\"application/vnd.mynfc\"},\n    \"tak\"                      => {\"audio/x-tak\"},\n    \"tao\"                      => {\"application/vnd.tao.intent-module-archive\"},\n    \"tap\"                      => {\"image/vnd.tencent.tap\"},\n    \"tar\"                      => {\"application/x-tar\", \"application/x-gtar\"},\n    \"tar.Z\"                    => {\"application/x-tarz\"},\n    \"tar.bz\"                   => {\"application/x-bzip1-compressed-tar\"},\n    \"tar.bz2\"                  => {\"application/x-bzip-compressed-tar\", \"application/x-bzip2-compressed-tar\"},\n    \"tar.bz3\"                  => {\"application/x-bzip3-compressed-tar\"},\n    \"tar.gz\"                   => {\"application/x-compressed-tar\"},\n    \"tar.lrz\"                  => {\"application/x-lrzip-compressed-tar\"},\n    \"tar.lz\"                   => {\"application/x-lzip-compressed-tar\"},\n    \"tar.lz4\"                  => {\"application/x-lz4-compressed-tar\"},\n    \"tar.lzma\"                 => {\"application/x-lzma-compressed-tar\"},\n    \"tar.lzo\"                  => {\"application/x-tzo\"},\n    \"tar.rz\"                   => {\"application/x-rzip-compressed-tar\"},\n    \"tar.xz\"                   => {\"application/x-xz-compressed-tar\"},\n    \"tar.zst\"                  => {\"application/x-zstd-compressed-tar\"},\n    \"target\"                   => {\"text/x-systemd-unit\"},\n    \"taz\"                      => {\"application/x-tarz\"},\n    \"tb2\"                      => {\"application/x-bzip-compressed-tar\", \"application/x-bzip2-compressed-tar\"},\n    \"tbz\"                      => {\"application/x-bzip1-compressed-tar\"},\n    \"tbz2\"                     => {\"application/x-bzip-compressed-tar\", \"application/x-bzip2-compressed-tar\"},\n    \"tbz3\"                     => {\"application/x-bzip3-compressed-tar\"},\n    \"tcap\"                     => {\"application/vnd.3gpp2.tcap\"},\n    \"tcl\"                      => {\"application/x-tcl\", \"text/tcl\", \"text/x-tcl\"},\n    \"td\"                       => {\"application/urc-targetdesc+xml\"},\n    \"teacher\"                  => {\"application/vnd.smart.teacher\"},\n    \"tei\"                      => {\"application/tei+xml\"},\n    \"teicorpus\"                => {\"application/tei+xml\"},\n    \"tex\"                      => {\"application/x-tex\", \"text/x-tex\"},\n    \"texi\"                     => {\"application/x-texinfo\", \"text/x-texinfo\"},\n    \"texinfo\"                  => {\"application/x-texinfo\", \"text/x-texinfo\"},\n    \"text\"                     => {\"text/plain\"},\n    \"tfi\"                      => {\"application/thraud+xml\"},\n    \"tfm\"                      => {\"application/x-tex-tfm\"},\n    \"tfx\"                      => {\"image/tiff-fx\"},\n    \"tga\"                      => {\"application/tga\", \"application/x-targa\", \"application/x-tga\", \"image/targa\", \"image/tga\", \"image/x-icb\", \"image/x-targa\", \"image/x-tga\"},\n    \"tgz\"                      => {\"application/x-compressed-tar\"},\n    \"theme\"                    => {\"application/x-theme\"},\n    \"themepack\"                => {\"application/x-windows-themepack\"},\n    \"thmx\"                     => {\"application/vnd.ms-officetheme\"},\n    \"tif\"                      => {\"image/tiff\"},\n    \"tiff\"                     => {\"image/tiff\"},\n    \"timer\"                    => {\"text/x-systemd-unit\"},\n    \"tk\"                       => {\"application/x-tcl\", \"text/tcl\", \"text/x-tcl\"},\n    \"tlrz\"                     => {\"application/x-lrzip-compressed-tar\"},\n    \"tlz\"                      => {\"application/x-lzma-compressed-tar\"},\n    \"tmo\"                      => {\"application/vnd.tmobile-livetv\"},\n    \"tmx\"                      => {\"application/x-tiled-tmx\"},\n    \"tnef\"                     => {\"application/ms-tnef\", \"application/vnd.ms-tnef\"},\n    \"tnf\"                      => {\"application/ms-tnef\", \"application/vnd.ms-tnef\"},\n    \"toc\"                      => {\"application/x-cdrdao-toc\"},\n    \"toml\"                     => {\"application/toml\"},\n    \"torrent\"                  => {\"application/x-bittorrent\"},\n    \"tpic\"                     => {\"application/tga\", \"application/x-targa\", \"application/x-tga\", \"image/targa\", \"image/tga\", \"image/x-icb\", \"image/x-targa\", \"image/x-tga\"},\n    \"tpl\"                      => {\"application/vnd.groove-tool-template\"},\n    \"tpt\"                      => {\"application/vnd.trid.tpt\"},\n    \"tr\"                       => {\"application/x-troff\", \"text/troff\", \"text/x-troff\"},\n    \"tra\"                      => {\"application/vnd.trueapp\"},\n    \"tres\"                     => {\"application/x-godot-resource\"},\n    \"trig\"                     => {\"application/trig\", \"application/x-trig\"},\n    \"trm\"                      => {\"application/x-msterminal\"},\n    \"trz\"                      => {\"application/x-rzip-compressed-tar\"},\n    \"ts\"                       => {\"application/typescript\", \"application/x-linguist\", \"text/vnd.qt.linguist\", \"text/vnd.trolltech.linguist\", \"video/mp2t\"},\n    \"tscn\"                     => {\"application/x-godot-scene\"},\n    \"tsd\"                      => {\"application/timestamped-data\"},\n    \"tsv\"                      => {\"text/tab-separated-values\"},\n    \"tsx\"                      => {\"application/x-tiled-tsx\"},\n    \"tta\"                      => {\"audio/tta\", \"audio/x-tta\"},\n    \"ttc\"                      => {\"font/collection\"},\n    \"ttf\"                      => {\"application/x-font-truetype\", \"application/x-font-ttf\", \"font/ttf\"},\n    \"ttl\"                      => {\"text/turtle\"},\n    \"ttml\"                     => {\"application/ttml+xml\"},\n    \"ttx\"                      => {\"application/x-font-ttx\"},\n    \"twd\"                      => {\"application/vnd.simtech-mindmapper\"},\n    \"twds\"                     => {\"application/vnd.simtech-mindmapper\"},\n    \"twig\"                     => {\"text/x-twig\"},\n    \"txd\"                      => {\"application/vnd.genomatix.tuxedo\"},\n    \"txf\"                      => {\"application/vnd.mobius.txf\"},\n    \"txt\"                      => {\"text/plain\"},\n    \"txz\"                      => {\"application/x-xz-compressed-tar\"},\n    \"typ\"                      => {\"text/vnd.typst\", \"text/x-typst\"},\n    \"tzo\"                      => {\"application/x-tzo\"},\n    \"tzst\"                     => {\"application/x-zstd-compressed-tar\"},\n    \"u32\"                      => {\"application/x-authorware-bin\"},\n    \"u3d\"                      => {\"model/u3d\"},\n    \"u8dsn\"                    => {\"message/global-delivery-status\"},\n    \"u8hdr\"                    => {\"message/global-headers\"},\n    \"u8mdn\"                    => {\"message/global-disposition-notification\"},\n    \"u8msg\"                    => {\"message/global\"},\n    \"ubj\"                      => {\"application/ubjson\"},\n    \"udeb\"                     => {\"application/vnd.debian.binary-package\", \"application/x-deb\", \"application/x-debian-package\"},\n    \"ufd\"                      => {\"application/vnd.ufdl\"},\n    \"ufdl\"                     => {\"application/vnd.ufdl\"},\n    \"ufraw\"                    => {\"application/x-ufraw\"},\n    \"ui\"                       => {\"application/x-designer\", \"application/x-gtk-builder\"},\n    \"uil\"                      => {\"text/x-uil\"},\n    \"ult\"                      => {\"audio/x-mod\"},\n    \"ulx\"                      => {\"application/x-glulx\"},\n    \"umj\"                      => {\"application/vnd.umajin\"},\n    \"unf\"                      => {\"application/x-nes-rom\"},\n    \"uni\"                      => {\"audio/x-mod\"},\n    \"unif\"                     => {\"application/x-nes-rom\"},\n    \"unityweb\"                 => {\"application/vnd.unity\"},\n    \"uo\"                       => {\"application/vnd.uoml+xml\"},\n    \"uoml\"                     => {\"application/vnd.uoml+xml\"},\n    \"uri\"                      => {\"text/uri-list\"},\n    \"uris\"                     => {\"text/uri-list\"},\n    \"url\"                      => {\"application/x-mswinurl\"},\n    \"urls\"                     => {\"text/uri-list\"},\n    \"usda\"                     => {\"model/vnd.usda\"},\n    \"usdz\"                     => {\"model/vnd.usdz+zip\"},\n    \"ustar\"                    => {\"application/x-ustar\"},\n    \"utz\"                      => {\"application/vnd.uiq.theme\"},\n    \"uu\"                       => {\"text/x-uuencode\"},\n    \"uue\"                      => {\"text/x-uuencode\", \"zz-application/zz-winassoc-uu\"},\n    \"uva\"                      => {\"audio/vnd.dece.audio\"},\n    \"uvd\"                      => {\"application/vnd.dece.data\"},\n    \"uvf\"                      => {\"application/vnd.dece.data\"},\n    \"uvg\"                      => {\"image/vnd.dece.graphic\"},\n    \"uvh\"                      => {\"video/vnd.dece.hd\"},\n    \"uvi\"                      => {\"image/vnd.dece.graphic\"},\n    \"uvm\"                      => {\"video/vnd.dece.mobile\"},\n    \"uvp\"                      => {\"video/vnd.dece.pd\"},\n    \"uvs\"                      => {\"video/vnd.dece.sd\"},\n    \"uvt\"                      => {\"application/vnd.dece.ttml+xml\"},\n    \"uvu\"                      => {\"video/vnd.uvvu.mp4\"},\n    \"uvv\"                      => {\"video/vnd.dece.video\"},\n    \"uvva\"                     => {\"audio/vnd.dece.audio\"},\n    \"uvvd\"                     => {\"application/vnd.dece.data\"},\n    \"uvvf\"                     => {\"application/vnd.dece.data\"},\n    \"uvvg\"                     => {\"image/vnd.dece.graphic\"},\n    \"uvvh\"                     => {\"video/vnd.dece.hd\"},\n    \"uvvi\"                     => {\"image/vnd.dece.graphic\"},\n    \"uvvm\"                     => {\"video/vnd.dece.mobile\"},\n    \"uvvp\"                     => {\"video/vnd.dece.pd\"},\n    \"uvvs\"                     => {\"video/vnd.dece.sd\"},\n    \"uvvt\"                     => {\"application/vnd.dece.ttml+xml\"},\n    \"uvvu\"                     => {\"video/vnd.uvvu.mp4\"},\n    \"uvvv\"                     => {\"video/vnd.dece.video\"},\n    \"uvvx\"                     => {\"application/vnd.dece.unspecified\"},\n    \"uvvz\"                     => {\"application/vnd.dece.zip\"},\n    \"uvx\"                      => {\"application/vnd.dece.unspecified\"},\n    \"uvz\"                      => {\"application/vnd.dece.zip\"},\n    \"v\"                        => {\"text/x-verilog\"},\n    \"v64\"                      => {\"application/x-n64-rom\"},\n    \"vala\"                     => {\"text/x-vala\"},\n    \"vapi\"                     => {\"text/x-vala\"},\n    \"vb\"                       => {\"application/x-virtual-boy-rom\", \"text/x-vb\"},\n    \"vbe\"                      => {\"text/vbscript.encode\"},\n    \"vbox\"                     => {\"application/x-virtualbox-vbox\"},\n    \"vbox-extpack\"             => {\"application/x-virtualbox-vbox-extpack\"},\n    \"vbs\"                      => {\"text/vbs\", \"text/vbscript\"},\n    \"vcard\"                    => {\"text/directory\", \"text/vcard\", \"text/x-vcard\"},\n    \"vcd\"                      => {\"application/x-cdlink\"},\n    \"vcf\"                      => {\"text/x-vcard\", \"text/directory\", \"text/vcard\"},\n    \"vcg\"                      => {\"application/vnd.groove-vcard\"},\n    \"vcs\"                      => {\"application/ics\", \"text/calendar\", \"text/x-vcalendar\"},\n    \"vct\"                      => {\"text/directory\", \"text/vcard\", \"text/x-vcard\"},\n    \"vcx\"                      => {\"application/vnd.vcx\"},\n    \"vda\"                      => {\"application/tga\", \"application/x-targa\", \"application/x-tga\", \"image/targa\", \"image/tga\", \"image/x-icb\", \"image/x-targa\", \"image/x-tga\"},\n    \"vdi\"                      => {\"application/x-vdi-disk\", \"application/x-virtualbox-vdi\"},\n    \"vds\"                      => {\"model/vnd.sap.vds\"},\n    \"vhd\"                      => {\"application/x-vhd-disk\", \"application/x-virtualbox-vhd\", \"text/x-vhdl\"},\n    \"vhdl\"                     => {\"text/x-vhdl\"},\n    \"vhdx\"                     => {\"application/x-vhdx-disk\", \"application/x-virtualbox-vhdx\"},\n    \"vis\"                      => {\"application/vnd.visionary\"},\n    \"viv\"                      => {\"video/vivo\", \"video/vnd.vivo\"},\n    \"vivo\"                     => {\"video/vivo\", \"video/vnd.vivo\"},\n    \"vlc\"                      => {\"application/m3u\", \"audio/m3u\", \"audio/mpegurl\", \"audio/x-m3u\", \"audio/x-mp3-playlist\", \"audio/x-mpegurl\"},\n    \"vmdk\"                     => {\"application/x-virtualbox-vmdk\", \"application/x-vmdk-disk\"},\n    \"vob\"                      => {\"video/mpeg\", \"video/mpeg-system\", \"video/x-mpeg\", \"video/x-mpeg-system\", \"video/x-mpeg2\", \"video/x-ms-vob\"},\n    \"voc\"                      => {\"audio/x-voc\"},\n    \"vor\"                      => {\"application/vnd.stardivision.writer\", \"application/x-starwriter\"},\n    \"vox\"                      => {\"application/x-authorware-bin\"},\n    \"vpc\"                      => {\"application/x-vhd-disk\", \"application/x-virtualbox-vhd\"},\n    \"vrm\"                      => {\"model/vrml\"},\n    \"vrml\"                     => {\"model/vrml\"},\n    \"vsd\"                      => {\"application/vnd.visio\"},\n    \"vsdm\"                     => {\"application/vnd.ms-visio.drawing.macroenabled.main+xml\"},\n    \"vsdx\"                     => {\"application/vnd.ms-visio.drawing.main+xml\"},\n    \"vsf\"                      => {\"application/vnd.vsf\"},\n    \"vss\"                      => {\"application/vnd.visio\"},\n    \"vssm\"                     => {\"application/vnd.ms-visio.stencil.macroenabled.main+xml\"},\n    \"vssx\"                     => {\"application/vnd.ms-visio.stencil.main+xml\"},\n    \"vst\"                      => {\"application/tga\", \"application/vnd.visio\", \"application/x-targa\", \"application/x-tga\", \"image/targa\", \"image/tga\", \"image/x-icb\", \"image/x-targa\", \"image/x-tga\"},\n    \"vstm\"                     => {\"application/vnd.ms-visio.template.macroenabled.main+xml\"},\n    \"vstx\"                     => {\"application/vnd.ms-visio.template.main+xml\"},\n    \"vsw\"                      => {\"application/vnd.visio\"},\n    \"vtf\"                      => {\"image/vnd.valve.source.texture\"},\n    \"vtt\"                      => {\"text/vtt\"},\n    \"vtu\"                      => {\"model/vnd.vtu\"},\n    \"vxml\"                     => {\"application/voicexml+xml\"},\n    \"w3d\"                      => {\"application/x-director\"},\n    \"wad\"                      => {\"application/x-doom\", \"application/x-doom-wad\", \"application/x-wii-wad\"},\n    \"wadl\"                     => {\"application/vnd.sun.wadl+xml\"},\n    \"war\"                      => {\"application/java-archive\"},\n    \"wasm\"                     => {\"application/wasm\"},\n    \"wav\"                      => {\"audio/wav\", \"audio/vnd.wave\", \"audio/wave\", \"audio/x-wav\"},\n    \"wax\"                      => {\"application/x-ms-asx\", \"audio/x-ms-asx\", \"audio/x-ms-wax\", \"video/x-ms-wax\", \"video/x-ms-wmx\", \"video/x-ms-wvx\"},\n    \"wb1\"                      => {\"application/x-quattropro\"},\n    \"wb2\"                      => {\"application/x-quattropro\"},\n    \"wb3\"                      => {\"application/x-quattropro\"},\n    \"wbmp\"                     => {\"image/vnd.wap.wbmp\"},\n    \"wbs\"                      => {\"application/vnd.criticaltools.wbs+xml\"},\n    \"wbxml\"                    => {\"application/vnd.wap.wbxml\"},\n    \"wcm\"                      => {\"application/vnd.ms-works\"},\n    \"wdb\"                      => {\"application/vnd.ms-works\"},\n    \"wdp\"                      => {\"image/jxr\", \"image/vnd.ms-photo\"},\n    \"weba\"                     => {\"audio/webm\"},\n    \"webapp\"                   => {\"application/x-web-app-manifest+json\"},\n    \"webm\"                     => {\"video/webm\"},\n    \"webmanifest\"              => {\"application/manifest+json\"},\n    \"webp\"                     => {\"image/webp\"},\n    \"wg\"                       => {\"application/vnd.pmi.widget\"},\n    \"wgsl\"                     => {\"text/wgsl\"},\n    \"wgt\"                      => {\"application/widget\"},\n    \"wif\"                      => {\"application/watcherinfo+xml\"},\n    \"wim\"                      => {\"application/x-ms-wim\"},\n    \"wk1\"                      => {\"application/lotus123\", \"application/vnd.lotus-1-2-3\", \"application/wk1\", \"application/x-123\", \"application/x-lotus123\", \"zz-application/zz-winassoc-123\"},\n    \"wk3\"                      => {\"application/lotus123\", \"application/vnd.lotus-1-2-3\", \"application/wk1\", \"application/x-123\", \"application/x-lotus123\", \"zz-application/zz-winassoc-123\"},\n    \"wk4\"                      => {\"application/lotus123\", \"application/vnd.lotus-1-2-3\", \"application/wk1\", \"application/x-123\", \"application/x-lotus123\", \"zz-application/zz-winassoc-123\"},\n    \"wkdownload\"               => {\"application/x-partial-download\"},\n    \"wks\"                      => {\"application/lotus123\", \"application/vnd.lotus-1-2-3\", \"application/vnd.ms-works\", \"application/wk1\", \"application/x-123\", \"application/x-lotus123\", \"zz-application/zz-winassoc-123\"},\n    \"wm\"                       => {\"video/x-ms-wm\"},\n    \"wma\"                      => {\"audio/x-ms-wma\", \"audio/wma\"},\n    \"wmd\"                      => {\"application/x-ms-wmd\"},\n    \"wmf\"                      => {\"application/wmf\", \"application/x-msmetafile\", \"application/x-wmf\", \"image/wmf\", \"image/x-win-metafile\", \"image/x-wmf\"},\n    \"wml\"                      => {\"text/vnd.wap.wml\"},\n    \"wmlc\"                     => {\"application/vnd.wap.wmlc\"},\n    \"wmls\"                     => {\"text/vnd.wap.wmlscript\"},\n    \"wmlsc\"                    => {\"application/vnd.wap.wmlscriptc\"},\n    \"wmv\"                      => {\"audio/x-ms-wmv\", \"video/x-ms-wmv\"},\n    \"wmx\"                      => {\"application/x-ms-asx\", \"audio/x-ms-asx\", \"video/x-ms-wax\", \"video/x-ms-wmx\", \"video/x-ms-wvx\"},\n    \"wmz\"                      => {\"application/x-ms-wmz\", \"application/x-msmetafile\"},\n    \"woff\"                     => {\"application/font-woff\", \"application/x-font-woff\", \"font/woff\"},\n    \"woff2\"                    => {\"font/woff2\"},\n    \"wp\"                       => {\"application/vnd.wordperfect\", \"application/wordperfect\", \"application/x-wordperfect\"},\n    \"wp4\"                      => {\"application/vnd.wordperfect\", \"application/wordperfect\", \"application/x-wordperfect\"},\n    \"wp5\"                      => {\"application/vnd.wordperfect\", \"application/wordperfect\", \"application/x-wordperfect\"},\n    \"wp6\"                      => {\"application/vnd.wordperfect\", \"application/wordperfect\", \"application/x-wordperfect\"},\n    \"wpd\"                      => {\"application/vnd.wordperfect\", \"application/wordperfect\", \"application/x-wordperfect\"},\n    \"wpg\"                      => {\"application/x-wpg\"},\n    \"wpl\"                      => {\"application/vnd.ms-wpl\"},\n    \"wpp\"                      => {\"application/vnd.wordperfect\", \"application/wordperfect\", \"application/x-wordperfect\"},\n    \"wps\"                      => {\"application/vnd.ms-works\"},\n    \"wqd\"                      => {\"application/vnd.wqd\"},\n    \"wri\"                      => {\"application/x-mswrite\"},\n    \"wrl\"                      => {\"model/vrml\"},\n    \"ws\"                       => {\"application/x-wonderswan-rom\"},\n    \"wsc\"                      => {\"application/x-wonderswan-color-rom\", \"message/vnd.wfa.wsc\"},\n    \"wsdl\"                     => {\"application/wsdl+xml\"},\n    \"wsgi\"                     => {\"text/x-python\"},\n    \"wspolicy\"                 => {\"application/wspolicy+xml\"},\n    \"wtb\"                      => {\"application/vnd.webturbo\"},\n    \"wv\"                       => {\"audio/x-wavpack\"},\n    \"wvc\"                      => {\"audio/x-wavpack-correction\"},\n    \"wvp\"                      => {\"audio/x-wavpack\"},\n    \"wvx\"                      => {\"application/x-ms-asx\", \"audio/x-ms-asx\", \"video/x-ms-wax\", \"video/x-ms-wmx\", \"video/x-ms-wvx\"},\n    \"wwf\"                      => {\"application/wwf\", \"application/x-wwf\"},\n    \"x32\"                      => {\"application/x-authorware-bin\"},\n    \"x3d\"                      => {\"model/x3d+xml\"},\n    \"x3db\"                     => {\"model/x3d+binary\", \"model/x3d+fastinfoset\"},\n    \"x3dbz\"                    => {\"model/x3d+binary\"},\n    \"x3dv\"                     => {\"model/x3d+vrml\", \"model/x3d-vrml\"},\n    \"x3dvz\"                    => {\"model/x3d+vrml\"},\n    \"x3dz\"                     => {\"model/x3d+xml\"},\n    \"x3f\"                      => {\"image/x-sigma-x3f\"},\n    \"x_b\"                      => {\"model/vnd.parasolid.transmit.binary\"},\n    \"x_t\"                      => {\"model/vnd.parasolid.transmit.text\"},\n    \"xac\"                      => {\"application/x-gnucash\"},\n    \"xaml\"                     => {\"application/xaml+xml\"},\n    \"xap\"                      => {\"application/x-silverlight-app\"},\n    \"xar\"                      => {\"application/vnd.xara\", \"application/x-xar\"},\n    \"xav\"                      => {\"application/xcap-att+xml\"},\n    \"xbap\"                     => {\"application/x-ms-xbap\"},\n    \"xbd\"                      => {\"application/vnd.fujixerox.docuworks.binder\"},\n    \"xbel\"                     => {\"application/x-xbel\"},\n    \"xbl\"                      => {\"application/xml\", \"text/xml\"},\n    \"xbm\"                      => {\"image/x-xbitmap\"},\n    \"xca\"                      => {\"application/xcap-caps+xml\"},\n    \"xcf\"                      => {\"image/x-xcf\"},\n    \"xcf.bz2\"                  => {\"image/x-compressed-xcf\"},\n    \"xcf.gz\"                   => {\"image/x-compressed-xcf\"},\n    \"xci\"                      => {\"application/x-nintendo-switch-xci\", \"application/x-nx-xci\"},\n    \"xcs\"                      => {\"application/calendar+xml\"},\n    \"xdcf\"                     => {\"application/vnd.gov.sk.xmldatacontainer+xml\"},\n    \"xdf\"                      => {\"application/mrb-consumer+xml\", \"application/mrb-publish+xml\", \"application/xcap-diff+xml\"},\n    \"xdgapp\"                   => {\"application/vnd.flatpak\", \"application/vnd.xdgapp\"},\n    \"xdm\"                      => {\"application/vnd.syncml.dm+xml\"},\n    \"xdp\"                      => {\"application/vnd.adobe.xdp+xml\"},\n    \"xdssc\"                    => {\"application/dssc+xml\"},\n    \"xdw\"                      => {\"application/vnd.fujixerox.docuworks\"},\n    \"xel\"                      => {\"application/xcap-el+xml\"},\n    \"xenc\"                     => {\"application/xenc+xml\"},\n    \"xer\"                      => {\"application/patch-ops-error+xml\", \"application/xcap-error+xml\"},\n    \"xfdf\"                     => {\"application/vnd.adobe.xfdf\", \"application/xfdf\"},\n    \"xfdl\"                     => {\"application/vnd.xfdl\"},\n    \"xhe\"                      => {\"audio/usac\"},\n    \"xht\"                      => {\"application/xhtml+xml\"},\n    \"xhtm\"                     => {\"application/vnd.pwg-xhtml-print+xml\"},\n    \"xhtml\"                    => {\"application/xhtml+xml\"},\n    \"xhvml\"                    => {\"application/xv+xml\"},\n    \"xi\"                       => {\"audio/x-xi\"},\n    \"xif\"                      => {\"image/vnd.xiff\"},\n    \"xla\"                      => {\"application/msexcel\", \"application/vnd.ms-excel\", \"application/x-msexcel\", \"zz-application/zz-winassoc-xls\"},\n    \"xlam\"                     => {\"application/vnd.ms-excel.addin.macroenabled.12\"},\n    \"xlc\"                      => {\"application/msexcel\", \"application/vnd.ms-excel\", \"application/x-msexcel\", \"zz-application/zz-winassoc-xls\"},\n    \"xld\"                      => {\"application/msexcel\", \"application/vnd.ms-excel\", \"application/x-msexcel\", \"zz-application/zz-winassoc-xls\"},\n    \"xlf\"                      => {\"application/x-xliff\", \"application/x-xliff+xml\", \"application/xliff+xml\"},\n    \"xliff\"                    => {\"application/x-xliff\", \"application/xliff+xml\"},\n    \"xll\"                      => {\"application/msexcel\", \"application/vnd.ms-excel\", \"application/x-msexcel\", \"zz-application/zz-winassoc-xls\"},\n    \"xlm\"                      => {\"application/msexcel\", \"application/vnd.ms-excel\", \"application/x-msexcel\", \"zz-application/zz-winassoc-xls\"},\n    \"xlr\"                      => {\"application/vnd.ms-works\"},\n    \"xls\"                      => {\"application/vnd.ms-excel\", \"application/msexcel\", \"application/x-msexcel\", \"zz-application/zz-winassoc-xls\"},\n    \"xlsb\"                     => {\"application/vnd.ms-excel.sheet.binary.macroenabled.12\"},\n    \"xlsm\"                     => {\"application/vnd.ms-excel.sheet.macroenabled.12\"},\n    \"xlsx\"                     => {\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"},\n    \"xlt\"                      => {\"application/msexcel\", \"application/vnd.ms-excel\", \"application/x-msexcel\", \"zz-application/zz-winassoc-xls\"},\n    \"xltm\"                     => {\"application/vnd.ms-excel.template.macroenabled.12\"},\n    \"xltx\"                     => {\"application/vnd.openxmlformats-officedocument.spreadsheetml.template\"},\n    \"xlw\"                      => {\"application/msexcel\", \"application/vnd.ms-excel\", \"application/x-msexcel\", \"zz-application/zz-winassoc-xls\"},\n    \"xm\"                       => {\"audio/x-xm\", \"audio/xm\"},\n    \"xmf\"                      => {\"audio/x-xmf\", \"audio/xmf\"},\n    \"xmi\"                      => {\"text/x-xmi\"},\n    \"xml\"                      => {\"application/xml\", \"text/xml\"},\n    \"xns\"                      => {\"application/xcap-ns+xml\"},\n    \"xo\"                       => {\"application/vnd.olpc-sugar\"},\n    \"xop\"                      => {\"application/xop+xml\"},\n    \"xpi\"                      => {\"application/x-xpinstall\"},\n    \"xpl\"                      => {\"application/xproc+xml\"},\n    \"xpm\"                      => {\"image/x-xpixmap\", \"image/x-xpm\"},\n    \"xpr\"                      => {\"application/vnd.is-xpr\"},\n    \"xps\"                      => {\"application/vnd.ms-xpsdocument\", \"application/xps\"},\n    \"xpw\"                      => {\"application/vnd.intercon.formnet\"},\n    \"xpx\"                      => {\"application/vnd.intercon.formnet\"},\n    \"xsd\"                      => {\"application/xml\", \"text/xml\"},\n    \"xsf\"                      => {\"application/prs.xsf+xml\"},\n    \"xsl\"                      => {\"application/xml\", \"application/xslt+xml\"},\n    \"xslfo\"                    => {\"text/x-xslfo\"},\n    \"xslt\"                     => {\"application/xslt+xml\"},\n    \"xsm\"                      => {\"application/vnd.syncml+xml\"},\n    \"xspf\"                     => {\"application/x-xspf+xml\", \"application/xspf+xml\"},\n    \"xul\"                      => {\"application/vnd.mozilla.xul+xml\"},\n    \"xvm\"                      => {\"application/xv+xml\"},\n    \"xvml\"                     => {\"application/xv+xml\"},\n    \"xwd\"                      => {\"image/x-xwindowdump\"},\n    \"xyz\"                      => {\"chemical/x-xyz\"},\n    \"xyze\"                     => {\"image/vnd.radiance\"},\n    \"xz\"                       => {\"application/x-xz\"},\n    \"yaml\"                     => {\"application/yaml\", \"application/x-yaml\", \"text/x-yaml\", \"text/yaml\"},\n    \"yang\"                     => {\"application/yang\"},\n    \"yin\"                      => {\"application/yin+xml\"},\n    \"yml\"                      => {\"application/yaml\", \"application/x-yaml\", \"text/x-yaml\", \"text/yaml\"},\n    \"ymp\"                      => {\"text/x-suse-ymp\"},\n    \"yt\"                       => {\"application/vnd.youtube.yt\", \"video/vnd.youtube.yt\"},\n    \"z1\"                       => {\"application/x-zmachine\"},\n    \"z2\"                       => {\"application/x-zmachine\"},\n    \"z3\"                       => {\"application/x-zmachine\"},\n    \"z4\"                       => {\"application/x-zmachine\"},\n    \"z5\"                       => {\"application/x-zmachine\"},\n    \"z6\"                       => {\"application/x-zmachine\"},\n    \"z64\"                      => {\"application/x-n64-rom\"},\n    \"z7\"                       => {\"application/x-zmachine\"},\n    \"z8\"                       => {\"application/x-zmachine\"},\n    \"zabw\"                     => {\"application/x-abiword\"},\n    \"zaz\"                      => {\"application/vnd.zzazz.deck+xml\"},\n    \"zim\"                      => {\"application/x-openzim\"},\n    \"zip\"                      => {\"application/zip\", \"application/x-zip\", \"application/x-zip-compressed\"},\n    \"zipx\"                     => {\"application/x-zip\", \"application/x-zip-compressed\", \"application/zip\"},\n    \"zir\"                      => {\"application/vnd.zul\"},\n    \"zirz\"                     => {\"application/vnd.zul\"},\n    \"zmm\"                      => {\"application/vnd.handheld-entertainment+xml\"},\n    \"zoo\"                      => {\"application/x-zoo\"},\n    \"zpaq\"                     => {\"application/x-zpaq\"},\n    \"zsav\"                     => {\"application/x-spss-sav\", \"application/x-spss-savefile\"},\n    \"zst\"                      => {\"application/zstd\"},\n    \"zz\"                       => {\"application/zlib\"},\n  }\nend\n"
  },
  {
    "path": "src/components/mime/src/types.cr",
    "content": "require \"./types/data\"\nrequire \"./types_interface\"\n\n# Default implementation of `AMIME::TypesInterface`.\n#\n# Also supports guessing a MIME type based on a given file path.\n# Custom guessers can be registered via the `#register_guesser` method.\n# Custom guessers are always called before any default ones.\n#\n# ```\n# mime_types = AMIME::Types.new\n#\n# mime_types.mime_types \"png\"                     # => {\"image/png\", \"image/apng\", \"image/vnd.mozilla.apng\"}\n# mime_types.extensions \"image/png\"               # => {\"png\"}\n# mime_types.guess_mime_type \"/path/to/image.png\" # => \"image/png\"\n# ```\nclass Athena::MIME::Types\n  include Athena::MIME::TypesInterface\n\n  # :nodoc:\n  #\n  # Key: MIME Type, Value: Array of extensions\n  alias Map = Hash(String, Array(String))\n\n  # Returns/sets the default singleton instance.\n  class_property default : self { new }\n\n  @extensions = Map.new { |hash, key| hash[key] = [] of String }\n  @mime_types = Map.new { |hash, key| hash[key] = [] of String }\n  @guessers : Array(AMIME::TypesGuesserInterface) = [] of AMIME::TypesGuesserInterface\n\n  def initialize(map : Hash(String, Enumerable(String)) = Map.new)\n    map.each do |mime_type, extensions|\n      @extensions[mime_type] = extensions.to_a\n\n      extensions.each do |ext|\n        @mime_types[ext] << mime_type\n      end\n    end\n\n    self.register_guesser AMIME::NativeTypesGuesser.new\n    self.register_guesser AMIME::MagicTypesGuesser.new\n  end\n\n  # Registers the provided *guesser*.\n  # The last registered guesser is preferred over previously registered ones.\n  def register_guesser(guesser : AMIME::TypesGuesserInterface) : Nil\n    @guessers.unshift guesser\n  end\n\n  # :inherit:\n  def extensions(for mime_type : String) : Enumerable(String)\n    extensions = @extensions[mime_type]? || @extensions[lower_case_mime_type = mime_type.downcase]?\n\n    extensions || MAP[mime_type]? || MAP[lower_case_mime_type || mime_type.downcase]? || [] of String\n  end\n\n  # :inherit:\n  def mime_types(for extension : String) : Enumerable(String)\n    mime_types = @mime_types[extension]? || @mime_types[lower_case_extension = extension.downcase]?\n\n    mime_types || REVERSE_MAP[extension]? || REVERSE_MAP[lower_case_extension || extension.downcase]? || [] of String\n  end\n\n  # :inherit:\n  def supported? : Bool\n    @guessers.any? &.supported?\n  end\n\n  # :inherit:\n  def guess_mime_type(path : String | Path) : String?\n    @guessers.each do |guesser|\n      next unless guesser.supported?\n\n      if guess = guesser.guess_mime_type path\n        return guess\n      end\n    end\n\n    unless self.supported?\n      raise AMIME::Exception::Logic.new \"Unable to guess the MIME type as no guessers are available.\"\n    end\n\n    nil\n  end\nend\n"
  },
  {
    "path": "src/components/mime/src/types_guesser_interface.cr",
    "content": "# Represents a type responsible for guessing the MIME type of a file.\nmodule Athena::MIME::TypesGuesserInterface\n  # Returns `true` if this guesser is supported, otherwise `false`.\n  #\n  # The value may be cached on the class level.\n  abstract def supported? : Bool\n\n  # Returns the guessed MIME type for the file at the provided *path*,\n  # or `nil` if it could not be determined.\n  #\n  # How exactly the MIME type is determined is up to each individual implementation.\n  #\n  # ```\n  # guesser.guess_mime_type \"/path/to/image.png\" # => \"image/png\"\n  # ```\n  abstract def guess_mime_type(path : String | Path) : String?\nend\n"
  },
  {
    "path": "src/components/mime/src/types_interface.cr",
    "content": "require \"./types_guesser_interface\"\n\n# Represents a type responsible for managing MIME types and file extensions.\nmodule Athena::MIME::TypesInterface\n  include Athena::MIME::TypesGuesserInterface\n\n  # Returns the valid file extensions for the provided *mime_type* in decreasing order of preference.\n  #\n  # ```\n  # types.extensions \"image/png\" # => {\"png\"}\n  # ```\n  abstract def extensions(for mime_type : String) : Enumerable(String)\n\n  # Returns the valid MIME types for the provided *extension* in decreasing order of preference.\n  #\n  # ```\n  # types.mime_types \"png\" # => {\"image/png\", \"image/apng\", \"image/vnd.mozilla.apng\"}\n  # ```\n  abstract def mime_types(for extension : String) : Enumerable(String)\nend\n"
  },
  {
    "path": "src/components/negotiation/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/negotiation/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/negotiation/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.2.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n- Use lowercase `utf-8` within header values ([#417]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.13.0` ([#428]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/negotiation/releases/tag/v0.2.0\n[#417]: https://github.com/athena-framework/athena/pull/417\n[#428]: https://github.com/athena-framework/athena/pull/428\n\n## [0.1.5] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.1.5]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.5\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.1.4] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.1.4]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.4\n\n## [0.1.3] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.3\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.1.2] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Added\n\n- Add `VERSION` constant to `Athena::Negotiation` namespace ([#166]) (George Dietrich)\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Correct the shard version in `README.md` ([#6]) (syeopite)\n\n[0.1.2]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.2\n[#6]: https://github.com/athena-framework/negotiation/pull/6\n[#166]: https://github.com/athena-framework/athena/pull/166\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.1] - 2021-02-04\n\n### Changed\n\n- Migrate documentation to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#4]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.1\n[#4]: https://github.com/athena-framework/negotiation/pull/4\n\n## [0.1.0] - 2020-12-24\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/negotiation/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/negotiation/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020 George Dietrich\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": "src/components/negotiation/README.md",
    "content": "# Negotiation\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/negotiation.svg)](https://github.com/athena-framework/negotiation/releases)\n\nFramework agnostic [content negotiation](https://tools.ietf.org/html/rfc7231#section-5.3) library based on [willdurand/Negotiation](https://github.com/willdurand/Negotiation).\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Negotiation).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/negotiation/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.2.0\n\n### Normalization of Exception types\n\nThe namespace exception types live in has changed from `ANG::Exceptions` to `ANG::Exception`.\nAny usages of `negotiation` exception types will need to be updated.\n\nIf using a `rescue` statement with a parent exception type, either from the `console` component or Crystal stdlib, double check it to ensure it'll still rescue what you are expecting it will.\n"
  },
  {
    "path": "src/components/negotiation/docs/README.md",
    "content": "The [Athena::Negotiation](/Negotiation/) component allows an application to support [content negotiation](https://tools.ietf.org/html/rfc7231#section-5.3).\nThe component has no dependencies and is framework agnostic; supporting various negotiators.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-negotiation:\n    github: athena-framework/negotiation\n    version: ~> 0.2.0\n```\n\n## Usage\n\nThe main type of [Athena::Negotiation](/Negotiation/) is [ANG::AbstractNegotiator](/Negotiation/AbstractNegotiator/) which is used to implement negotiators for each `Accept*` header.\n`Athena::Negotiation` exposes class level getters for each negotiator; that return a lazily initialized singleton instance.\nEach negotiator exposes two methods: [ANG::AbstractNegotiator#best](</Negotiation/AbstractNegotiator/#Athena::Negotiation::AbstractNegotiator#best(header,priorities,strict)>) and [ANG::AbstractNegotiator#ordered_elements](</Negotiation/AbstractNegotiator/#Athena::Negotiation::AbstractNegotiator#ordered_elements(header)>).\n\n### Media Type\n\n```crystal\nnegotiator = ANG.negotiator\n\naccept_header = \"text/html, application/xhtml+xml, application/xml;q=0.9\"\npriorities = [\"text/html; charset=utf-8\", \"application/json\", \"application/xml;q=0.5\"]\n\naccept = negotiator.best(accept_header, priorities).not_nil!\n\naccept.media_range # => \"text/html\"\naccept.parameters  # => {\"charset\" => \"utf-8\"}\n```\n\nThe [ANG::Negotiator](/Negotiation/Negotiator/) type returns an [ANG::Accept](/Negotiation/Accept/), or `nil` if negotiating the best media type has failed.\n\n### Character Set\n\n```crystal\nnegotiator = ANG.charset_negotiator\n\naccept_header = \"ISO-8859-1, utf-8; q=0.9\"\npriorities = [\"iso-8859-1;q=0.3\", \"utf-8;q=0.9\", \"utf-16;q=1.0\"]\n\naccept = negotiator.best(accept_header, priorities).not_nil!\n\naccept.charset # => \"utf-8\"\naccept.quality # => 0.9\n```\n\nThe [ANG::CharsetNegotiator](/Negotiation/CharsetNegotiator/) type returns an [ANG::AcceptCharset](/Negotiation/AcceptCharset/), or `nil` if negotiating the best character set has failed.\n\n### Encoding\n\n```crystal\nnegotiator = ANG.encoding_negotiator\n\naccept_header = \"gzip;q=1.0, identity; q=0.5, *;q=0\"\npriorities = [\"gzip\", \"foo\"]\n\naccept = negotiator.best(accept_header, priorities).not_nil!\n\naccept.coding # => \"gzip\"\n```\n\nThe [ANG::EncodingNegotiator](/Negotiation/EncodingNegotiator/) type returns an [ANG::AcceptEncoding](/Negotiation/AcceptEncoding/), or `nil` if negotiating the best encoding has failed.\n\n### Language\n\n```crystal\nnegotiator = ANG.language_negotiator\n\naccept_header = \"en; q=0.1, fr; q=0.4, zh-Hans-CN; q=0.9, de; q=0.2\"\npriorities = [\"de\", \"zh-Hans-CN\", \"en\"]\n\naccept = negotiator.best(accept_header, priorities).not_nil!\n\naccept.language # => \"zh\"\naccept.region   # => \"cn\"\naccept.script   # => \"hans\"\n```\n\nThe [ANG::LanguageNegotiator](/Negotiation/LanguageNegotiator/) type returns an [ANG::AcceptLanguage](/Negotiation/AcceptLanguage/), or `nil` if negotiating the best language has failed.\n"
  },
  {
    "path": "src/components/negotiation/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Negotiation\nsite_url: https://athenaframework.org/Negotiation/\nrepo_url: https://github.com/athena-framework/negotiation\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-negotiation/src/athena-negotiation.cr\n          source_locations:\n            lib/athena-negotiation: https://github.com/athena-framework/negotiation/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/negotiation/shard.yml",
    "content": "name: athena-negotiation\n\nversion: 0.2.0\n\ncrystal: ~> 1.13\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/negotiation\n\ndocumentation: https://athenaframework.org/Negotiation\n\ndescription: |\n  Framework agnostic content negotiation library.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n"
  },
  {
    "path": "src/components/negotiation/spec/accept_language_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct AcceptLanguageTest < ASPEC::TestCase\n  @[DataProvider(\"accept_value_data_provider\")]\n  def test_accept_value(header : String?, expected : String?) : Nil\n    ANG::AcceptLanguage.new(header).accept_value.should eq expected\n  end\n\n  def accept_value_data_provider : Tuple\n    {\n      {\"en;q=0.7\", \"en\"},\n      {\"en-GB;q=0.8\", \"en-gb\"},\n      {\"da\", \"da\"},\n      {\"en-gb;q=0.8\", \"en-gb\"},\n      {\"es;q=0.7\", \"es\"},\n      {\"fr ; q= 0.1\", \"fr\"},\n    }\n  end\n\n  @[DataProvider(\"header_data_provider\")]\n  def test_get_value(header : String?, expected : String?) : Nil\n    ANG::AcceptLanguage.new(header).header.should eq expected\n  end\n\n  def header_data_provider : Tuple\n    {\n      {\"en;q=0.7\", \"en;q=0.7\"},\n      {\"en-GB;q=0.8\", \"en-GB;q=0.8\"},\n    }\n  end\n\n  @[TestWith(\n    {\"en;q=0.7\", \"en\"},\n    {\"en-GB;q=0.8\", \"en-gb\"},\n    {\"zh-Hans-CN;q=0.8\", \"zh-hans-cn\"},\n  )]\n  def test_language_range(header : String, expected : String) : Nil\n    ANG::AcceptLanguage.new(header).language_range.should eq expected\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/spec/accept_match_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct AcceptMatchTest < ASPEC::TestCase\n  @[DataProvider(\"compare_data_provider\")]\n  def test_compare(match1 : ANG::AcceptMatch, match2 : ANG::AcceptMatch, expected : Int32) : Nil\n    (match1 <=> match2).should eq expected\n  end\n\n  def compare_data_provider : Tuple\n    {\n      {ANG::AcceptMatch.new(1.0, 110, 1), ANG::AcceptMatch.new(1.0, 111, 1), 0},\n      {ANG::AcceptMatch.new(0.1, 10, 1), ANG::AcceptMatch.new(0.1, 10, 2), -1},\n      {ANG::AcceptMatch.new(0.5, 110, 5), ANG::AcceptMatch.new(0.5, 11, 4), 1},\n      {ANG::AcceptMatch.new(0.4, 110, 1), ANG::AcceptMatch.new(0.6, 111, 3), 1},\n      {ANG::AcceptMatch.new(0.6, 110, 1), ANG::AcceptMatch.new(0.4, 111, 3), -1},\n    }\n  end\n\n  @[DataProvider(\"reduce_data_provider\")]\n  def test_reduce(matches : Hash(Int32, ANG::AcceptMatch), match : ANG::AcceptMatch, expected : Hash(Int32, ANG::AcceptMatch)) : Nil\n    ANG::AcceptMatch.reduce(matches, match).should eq expected\n  end\n\n  def reduce_data_provider : Tuple\n    {\n      {\n        {1 => ANG::AcceptMatch.new(1.0, 10, 1)},\n        ANG::AcceptMatch.new(0.5, 111, 1),\n        {1 => ANG::AcceptMatch.new(0.5, 111, 1)},\n      },\n      {\n        {1 => ANG::AcceptMatch.new(1.0, 110, 1)},\n        ANG::AcceptMatch.new(0.5, 11, 1),\n        {1 => ANG::AcceptMatch.new(1.0, 110, 1)},\n      },\n      {\n        {0 => ANG::AcceptMatch.new(1.0, 10, 1)},\n        ANG::AcceptMatch.new(0.5, 111, 1),\n        {0 => ANG::AcceptMatch.new(1.0, 10, 1), 1 => ANG::AcceptMatch.new(0.5, 111, 1)},\n      },\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/spec/accept_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct AcceptTest < ASPEC::TestCase\n  def test_parameters : Nil\n    ANG::Accept.new(\"foo/bar; q=1; hello=world\").parameters[\"hello\"]?.should eq \"world\"\n  end\n\n  @[DataProvider(\"normalized_header_data_provider\")]\n  def test_normalized_header(header : String, expected : String) : Nil\n    ANG::Accept.new(header).normalized_header.should eq expected\n  end\n\n  def normalized_header_data_provider : Tuple\n    {\n      {\"text/html  ; z=y; a  = b; c=d\", \"text/html; a=b; c=d; z=y\"},\n      {\"application/pdf; q=1; param=p\", \"application/pdf; param=p\"},\n    }\n  end\n\n  @[DataProvider(\"media_range_data_provider\")]\n  def test_media_range(header : String, expected : String) : Nil\n    ANG::Accept.new(header).media_range.should eq expected\n  end\n\n  def media_range_data_provider : Tuple\n    {\n      {\"text/html;hello=world\", \"text/html\"},\n      {\"application/pdf\", \"application/pdf\"},\n      {\"application/xhtml+xml;q=0.9\", \"application/xhtml+xml\"},\n      {\"text/plain; q=0.5\", \"text/plain\"},\n      {\"text/html;level=2;q=0.4\", \"text/html\"},\n      {\"text/html ; level = 2   ; q = 0.4\", \"text/html\"},\n      {\"text/*\", \"text/*\"},\n      {\"text/* ;q=1 ;level=2\", \"text/*\"},\n      {\"*/*\", \"*/*\"},\n      {\"*\", \"*/*\"},\n      {\"*/* ; param=555\", \"*/*\"},\n      {\"* ; param=555\", \"*/*\"},\n      {\"TEXT/hTmL;leVel=2; Q=0.4\", \"text/html\"},\n    }\n  end\n\n  @[DataProvider(\"header_data_provider\")]\n  def test_accept_value(header : String, expected : String) : Nil\n    ANG::Accept.new(header).header.should eq expected\n  end\n\n  def header_data_provider : Tuple\n    {\n      {\"text/html;hello=world  ;q=0.5\", \"text/html;hello=world  ;q=0.5\"},\n      {\"application/pdf\", \"application/pdf\"},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/spec/base_accept_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate struct MockAccept < ANG::BaseAccept; end\n\nstruct BaseAcceptTest < ASPEC::TestCase\n  @[DataProvider(\"build_parameters_data_provider\")]\n  def test_build_parameters_string(header : String, expected : String) : Nil\n    MockAccept.new(header).normalized_header.should eq expected\n  end\n\n  def build_parameters_data_provider : Tuple\n    {\n      {\"media/type; xxx = 1.0;level=2;foo=bar\", \"media/type; foo=bar; level=2; xxx=1.0\"},\n    }\n  end\n\n  @[DataProvider(\"parameters_data_provider\")]\n  def test_parse_parameters(header : String, expected_parameters : Hash(String, String)) : Nil\n    accept = MockAccept.new header\n    parameters = accept.parameters\n\n    # TODO: Can this be improved?\n    if header.includes? 'q'\n      parameters[\"q\"] = accept.quality.to_s\n    end\n\n    expected_parameters.size.should eq parameters.size\n\n    expected_parameters.each do |k, v|\n      parameters.has_key?(k).should be_true\n      parameters[k].should eq v\n    end\n  end\n\n  def parameters_data_provider : Tuple\n    {\n      {\n        \"application/json ;q=1.0; level=2;foo= bar\",\n        {\n          \"q\"     => \"1.0\",\n          \"level\" => \"2\",\n          \"foo\"   => \"bar\",\n        },\n      },\n      {\n        \"application/json ;q = 1.0; level = 2;     FOO  = bAr\",\n        {\n          \"q\"     => \"1.0\",\n          \"level\" => \"2\",\n          \"foo\"   => \"bAr\",\n        },\n      },\n      {\n        \"application/json;q=1.0\",\n        {\n          \"q\" => \"1.0\",\n        },\n      },\n      {\n        \"application/json;foo\",\n        {} of String => String,\n      },\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/spec/charset_negotiator_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct CharsetNegotiatorTest < NegotiatorTestCase\n  @negotiator : ANG::CharsetNegotiator\n\n  def initialize\n    @negotiator = ANG::CharsetNegotiator.new\n  end\n\n  def test_best_unmatched_header : Nil\n    @negotiator.best(\"foo, bar, yo\", {\"baz\"}).should be_nil\n  end\n\n  def test_best_ignores_missing_content : Nil\n    accept = @negotiator.best \"en; q=0.1, fr; q=0.4, bu; q=1.0\", {\"en\", \"fr\"}\n\n    accept = accept.should_not be_nil\n    accept.should be_a ANG::AcceptCharset\n    accept.charset.should eq \"fr\"\n  end\n\n  def test_best_respects_priorities : Nil\n    accept = @negotiator.best \"foo, bar, yo\", {\"yo\"}\n    accept = accept.should_not be_nil\n    accept.should be_a ANG::AcceptCharset\n    accept.charset.should eq \"yo\"\n  end\n\n  def test_best_respects_quality : Nil\n    accept = @negotiator.best \"utf-8;q=0.5,iso-8859-1\", {\"iso-8859-1;q=0.3\", \"utf-8;q=0.9\", \"utf-16;q=1.0\"}\n    accept = accept.should_not be_nil\n    accept.should be_a ANG::AcceptCharset\n    accept.charset.should eq \"utf-8\"\n  end\n\n  @[DataProvider(\"best_data_provider\")]\n  def test_best(header : String, priorities : Indexable(String), expected : String?) : Nil\n    accept = @negotiator.best header, priorities\n\n    if accept.nil?\n      expected.should be_nil\n    else\n      accept.should be_a ANG::AcceptCharset\n      accept.header.should eq expected\n    end\n  end\n\n  def best_data_provider : Tuple\n    php_pear_charset = \"ISO-8859-1, Big5;q=0.6,utf-8;q=0.7, *;q=0.5\"\n    php_pear_charset2 = \"ISO-8859-1, Big5;q=0.6,utf-8;q=0.7\"\n\n    {\n      {php_pear_charset, {\"utf-8\", \"big5\", \"iso-8859-1\", \"shift-jis\"}, \"iso-8859-1\"},\n      {php_pear_charset, {\"utf-8\", \"big5\", \"shift-jis\"}, \"utf-8\"},\n      {php_pear_charset, {\"Big5\", \"shift-jis\"}, \"Big5\"},\n      {php_pear_charset, {\"shift-jis\"}, \"shift-jis\"},\n      {php_pear_charset2, {\"utf-8\", \"big5\", \"iso-8859-1\", \"shift-jis\"}, \"iso-8859-1\"},\n      {php_pear_charset2, {\"utf-8\", \"big5\", \"shift-jis\"}, \"utf-8\"},\n      {php_pear_charset2, {\"Big5\", \"shift-jis\"}, \"Big5\"},\n      {\"utf-8;q=0.6,iso-8859-5;q=0.9\", {\"iso-8859-5\", \"utf-8\"}, \"iso-8859-5\"},\n      {\"en, *;q=0.9\", {\"fr\"}, \"fr\"},\n      # Quality of source factors\n      {php_pear_charset, {\"iso-8859-1;q=0.5\", \"utf-8\", \"utf-16;q=1.0\"}, \"utf-8\"},\n      {php_pear_charset, {\"iso-8859-1;q=0.8\", \"utf-8\", \"utf-16;q=1.0\"}, \"iso-8859-1;q=0.8\"},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/spec/encoding_negotiator_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct EncodingNegotiatorTest < NegotiatorTestCase\n  @negotiator : ANG::EncodingNegotiator\n\n  def initialize\n    @negotiator = ANG::EncodingNegotiator.new\n  end\n\n  def test_best_unmatched_header : Nil\n    @negotiator.best(\"foo, bar, yo\", {\"baz\"}).should be_nil\n  end\n\n  def test_best_respects_quality : Nil\n    accept = @negotiator.best \"gzip;q=0.7,identity\", {\"identity;q=0.5\", \"gzip;q=0.9\"}\n    accept = accept.should_not be_nil\n    accept.should be_a ANG::AcceptEncoding\n    accept.coding.should eq \"gzip\"\n  end\n\n  @[DataProvider(\"best_data_provider\")]\n  def test_best(header : String, priorities : Indexable(String), expected : String?) : Nil\n    accept = @negotiator.best header, priorities\n\n    if accept.nil?\n      expected.should be_nil\n    else\n      accept.should be_a ANG::AcceptEncoding\n      accept.header.should eq expected\n    end\n  end\n\n  def best_data_provider : Tuple\n    {\n      {\"gzip;q=1.0, identity; q=0.5, *;q=0\", {\"identity\"}, \"identity\"},\n      {\"gzip;q=0.5, identity; q=0.5, *;q=0.7\", {\"bzip\", \"foo\"}, \"bzip\"},\n      {\"gzip;q=0.7, identity; q=0.5, *;q=0.7\", {\"gzip\", \"foo\"}, \"gzip\"},\n      # Quality of source factors\n      {\"gzip;q=0.7,identity\", {\"identity;q=0.5\", \"gzip;q=0.9\"}, \"gzip;q=0.9\"},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/spec/language_negotiator_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct LanguageNegotiatorTest < NegotiatorTestCase\n  @negotiator : ANG::LanguageNegotiator\n\n  def initialize\n    @negotiator = ANG::LanguageNegotiator.new\n  end\n\n  def test_best_respects_quality : Nil\n    accept = @negotiator.best \"en;q=0.5,de\", {\"de;q=0.3\", \"en;q=0.9\"}\n    accept = accept.should_not be_nil\n    accept.should be_a ANG::AcceptLanguage\n    accept.language.should eq \"en\"\n  end\n\n  @[DataProvider(\"best_data_provider\")]\n  def test_best(header : String, priorities : Indexable(String), expected : String?) : Nil\n    accept = @negotiator.best header, priorities\n\n    if accept.nil?\n      expected.should be_nil\n    else\n      accept.should be_a ANG::AcceptLanguage\n      accept.header.should eq expected\n    end\n  end\n\n  def best_data_provider : Tuple\n    {\n      {\"en, de\", {\"fr\"}, nil},\n      {\"foo, bar, yo\", {\"baz\", \"biz\"}, nil},\n      {\"fr-FR, en;q=0.8\", {\"en-US\", \"de-DE\"}, \"en-US\"},\n      {\"en, *;q=0.9\", {\"fr\"}, \"fr\"},\n      {\"foo, bar, yo\", {\"yo\"}, \"yo\"},\n      {\"en; q=0.1, fr; q=0.4, bu; q=1.0\", {\"en\", \"fr\"}, \"fr\"},\n      {\"en; q=0.1, fr; q=0.4, fu; q=0.9, de; q=0.2\", {\"en\", \"fu\"}, \"fu\"},\n      {\"fr, zh-Hans-CN;q=0.3\", {\"fr\"}, \"fr\"},\n      # Quality of source factors\n      {\"en;q=0.5,de\", {\"de;q=0.3\", \"en;q=0.9\"}, \"en;q=0.9\"},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/spec/negotiator_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct NegotiatorTest < NegotiatorTestCase\n  @negotiator : ANG::Negotiator\n\n  def initialize\n    @negotiator = ANG::Negotiator.new\n  end\n\n  def test_best_respects_quality : Nil\n    accept = @negotiator.best \"text/html,text/*;q=0.7\", {\"text/html;q=0.5\", \"text/plain;q=0.9\"}\n    accept = accept.should_not be_nil\n    accept.should be_a ANG::Accept\n    accept.media_range.should eq \"text/plain\"\n  end\n\n  def test_best_invalid_unstrict\n    @negotiator.best(\"/qwer\", {\"foo/bar\"}, false).should be_nil\n  end\n\n  def test_invalid_media_type : Nil\n    ex = expect_raises ANG::Exception::InvalidMediaType, \"Invalid media type: '/qwer'.\" do\n      @negotiator.best \"foo/bar\", {\"/qwer\"}\n    end\n\n    ex.media_range.should eq \"/qwer\"\n  end\n\n  @[DataProvider(\"best_data_provider\")]\n  def test_best(header : String, priorities : Indexable(String), expected : Tuple(String, Hash(String, String) | Nil) | Nil) : Nil\n    begin\n      accept_header = @negotiator.best header, priorities\n    rescue ex\n      ex.should eq expected\n\n      return\n    end\n\n    if accept_header.nil?\n      expected.should be_nil\n\n      return\n    end\n\n    accept_header.should be_a ANG::Accept\n\n    expected = expected.should_not be_nil\n\n    accept_header.media_range.should eq expected[0]\n    accept_header.parameters.should eq(expected[1] || Hash(String, String).new)\n  end\n\n  def best_data_provider : Tuple\n    rfc_header = \"text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5\"\n    php_pear_header = \"text/html,application/xhtml+xml,application/xml;q=0.9,text/*;q=0.7,*/*,image/gif; q=0.8, image/jpeg; q=0.6, image/*\"\n\n    {\n      {\"/qwer\", {\"f/g\"}, nil},\n      {\"text/html\", {\"application/rss\"}, nil},\n      {rfc_header, {\"text/html;q=0.4\", \"text/plain\"}, {\"text/plain\", nil}},\n\n      # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html.\n      {rfc_header, {\"text/html;level=1\"}, {\"text/html\", {\"level\" => \"1\"}}},\n      {rfc_header, {\"text/html\"}, {\"text/html\", nil}},\n      {rfc_header, {\"image/jpeg\"}, {\"image/jpeg\", nil}},\n      {rfc_header, {\"text/html;level=2\"}, {\"text/html\", {\"level\" => \"2\"}}},\n      {rfc_header, {\"text/html;level=3\"}, {\"text/html\", {\"level\" => \"3\"}}},\n\n      {\"text/*;q=0.7, text/html;q=0.3, */*;q=0.5, image/png;q=0.4\", {\"text/html\", \"image/png\"}, {\"image/png\", nil}},\n      {\"image/png;q=0.1, text/plain, audio/ogg;q=0.9\", {\"image/png\", \"text/plain\", \"audio/ogg\"}, {\"text/plain\", nil}},\n      {\"image/png, text/plain, audio/ogg\", {\"baz/asdf\"}, nil},\n      {\"image/png, text/plain, audio/ogg\", {\"audio/ogg\"}, {\"audio/ogg\", nil}},\n      {\"image/png, text/plain, audio/ogg\", {\"YO/SuP\"}, nil},\n      {\"text/html; charset=utf-8, application/pdf\", {\"text/html; charset=utf-8\"}, {\"text/html\", {\"charset\" => \"utf-8\"}}},\n      {\"text/html; charset=utf-8, application/pdf\", {\"text/html\"}, nil},\n      {\"text/html, application/pdf\", {\"text/html; charset=utf-8\"}, {\"text/html\", {\"charset\" => \"utf-8\"}}},\n\n      # PHP\"s PEAR HTTP2 assertions I took from the other lib.\n      {php_pear_header, {\"image/gif\", \"image/png\", \"application/xhtml+xml\", \"application/xml\", \"text/html\", \"image/jpeg\", \"text/plain\"}, {\"image/png\", nil}},\n      {php_pear_header, {\"image/gif\", \"application/xhtml+xml\", \"application/xml\", \"image/jpeg\", \"text/plain\"}, {\"application/xhtml+xml\", nil}},\n      {php_pear_header, {\"image/gif\", \"application/xml\", \"image/jpeg\", \"text/plain\"}, {\"application/xml\", nil}},\n      {php_pear_header, {\"image/gif\", \"image/jpeg\", \"text/plain\"}, {\"image/gif\", nil}},\n      {php_pear_header, {\"text/plain\", \"image/png\", \"image/jpeg\"}, {\"image/png\", nil}},\n      {php_pear_header, {\"image/jpeg\", \"image/gif\"}, {\"image/gif\", nil}},\n      {php_pear_header, {\"image/png\"}, {\"image/png\", nil}},\n      {php_pear_header, {\"audio/midi\"}, {\"audio/midi\", nil}},\n      {\"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\", {\"application/rss+xml\"}, {\"application/rss+xml\", nil}},\n\n      # Case sensitivity\n      {\"text/* ; q=0.3, TEXT/html ;Q=0.7, text/html ; level=1, texT/Html ;leVel = 2 ;q=0.4, */* ; q=0.5\", {\"text/html; level=2\"}, {\"text/html\", {\"level\" => \"2\"}}},\n      {\"text/* ; q=0.3, text/html;Q=0.7, text/html ;level=1, text/html; level=2;q=0.4, */*;q=0.5\", {\"text/HTML; level=3\"}, {\"text/html\", {\"level\" => \"3\"}}},\n\n      # IE8\n      {\"image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, */*\", {\"text/html\", \"application/xhtml+xml\"}, {\"text/html\", nil}},\n\n      # wildcards with `+`\n      {\"application/vnd.api+json\", {\"application/json\", \"application/*+json\"}, {\"application/*+json\", nil}},\n      {\"application/json;q=0.7, application/*+json;q=0.7\", {\"application/hal+json\", \"application/problem+json\"}, {\"application/hal+json\", nil}},\n      {\"application/json;q=0.7, application/problem+*;q=0.7\", {\"application/hal+xml\", \"application/problem+xml\"}, {\"application/problem+xml\", nil}},\n      {php_pear_header, {\"application/*+xml\"}, {\"application/*+xml\", nil}},\n      {\"application/hal+json\", {\"application/ld+json\", \"application/hal+json\", \"application/xml\", \"text/xml\", \"application/json\", \"text/html\"}, {\"application/hal+json\", nil}},\n    }\n  end\n\n  def test_ordered_elements_exception_handling : Nil\n    expect_raises ArgumentError, \"The header string should not be empty.\" do\n      @negotiator.ordered_elements \"\"\n    end\n  end\n\n  @[DataProvider(\"test_ordered_elements_data_provider\")]\n  def test_ordered_elements(header : String, expected : Indexable(String)) : Nil\n    elements = @negotiator.ordered_elements header\n\n    elements.should be_a Array(ANG::Accept)\n\n    expected.each_with_index do |element, idx|\n      elements[idx].should be_a ANG::Accept\n      element.should eq elements[idx].header\n    end\n  end\n\n  def test_ordered_elements_data_provider : Tuple\n    {\n      {\"/qwer\", [] of String},                                                                                                                                                                    # Invalid\n      {\"text/html, text/xml\", {\"text/html\", \"text/xml\"}},                                                                                                                                         # Ordered as given if no quality modifier\n      {\"text/html;q=0.3, text/html;q=0.7\", {\"text/html;q=0.7\", \"text/html;q=0.3\"}},                                                                                                               # Ordered by quality modifier\n      {\"text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5\", {\"text/html;level=1\", \"text/html;q=0.7\", \"*/*;q=0.5\", \"text/html;level=2;q=0.4\", \"text/*;q=0.3\"}}, # Ordered by quality modifier; one without wins\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/spec/negotiator_test_case.cr",
    "content": "abstract struct NegotiatorTestCase < ASPEC::TestCase\n  def test_best_exception_handling : Nil\n    expect_raises ArgumentError, \"priorities should not be empty.\" do\n      @negotiator.best \"foo/bar\", [] of String\n    end\n\n    expect_raises ArgumentError, \"The header string should not be empty.\" do\n      @negotiator.best \"\", {\"text/html\"}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"athena-spec\"\nrequire \"../src/athena-negotiation\"\nrequire \"./negotiator_test_case\"\n\ninclude ASPEC::Methods\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/negotiation/src/abstract_negotiator.cr",
    "content": "# Base negotiator type.  Implements logic common to all negotiators.\nabstract class Athena::Negotiation::AbstractNegotiator(HeaderType)\n  private record OrderKey, quality : Float32, index : Int32, value : String do\n    include Comparable(self)\n\n    def <=>(other : self) : Int32\n      return @index <=> other.index if @quality == other.quality\n      @quality > other.quality ? -1 : 1\n    end\n  end\n\n  # Returns the best `HeaderType` based on the provided *header* value and *priorities*.\n  #\n  # If *strict* is `true`, an `ANG::Exception::Exception` will be raised if the *header* contains an invalid value, otherwise it is ignored.\n  #\n  # See `Athena::Negotiation` for examples.\n  def best(header : String, priorities : Indexable(String), strict : Bool = false) : HeaderType?\n    raise ANG::Exception::InvalidArgument.new \"priorities should not be empty.\" if priorities.empty?\n    raise ANG::Exception::InvalidArgument.new \"The header string should not be empty.\" if header.blank?\n\n    accepted_headers = Array(HeaderType).new\n\n    self.parse_header(header) do |h|\n      accepted_headers << HeaderType.new h\n    rescue ex\n      raise ex if strict\n    end\n\n    accepted_priorities = priorities.map { |p| HeaderType.new p }\n\n    matches = self.find_matches accepted_headers, accepted_priorities\n\n    specific_matches = matches.reduce({} of Int32 => ANG::AcceptMatch) do |acc, match|\n      ANG::AcceptMatch.reduce acc, match\n    end.values\n\n    specific_matches.sort!\n\n    match = specific_matches.shift?\n\n    match.nil? ? nil : accepted_priorities[match.index]\n  end\n\n  # Returns an array of `HeaderType` that the provided *header* allows, ordered so that the `#best` match is first.\n  #\n  # ```\n  # header = \"text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5\"\n  #\n  # ordered_elements = ANG.negotiator.ordered_elements header\n  #\n  # ordered_elements[0].media_range # => \"text/html\"\n  # ordered_elements[1].media_range # => \"text/html\"\n  # ordered_elements[2].media_range # => \"*/*\"\n  # ordered_elements[3].media_range # => \"text/html\"\n  # ordered_elements[4].media_range # => \"text/*\"\n  # ```\n  def ordered_elements(header : String) : Array(HeaderType)\n    raise ANG::Exception::InvalidArgument.new \"The header string should not be empty.\" if header.blank?\n\n    elements = Array(HeaderType).new\n    order_keys = Array(OrderKey).new\n\n    idx = 0\n    self.parse_header(header) do |h|\n      element = HeaderType.new h\n      elements << element\n      order_keys << OrderKey.new element.quality, idx, element.header\n    rescue ex\n      # skip\n    ensure\n      idx += 1\n    end\n\n    order_keys.sort!.map do |ok|\n      elements[ok.index]\n    end\n  end\n\n  protected def match(header : ANG::BaseAccept, priority : ANG::BaseAccept, index : Int32) : ANG::AcceptMatch?\n    accept_value = header.accept_value\n    priority_value = priority.accept_value\n\n    equal = accept_value.downcase == priority_value.downcase\n\n    if equal || accept_value == \"*\"\n      return ANG::AcceptMatch.new header.quality * priority.quality, 1 * (equal ? 1 : 0), index\n    end\n\n    nil\n  end\n\n  private def parse_header(header : String, & : String ->) : Nil\n    header.scan /(?:[^,\\\"]*+(?:\"[^\"]*+\\\")?)+[^,\\\"]*+/ do |match|\n      yield match[0].strip unless match[0].blank?\n    end\n  end\n\n  private def find_matches(headers : Array(HeaderType), priorities : Indexable(HeaderType)) : Array(ANG::AcceptMatch)\n    matches = [] of ANG::AcceptMatch\n\n    priorities.each_with_index do |priority, idx|\n      headers.each do |header|\n        if match = self.match(header, priority, idx)\n          matches << match\n        end\n      end\n    end\n\n    matches\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/accept.cr",
    "content": "require \"./base_accept\"\n\n# Represents an [Accept](https://tools.ietf.org/html/rfc7231#section-5.3.2) header media type.\n#\n# ```\n# accept = ANG::Accept.new \"application/json; q = 0.75; charset = utf-8\"\n#\n# accept.header            # => \"application/json; q = 0.75; charset = utf-8\"\n# accept.normalized_header # => \"application/json; charset=utf-8\"\n# accept.parameters        # => {\"charset\" => \"utf-8\"}\n# accept.quality           # => 0.75\n# accept.type              # => \"application\"\n# accept.sub_type          # => \"json\"\n# ```\nstruct Athena::Negotiation::Accept < Athena::Negotiation::BaseAccept\n  # Returns the type for this `Accept` header.\n  # E.x. if the `#media_range` is `application/json`, the type would be `application`.\n  getter type : String\n\n  # Returns the sub type for this `Accept` header.\n  # E.x. if the `#media_range` is `application/json`, the sub type would be `json`.\n  getter sub_type : String\n\n  def initialize(value : String)\n    super value\n\n    @accept_value = \"*/*\" if @accept_value == \"*\"\n\n    parts = @accept_value.split '/'\n\n    if parts.size != 2 || !parts[0].presence || !parts[1].presence\n      raise ANG::Exception::InvalidMediaType.new @accept_value\n    end\n\n    @type = parts[0]\n    @sub_type = parts[1]\n  end\n\n  # Returns the media range this `Accept` header represents.\n  #\n  # I.e. `#header` minus the `#quality` and `#parameters`.\n  def media_range : String\n    @accept_value\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/accept_charset.cr",
    "content": "require \"./base_accept\"\n\n# Represents an [Accept-Charset](https://tools.ietf.org/html/rfc7231#section-5.3.3) header character set.\n#\n# ```\n# accept = ANG::AcceptCharset.new \"iso-8859-1; q = 0.5; key=value\"\n#\n# accept.header            # => \"iso-8859-1; q = 0.5; key=value\"\n# accept.normalized_header # => \"iso-8859-1; key=value\"\n# accept.parameters        # => {\"key\" => \"value\"}\n# accept.quality           # => 0.5\n# accept.charset           # => \"iso-8859-1\"\n# ```\nstruct Athena::Negotiation::AcceptCharset < Athena::Negotiation::BaseAccept\n  # Returns the character set this `AcceptCharset` header represents.\n  #\n  # I.e. `#header` minus the `#quality` and `#parameters`.\n  def charset : String\n    @accept_value\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/accept_encoding.cr",
    "content": "require \"./base_accept\"\n\n# Represents an [Accept-Encoding](https://tools.ietf.org/html/rfc7231#section-5.3.4) header character set.\n#\n# ```\n# accept = ANG::AcceptEncoding.new \"gzip; q = 0.5; key=value\"\n#\n# accept.header            # => \"gzip; q = 0.5; key=value\"\n# accept.normalized_header # => \"gzip; key=value\"\n# accept.parameters        # => {\"key\" => \"value\"}\n# accept.quality           # => 0.5\n# accept.coding            # => \"gzip\"\n# ```\nstruct Athena::Negotiation::AcceptEncoding < Athena::Negotiation::BaseAccept\n  # Returns the content coding this `AcceptEncoding` header represents.\n  #\n  # I.e. `#header` minus the `#quality` and `#parameters`.\n  def coding : String\n    @accept_value\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/accept_language.cr",
    "content": "require \"./base_accept\"\n\n# Represents an [Accept-Language](https://tools.ietf.org/html/rfc7231#section-5.3.5) header character set.\n#\n# ```\n# accept = ANG::AcceptLanguage.new \"zh-Hans-CN; q = 0.3; key=value\"\n#\n# accept.header            # => \"zh-Hans-CN; q = 0.3; key=value\"\n# accept.normalized_header # => \"zh-Hans-CN; key=value\"\n# accept.parameters        # => {\"key\" => \"value\"}\n# accept.quality           # => 0.3\n# accept.language          # => \"zh\"\n# accept.region            # => \"cn\"\n# accept.script            # => \"hans\"\n# ```\nstruct Athena::Negotiation::AcceptLanguage < Athena::Negotiation::BaseAccept\n  # Returns the language for this `AcceptLanguage` header.\n  # E.x. if the `#language_range` is `zh-Hans-CN`, the language would be `zh`.\n  getter language : String\n\n  # Returns the region, if any, for this `AcceptLanguage` header.\n  # E.x. if the `#language_range` is `zh-Hans-CN`, the region would be `cn`\n  getter region : String? = nil\n\n  # Returns the script, if any, for this `AcceptLanguage` header.\n  # E.x. if the `#language_range` is `zh-Hans-CN`, the script would be `hans`\n  getter script : String? = nil\n\n  def initialize(value : String)\n    super value\n\n    parts = @accept_value.split '-'\n\n    case parts.size\n    when 1\n      @language = parts[0]\n    when 2\n      @language = parts[0]\n      @region = parts[1]\n    when 3\n      @language = parts[0]\n      @script = parts[1]\n      @region = parts[2]\n    else\n      raise ANG::Exception::InvalidLanguage.new @accept_value\n    end\n  end\n\n  # Returns the language range this `AcceptLanguage` header represents.\n  #\n  # I.e. `#header` minus the `#quality` and `#parameters`.\n  def language_range : String\n    @accept_value\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/accept_match.cr",
    "content": "# :nodoc:\nstruct Athena::Negotiation::AcceptMatch\n  include Comparable(self)\n\n  getter quality : Float32\n  getter score : Int32\n  getter index : Int32\n\n  def self.reduce(matches : Hash(Int32, self), match : self) : Hash(Int32, self)\n    if !matches.has_key?(match.index) || matches[match.index].score < match.score\n      matches[match.index] = match\n    end\n\n    matches\n  end\n\n  def initialize(@quality : Float32, @score : Int32, @index : Int32); end\n\n  def <=>(other : self) : Int32\n    if @quality != other.quality\n      return @quality > other.quality ? -1 : 1\n    end\n\n    if @index != other.index\n      return @index > other.index ? 1 : -1\n    end\n\n    0\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/athena-negotiation.cr",
    "content": "require \"./accept\"\nrequire \"./accept_match\"\nrequire \"./accept_charset\"\nrequire \"./accept_encoding\"\nrequire \"./accept_language\"\nrequire \"./charset_negotiator\"\nrequire \"./encoding_negotiator\"\nrequire \"./language_negotiator\"\nrequire \"./negotiator\"\n\nrequire \"./exception/*\"\n\n# Convenience alias to make referencing `Athena::Negotiation` types easier.\nalias ANG = Athena::Negotiation\n\n# The `Athena::Negotiation` component allows an application to support [content negotiation](https://tools.ietf.org/html/rfc7231#section-5.3).\nmodule Athena::Negotiation\n  VERSION = \"0.2.0\"\n\n  # Returns a lazily initialized `ANG::Negotiator` singleton instance.\n  class_getter(negotiator) { ANG::Negotiator.new }\n\n  # Returns a lazily initialized `ANG::CharsetNegotiator` singleton instance.\n  class_getter(charset_negotiator) { ANG::CharsetNegotiator.new }\n\n  # Returns a lazily initialized `ANG::EncodingNegotiator` singleton instance.\n  class_getter(encoding_negotiator) { ANG::EncodingNegotiator.new }\n\n  # Returns a lazily initialized `ANG::LanguageNegotiator` singleton instance.\n  class_getter(language_negotiator) { ANG::LanguageNegotiator.new }\n\n  # Both acts as a namespace for exceptions related to the `Athena::Negotiation` component, as well as a way to check for exceptions from the component.\n  module Exception; end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/base_accept.cr",
    "content": "# Base type for properties/logic all [Accept*](https://tools.ietf.org/html/rfc7231#section-5.3) headers share.\nabstract struct Athena::Negotiation::BaseAccept\n  # Returns the full unaltered header `self` represents.\n  # E.x. `text/html`, `unicode-1-1;q=0.8`, or `zh-Hans-CN`.\n  getter header : String\n\n  # Returns a normalized version of the `#header`, excluding the `#quality` parameter.\n  #\n  # This includes removing extraneous whitespace, and alphabetizing the `#parameters`.\n  getter normalized_header : String\n\n  # Returns any extension parameters included in the header `self` represents.\n  # E.x. `charset=utf-8` or `version=2`.\n  getter parameters : Hash(String, String) = Hash(String, String).new\n\n  # Returns the [quality value](https://tools.ietf.org/html/rfc7231#section-5.3.1) of the header `self` represents.\n  getter quality : Float32 = 1.0\n\n  # Represents the base header value, e.g. `#header` minus the `#quality` and `#parameters`.\n  # This is exposed as a getter on each subtype to have a more descriptive API.\n  protected getter accept_value : String\n\n  def initialize(@header : String)\n    parts = @header.split ';'\n    @accept_value = parts.shift.strip.downcase\n\n    parts.each do |part|\n      part = part.split '='\n\n      # Skip invalid parameters\n      next unless part.size == 2\n\n      @parameters[part[0].strip.downcase] = part[1].strip(\" \\\"\")\n    end\n\n    if quality = @parameters.delete \"q\"\n      # RFC Only allows max of 3 decimal points.\n      @quality = quality.to_f32.round 3\n    end\n\n    @normalized_header = String.build do |io|\n      io << @accept_value\n\n      unless @parameters.empty?\n        io << \"; \"\n        @parameters.keys.sort!.join(io, \"; \") { |k, join_io| join_io << \"#{k}=#{@parameters[k]}\" }\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/charset_negotiator.cr",
    "content": "require \"./abstract_negotiator\"\n\n# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::AcceptCharset` headers.\nclass Athena::Negotiation::CharsetNegotiator < Athena::Negotiation::AbstractNegotiator(Athena::Negotiation::AcceptCharset)\nend\n"
  },
  {
    "path": "src/components/negotiation/src/encoding_negotiator.cr",
    "content": "require \"./abstract_negotiator\"\n\n# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::AcceptEncoding` headers.\nclass Athena::Negotiation::EncodingNegotiator < Athena::Negotiation::AbstractNegotiator(Athena::Negotiation::AcceptEncoding)\nend\n"
  },
  {
    "path": "src/components/negotiation/src/exception/invalid_argument.cr",
    "content": "class Athena::Negotiation::Exception::InvalidArgument < ArgumentError\n  include Athena::Negotiation::Exception\nend\n"
  },
  {
    "path": "src/components/negotiation/src/exception/invalid_language.cr",
    "content": "# Represents an invalid `ANG::AcceptLanguage` header.\nclass Athena::Negotiation::Exception::InvalidLanguage < RuntimeError\n  include Athena::Negotiation::Exception\n\n  # Returns the invalid language code.\n  getter language : String\n\n  def initialize(@language : String, cause : ::Exception? = nil)\n    super \"Invalid language: '#{@language}'.\", cause\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/exception/invalid_media_type.cr",
    "content": "# Represents an invalid `ANG::Accept` header.\nclass Athena::Negotiation::Exception::InvalidMediaType < RuntimeError\n  include Athena::Negotiation::Exception\n\n  # Returns the invalid media range.\n  getter media_range : String\n\n  def initialize(@media_range : String, cause : ::Exception? = nil)\n    super \"Invalid media type: '#{@media_range}'.\", cause\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/language_negotiator.cr",
    "content": "require \"./abstract_negotiator\"\n\n# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::AcceptLanguage` headers.\nclass Athena::Negotiation::LanguageNegotiator < Athena::Negotiation::AbstractNegotiator(Athena::Negotiation::AcceptLanguage)\n  protected def match(accept : ANG::AcceptLanguage, priority : ANG::AcceptLanguage, index : Int32) : ANG::AcceptMatch?\n    accept_base = accept.language\n    priority_base = priority.language\n\n    accept_sub = accept.region\n    priority_sub = priority.region\n\n    base_equal = accept_base.downcase == priority_base.downcase\n    sub_equal = accept_sub.try &.downcase == priority_sub.try &.downcase\n\n    if (accept_base == \"*\" || base_equal) && (accept_sub.nil? || sub_equal)\n      score = 10 * (base_equal ? 1 : 0) + (sub_equal ? 1 : 0)\n\n      return ANG::AcceptMatch.new accept.quality * priority.quality, score, index\n    end\n\n    nil\n  end\nend\n"
  },
  {
    "path": "src/components/negotiation/src/negotiator.cr",
    "content": "require \"./abstract_negotiator\"\n\n# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::Accept` headers.\nclass Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator(Athena::Negotiation::Accept)\n  # TODO: Make this method less complex.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  protected def match(accept : ANG::Accept, priority : ANG::Accept, index : Int32) : ANG::AcceptMatch?\n    accept_type = accept.type\n    priority_type = priority.type\n\n    accept_sub_type = accept.sub_type\n    priority_sub_type = priority.sub_type\n\n    intersection = accept.parameters.each_with_object({} of String => String) do |(k, v), params|\n      priority.parameters.tap do |pp|\n        params[k] = v if pp.has_key?(k) && pp[k] == v\n      end\n    end\n\n    type_equals = accept_type.downcase == priority_type.downcase\n    sub_type_equals = accept_sub_type.downcase == priority_sub_type.downcase\n\n    if (accept_type == \"*\" || type_equals) &&\n       (accept_sub_type == \"*\" || sub_type_equals) &&\n       intersection.size == accept.parameters.size\n      score = 100 * (type_equals ? 1 : 0) + 10 * (sub_type_equals ? 1 : 0) + intersection.size\n\n      return ANG::AcceptMatch.new accept.quality * priority.quality, score, index\n    end\n\n    return nil if !accept_sub_type.includes?('+') || !priority_sub_type.includes?('+')\n\n    accept_sub_type, accept_plus = self.split_sub_type accept_sub_type\n    priority_sub_type, priority_plus = self.split_sub_type priority_sub_type\n\n    if !(accept_type == \"*\" || type_equals) ||\n       !(accept_sub_type == \"*\" || priority_sub_type == \"*\" || accept_plus == \"*\" || priority_plus == \"*\")\n      return nil\n    end\n\n    sub_type_equals = accept_sub_type.downcase == priority_sub_type.downcase\n    plus_equals = accept_plus.downcase == priority_plus.downcase\n\n    if (accept_sub_type == \"*\" || priority_sub_type == \"*\" || sub_type_equals) &&\n       (accept_plus == \"*\" || priority_plus == '*' || plus_equals) &&\n       intersection.size == accept.parameters.size\n      score = 100 * (type_equals ? 1 : 0) + 10 * (sub_type_equals ? 1 : 0) + (plus_equals ? 1 : 0) + intersection.size\n      return ANG::AcceptMatch.new accept.quality * priority.quality, score, index\n    end\n\n    nil\n  end\n\n  private def split_sub_type(sub_type : String) : Array(String)\n    return [sub_type, \"\"] unless sub_type.includes? '+'\n\n    sub_type.split '+', limit: 2\n  end\nend\n"
  },
  {
    "path": "src/components/routing/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/routing/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/routing/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.2.0] - 2026-04-19\n\n### Changed\n\n- **Breaking:** Change `ART::Route#defaults` and matched route parameters return type to `ART::Parameters` ([#652]) (George Dietrich) <!-- blacksmoke16 -->\n- **Breaking:** Loosen the type restriction for the `params` parameter of `ART::Generator::Interface#generate` ([#669]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Allow query-specific parameters within `ART::Generator::URLGenerator` via special `_query` parameter ([#669]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.2.0]: https://github.com/athena-framework/routing/releases/tag/v0.2.0\n[#652]: https://github.com/athena-framework/athena/pull/652\n[#669]: https://github.com/athena-framework/athena/pull/669\n\n## [0.1.12] - 2025-11-01\n\n### Fixed\n\n- Fix Crystal `1.19` incompatibility ([#600]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.1.12]: https://github.com/athena-framework/routing/releases/tag/v0.1.12\n[#600]: https://github.com/athena-framework/athena/pull/600\n\n## [0.1.11] - 2025-09-04\n\n### Fixed\n\n- Fix linker warning due to duplicate `pcre2-8` linkage ([#560]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.1.11]: https://github.com/athena-framework/routing/releases/tag/v0.1.11\n[#560]: https://github.com/athena-framework/athena/pull/560\n\n## [0.1.10] - 2025-01-26\n\n### Changed\n\n- Allow having multiple independent compiled route collections ([#468]) (George Dietrich)\n- Log unhandled `ART::RoutingHandler` exceptions ([#470]) (George Dietrich)\n\n### Fixed\n\n- Make `ART::RequestContext.from_uri` more robust ([#498]) (George Dietrich)\n\n[0.1.10]: https://github.com/athena-framework/routing/releases/tag/v0.1.10\n[#468]: https://github.com/athena-framework/athena/pull/468\n[#470]: https://github.com/athena-framework/athena/pull/470\n[#498]: https://github.com/athena-framework/athena/pull/498\n\n## [0.1.9] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n### Added\n\n- **Breaking:** add kwargs overload to `ART::Generator::Interface#generate` ([#375]) (George Dietrich)\n\n### Fixed\n\n- Fix compatibility with PCRE2 10.43 ([#362]) (George Dietrich)\n- Fix error when PCRE2 JIT mode is unavailable ([#381]) (George Dietrich)\n\n[0.1.9]: https://github.com/athena-framework/routing/releases/tag/v0.1.9\n[#362]: https://github.com/athena-framework/athena/pull/362\n[#365]: https://github.com/athena-framework/athena/pull/365\n[#375]: https://github.com/athena-framework/athena/pull/375\n[#381]: https://github.com/athena-framework/athena/pull/381\n\n## [0.1.8] - 2023-10-09\n\n### Added\n\n- Internal support for redirecting within an `ART::Matcher::*` ([#307]) (George Dietrich)\n\n[0.1.8]: https://github.com/athena-framework/routing/releases/tag/v0.1.8\n[#307]: https://github.com/athena-framework/athena/pull/307\n\n## [0.1.7] - 2023-05-29\n\n### Changed\n\n- **Breaking:** Update minimum `crystal` version to `~> 1.8.0`. Drop support for `PCRE1`. ([#281]) (George Dietrich)\n\n[0.1.7]: https://github.com/athena-framework/routing/releases/tag/v0.1.7\n[#281]: https://github.com/athena-framework/athena/pull/281\n\n## [0.1.6] - 2023-03-26\n\n### Fixed\n\n- Fix compatibility with Crystal `1.8.0-dev` ([#272]) (George Dietrich)\n\n[0.1.6]: https://github.com/athena-framework/routing/releases/tag/v0.1.6\n[#272]: https://github.com/athena-framework/athena/pull/272\n\n## [0.1.5] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n### Added\n\n- Add additional `ART::Requirement` constants ([#257]) (George Dietrich)\n\n### Fixed\n\n- Fix formatting issue in Crystal `1.8-dev` ([#258]) (George Dietrich)\n\n[0.1.5]: https://github.com/athena-framework/routing/releases/tag/v0.1.5\n[#257]: https://github.com/athena-framework/athena/pull/257\n[#258]: https://github.com/athena-framework/athena/pull/258\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.1.4] - 2023-01-07\n\n### Changed\n\n- Change route compilation to be eager ([#207]) (George Dietrich)\n\n### Added\n\n- Add ability to bubble up exceptions from `ART::RoutingHandler` ([#206]) (George Dietrich)\n- Add `ART::Matcher::TraceableURLMatcher` to help with debugging route matches ([#224]) (George Dietrich)\n- Add `ART::Route#has_scheme?` ([#224]) (George Dietrich)\n\n[0.1.4]: https://github.com/athena-framework/routing/releases/tag/v0.1.4\n[#207]: https://github.com/athena-framework/athena/pull/207\n[#206]: https://github.com/athena-framework/athena/pull/206\n[#224]: https://github.com/athena-framework/athena/pull/224\n\n## [0.1.3] - 2022-09-05\n\n### Changed\n\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n### Added\n\n- Add an `HTTP::Handler` to add basic routing support to a `HTTP::Server` ([#189]) (George Dietrich)\n\n### Fixed\n\n- Fixed slash characters being double escaped in generated URL query params ([#180]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/routing/releases/tag/v0.1.3\n[#180]: https://github.com/athena-framework/athena/pull/180\n[#188]: https://github.com/athena-framework/athena/pull/188\n[#189]: https://github.com/athena-framework/athena/pull/189\n\n## [0.1.2] - 2022-05-14\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n- Add common route requirement constants to the [ART::Requirement](https://athenaframework.org/Routing/Requirement/) namespace ([#173]) (George Dietrich)\n- Add [ART::Requirement::Enum](https://athenaframework.org/Routing/Requirement/Enum/) to make creating [Enum](https://crystal-lang.org/api/Enum.html) based route requirements easier ([#173]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/routing/releases/tag/v0.1.2\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n[#173]: https://github.com/athena-framework/athena/pull/173\n\n## [0.1.1] - 2022-02-05\n\n_First release a part of the monorepo._\n\n### Fixed\n\n- Fix erroneous mutating of matched route data ([#144]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/routing/releases/tag/v0.1.1\n[#144]: https://github.com/athena-framework/athena/pull/144\n\n## [0.1.0] - 2022-01-10\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/routing/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/routing/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/routing/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 George Dietrich\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": "src/components/routing/README.md",
    "content": "# Routing\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/routing.svg)](https://github.com/athena-framework/routing/releases)\n\nHTTP based routing library/framework.\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Routing).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/routing/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.2.0\n\n### `params` parameter of `ART::Generator::Interface#generate` has a looser type restriction\n\nThis parameter was previously typed as a `Hash(String, String?)`, but is now accepts any `Hash`.\nCustom URL generators will need their type restriction updated, and may need to normalize/validate the hash value itself.\n\n### New route default/matched route parameter type\n\nRoute defaults and matcher return values now use the new `ART::Parameters` type instead of `Hash(String, String?)`.\n\nThe new type supports _mostly_ the same API as the old `Hash` type, but may need to update type restrictions if you were passing around the defaults/matched route parameters hash.\nAdditionally, if implementing a custom URL matcher, update return types from `Hash(String, String?)` to `ART::Parameters`.\n\n## Upgrade to 0.1.9\n\n### New `ART::Generator::Interface` method\n\nIf implementing a custom URL Generator, you will now need to implement the following new method:\n\n- `abstract def generate(route : String, reference_type : ART::Generator::ReferenceType = :absolute_path, **params) : String`\n"
  },
  {
    "path": "src/components/routing/docs/README.md",
    "content": "The `Athena::Routing` component provides a performant and robust HTTP based routing library/framework.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-routing:\n    github: athena-framework/routing\n    version: ~> 0.2.0\n```\n\n## Usage\n\nThis component is primarily intended to be used as a basis for a routing implementation for a framework, handling the majority of the heavy lifting.\n\nThe routing component supports various ways to control which routes are matched, including:\n\n* Regex patterns\n* [host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header values\n* HTTP method/scheme\n* Request format/locale\n* Dynamic callbacks\n\nUsing the routing component involves adding [ART::Route](/Routing/Route/) instances to an [ART::RouteCollection](/Routing/RouteCollection/).\nThe collection is then compiled via [ART.compile](</Routing/top_level/#Athena::Routing.compile(routes,*,route_provider)>).\nFrom here, an [ART::Matcher::URLMatcherInterface](/Routing/Matcher/URLMatcherInterface/) or [ART::Matcher::RequestMatcherInterface](/Routing/Matcher/RequestMatcherInterface/) could then be used to determine which route matches a given path or [ART::Request](/Routing/Request/).\n\n```crystal\n# Create a new route collection and add a route with a single parameter to it.\nroutes = ART::RouteCollection.new\nroutes.add \"blog_show\", ART::Route.new \"/blog/{slug}\"\n\n# Compile the routes.\nART.compile routes\n\n# Represents the request in an agnostic data format.\n# In practice this would be created from the current `ART::Request`.\ncontext = ART::RequestContext.new\n\n# Match a request by path.\nmatcher = ART::Matcher::URLMatcher.new context\nmatcher.match \"/blog/foo-bar\" # => ART::Parameters{\"_route\" => \"blog_show\", \"slug\" => \"foo-bar\"}\n```\n\nIt is also possible to go the other way, generate a URL based on its name and set of parameters:\n\n```crystal\n# Generating routes based on route name and parameters is also possible.\ngenerator = ART::Generator::URLGenerator.new context\ngenerator.generate \"blog_show\", slug: \"bar-baz\", source: \"Crystal\" # => \"/blog/bar-baz?source=Crystal\"\n```\n\n### Simple Webapp\n\nThe Crystal stdlib provides [HTTP::Server](https://crystal-lang.org/api/HTTP/Server.html) as a very robust basis to a web application.\nHowever it lacks a fairly critical feature: routing.\nThe Routing component provides [ART::RoutingHandler](/Routing/RoutingHandler/) which can be used to add basic routing functionality.\nThis can be a good choice for super simple web applications that do not need any additional frameworky features.\n\n```crystal\nhandler = ART::RoutingHandler.new\n\n# The `methods` property can be used to limit the route to a particular HTTP method.\nhandler.add \"new_article\", ART::Route.new(\"/article\", methods: \"post\") do |ctx|\n  pp ctx.request.body.try &.gets_to_end\nend\n\n# The match parameters from the route are passed to the callback as a `Hash(String, String?)`.\nhandler.add \"article\", ART::Route.new(\"/article/{id<\\\\d+>}\", methods: \"get\") do |ctx, params|\n  pp params # => {\"_route\" => \"article\", \"id\" => \"10\"}\nend\n\n# Call the `#compile` method when providing the handler to the handler array.\nserver = HTTP::Server.new([\n  handler.compile,\n])\n\naddress = server.bind_tcp 8080\nputs \"Listening on http://#{address}\"\nserver.listen\n```\n\n## Learn More\n\n* [Parameter Validation](/Routing/Route/#Athena::Routing::Route--parameter-validation)\n* Route [Requirement](/Routing/Requirement/) helpers\n* [Catch-all/Glob](/Routing/Route/#Athena::Routing::Route--slash-characters-in-route-parameters) routes\n"
  },
  {
    "path": "src/components/routing/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Routing\nsite_url: https://athenaframework.org/Routing/\nrepo_url: https://github.com/athena-framework/routing\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-routing/src/athena-routing.cr\n          source_locations:\n            lib/athena-routing: https://github.com/athena-framework/routing/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/routing/shard.yml",
    "content": "name: athena-routing\n\nversion: 0.2.0\n\ncrystal: ~> 1.8\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/routing\n\ndocumentation: https://athenaframework.org/Routing\n\ndescription: |\n  HTTP based routing library/framework.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection0.cr",
    "content": "false\n####\nHash(String, Array(ART::RouteProvider::StaticRouteData)).new\n####\nHash(Int32, Regex).new\n####\nHash(String, Array(ART::RouteProvider::DynamicRouteData)).new\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection1.cr",
    "content": "true\n####\n{\n  \"/test/baz\"      => [{ART::Parameters.new({\"_route\" => \"baz\"}), nil, nil, nil, false, false, nil}],\n  \"/test/baz.html\" => [{ART::Parameters.new({\"_route\" => \"baz2\"}), nil, nil, nil, false, false, nil}],\n  \"/test/baz3\"     => [{ART::Parameters.new({\"_route\" => \"baz3\"}), nil, nil, nil, true, false, nil}],\n  \"/foofoo\"        => [{ART::Parameters.new({\"_route\" => \"foofoo\", \"def\" => \"test\"}), nil, nil, nil, false, false, nil}],\n  \"/spa ce\"        => [{ART::Parameters.new({\"_route\" => \"space\"}), nil, nil, nil, false, false, nil}],\n  \"/multi/new\"     => [{ART::Parameters.new({\"_route\" => \"overridden2\"}), nil, nil, nil, false, false, nil}],\n  \"/multi/hey\"     => [{ART::Parameters.new({\"_route\" => \"hey\"}), nil, nil, nil, true, false, nil}],\n  \"/ababa\"         => [{ART::Parameters.new({\"_route\" => \"ababa\"}), nil, nil, nil, false, false, nil}],\n  \"/route1\"        => [{ART::Parameters.new({\"_route\" => \"route1\"}), \"a.example.com\", nil, nil, false, false, nil}],\n  \"/c2/route2\"     => [{ART::Parameters.new({\"_route\" => \"route2\"}), \"a.example.com\", nil, nil, false, false, nil}],\n  \"/route4\"        => [{ART::Parameters.new({\"_route\" => \"route4\"}), \"a.example.com\", nil, nil, false, false, nil}],\n  \"/c2/route3\"     => [{ART::Parameters.new({\"_route\" => \"route3\"}), \"b.example.com\", nil, nil, false, false, nil}],\n  \"/route5\"        => [{ART::Parameters.new({\"_route\" => \"route5\"}), \"c.example.com\", nil, nil, false, false, nil}],\n  \"/route6\"        => [{ART::Parameters.new({\"_route\" => \"route6\"}), nil, nil, nil, false, false, nil}],\n  \"/route11\"       => [{ART::Parameters.new({\"_route\" => \"route11\"}), /^(?P<var1>[^\\.]++)\\.example\\.com$/i, nil, nil, false, false, nil}],\n  \"/route12\"       => [{ART::Parameters.new({\"_route\" => \"route12\", \"var1\" => \"val\"}), /^(?P<var1>[^\\.]++)\\.example\\.com$/i, nil, nil, false, false, nil}],\n  \"/route17\"       => [{ART::Parameters.new({\"_route\" => \"route17\"}), nil, nil, nil, false, false, nil}],\n}\n####\n{\n  0 => ART.create_regex \"^(?|(?:(?:[^./]*+\\\\.)++)(?|/foo/(baz|athenaa)(*:47)|/bar(?|/([^/]++)(*:70)|head/([^/]++)(*:90))|/test/([^/]++)(?|(*:115))|/([']+)(*:131)|/a/(?|b\\\"b/([^/]++)(?|(*:160)|(*:168))|(.*)(*:181)|b\\\"b/([^/]++)(?|(*:204)|(*:212)))|/multi/hello(?:/([^/]++))?(*:248)|/([^/]++)/b/([^/]++)(?|(*:279)|(*:287))|/aba/([^/]++)(*:309))|(?i:([^\\\\.]++)\\\\.example\\\\.com)\\\\.(?|/route1(?|3/([^/]++)(*:371)|4/([^/]++)(*:389)))|(?i:c\\\\.example\\\\.com)\\\\.(?|/route15/([^/]++)(*:441))|(?:(?:[^./]*+\\\\.)++)(?|/route16/([^/]++)(*:489)|/a/(?|a\\\\.\\\\.\\\\.(*:510)|b/(?|([^/]++)(*:531)|c/([^/]++)(*:549)))))/?$\",\n}\n####\n{\n  \"47\"  => [{ART::Parameters.new({\"_route\" => \"foo\", \"def\" => \"test\"}), Set{\"bar\"}, nil, nil, false, true, nil}],\n  \"70\"  => [{ART::Parameters.new({\"_route\" => \"bar\"}), Set{\"foo\"}, Set{\"GET\", \"HEAD\"}, nil, false, true, nil}],\n  \"90\"  => [{ART::Parameters.new({\"_route\" => \"barhead\"}), Set{\"foo\"}, Set{\"GET\"}, nil, false, true, nil}],\n  \"115\" => [\n    {ART::Parameters.new({\"_route\" => \"baz4\"}), Set{\"foo\"}, nil, nil, true, true, nil},\n    {ART::Parameters.new({\"_route\" => \"baz5\"}), Set{\"foo\"}, Set{\"POST\"}, nil, true, true, nil},\n    {ART::Parameters.new({\"_route\" => \"baz.baz6\"}), Set{\"foo\"}, Set{\"PUT\"}, nil, true, true, nil},\n  ],\n  \"131\" => [{ART::Parameters.new({\"_route\" => \"quoter\"}), Set{\"quoter\"}, nil, nil, false, true, nil}],\n  \"160\" => [{ART::Parameters.new({\"_route\" => \"foo1\"}), Set{\"foo\"}, Set{\"PUT\"}, nil, false, true, nil}],\n  \"168\" => [{ART::Parameters.new({\"_route\" => \"bar1\"}), Set{\"bar\"}, nil, nil, false, true, nil}],\n  \"181\" => [{ART::Parameters.new({\"_route\" => \"overridden\"}), Set{\"var\"}, nil, nil, false, true, nil}],\n  \"204\" => [{ART::Parameters.new({\"_route\" => \"foo2\"}), Set{\"foo1\"}, nil, nil, false, true, nil}],\n  \"212\" => [{ART::Parameters.new({\"_route\" => \"bar2\"}), Set{\"bar1\"}, nil, nil, false, true, nil}],\n  \"248\" => [{ART::Parameters.new({\"_route\" => \"helloWorld\", \"who\" => \"World!\"}), Set{\"who\"}, nil, nil, false, true, nil}],\n  \"279\" => [{ART::Parameters.new({\"_route\" => \"foo3\"}), Set{\"_locale\", \"foo\"}, nil, nil, false, true, nil}],\n  \"287\" => [{ART::Parameters.new({\"_route\" => \"bar3\"}), Set{\"_locale\", \"bar\"}, nil, nil, false, true, nil}],\n  \"309\" => [{ART::Parameters.new({\"_route\" => \"foo4\"}), Set{\"foo\"}, nil, nil, false, true, nil}],\n  \"371\" => [{ART::Parameters.new({\"_route\" => \"route13\"}), Set{\"name\", \"var1\"}, nil, nil, false, true, nil}],\n  \"389\" => [{ART::Parameters.new({\"_route\" => \"route14\", \"var1\" => \"val\"}), Set{\"name\", \"var1\"}, nil, nil, false, true, nil}],\n  \"441\" => [{ART::Parameters.new({\"_route\" => \"route15\"}), Set{\"name\"}, nil, nil, false, true, nil}],\n  \"489\" => [{ART::Parameters.new({\"_route\" => \"route16\", \"var1\" => \"val\"}), Set{\"name\"}, nil, nil, false, true, nil}],\n  \"510\" => [{ART::Parameters.new({\"_route\" => \"a\"}), Set(String).new, nil, nil, false, false, nil}],\n  \"531\" => [{ART::Parameters.new({\"_route\" => \"b\"}), Set{\"var\"}, nil, nil, false, true, nil}],\n  \"549\" => [{ART::Parameters.new({\"_route\" => \"c\"}), Set{\"var\"}, nil, nil, false, true, nil}],\n}\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection10.cr",
    "content": "false\n####\nHash(String, Array(ART::RouteProvider::StaticRouteData)).new\n####\n{\n  0 => ART.create_regex \"^(?|/(en|fr)/(?|admin/post(?|(*:32)|/(?|new(*:46)|(\\\\d+)(*:58)|(\\\\d+)/edit(*:75)|(\\\\d+)/delete(*:94)))|blog(?|(*:110)|/(?|rss\\\\.xml(*:130)|p(?|age/([^/]++)(*:154)|osts/([^/]++)(*:175))|comments/(\\\\d+)/new(*:202)|search(*:216)))|log(?|in(*:234)|out(*:245)))|/(en|fr)?(*:264))/?$\",\n}\n####\n{\n  \"32\"  => [{ART::Parameters.new({\"_route\" => \"a\", \"_locale\" => \"en\"}), Set{\"_locale\"}, nil, nil, true, false, nil}],\n  \"46\"  => [{ART::Parameters.new({\"_route\" => \"b\", \"_locale\" => \"en\"}), Set{\"_locale\"}, nil, nil, false, false, nil}],\n  \"58\"  => [{ART::Parameters.new({\"_route\" => \"c\", \"_locale\" => \"en\"}), Set{\"_locale\", \"id\"}, nil, nil, false, true, nil}],\n  \"75\"  => [{ART::Parameters.new({\"_route\" => \"d\", \"_locale\" => \"en\"}), Set{\"_locale\", \"id\"}, nil, nil, false, false, nil}],\n  \"94\"  => [{ART::Parameters.new({\"_route\" => \"e\", \"_locale\" => \"en\"}), Set{\"_locale\", \"id\"}, nil, nil, false, false, nil}],\n  \"110\" => [{ART::Parameters.new({\"_route\" => \"f\", \"_locale\" => \"en\"}), Set{\"_locale\"}, nil, nil, true, false, nil}],\n  \"130\" => [{ART::Parameters.new({\"_route\" => \"g\", \"_locale\" => \"en\"}), Set{\"_locale\"}, nil, nil, false, false, nil}],\n  \"154\" => [{ART::Parameters.new({\"_route\" => \"h\", \"_locale\" => \"en\"}), Set{\"_locale\", \"page\"}, nil, nil, false, true, nil}],\n  \"175\" => [{ART::Parameters.new({\"_route\" => \"i\", \"_locale\" => \"en\"}), Set{\"_locale\", \"page\"}, nil, nil, false, true, nil}],\n  \"202\" => [{ART::Parameters.new({\"_route\" => \"j\", \"_locale\" => \"en\"}), Set{\"_locale\", \"id\"}, nil, nil, false, false, nil}],\n  \"216\" => [{ART::Parameters.new({\"_route\" => \"k\", \"_locale\" => \"en\"}), Set{\"_locale\"}, nil, nil, false, false, nil}],\n  \"234\" => [{ART::Parameters.new({\"_route\" => \"l\", \"_locale\" => \"en\"}), Set{\"_locale\"}, nil, nil, false, false, nil}],\n  \"245\" => [{ART::Parameters.new({\"_route\" => \"m\", \"_locale\" => \"en\"}), Set{\"_locale\"}, nil, nil, false, false, nil}],\n  \"264\" => [{ART::Parameters.new({\"_route\" => \"n\", \"_locale\" => \"en\"}), Set{\"_locale\"}, nil, nil, false, true, nil}],\n}\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection11.cr",
    "content": "false\n####\nHash(String, Array(ART::RouteProvider::StaticRouteData)).new\n####\n{\n  0 => ART.create_regex \"^(?|/abc([^/]++)/(?|1(?|(*:27)|0(?|(*:38)|0(*:46)))|2(?|(*:59)|0(?|(*:70)|0(*:78)))))/?$\",\n}\n####\n{\n  \"27\" => [{ART::Parameters.new({\"_route\" => \"r1\"}), Set{\"foo\"}, nil, nil, false, false, nil}],\n  \"38\" => [{ART::Parameters.new({\"_route\" => \"r10\"}), Set{\"foo\"}, nil, nil, false, false, nil}],\n  \"46\" => [{ART::Parameters.new({\"_route\" => \"r100\"}), Set{\"foo\"}, nil, nil, false, false, nil}],\n  \"59\" => [{ART::Parameters.new({\"_route\" => \"r2\"}), Set{\"foo\"}, nil, nil, false, false, nil}],\n  \"70\" => [{ART::Parameters.new({\"_route\" => \"r20\"}), Set{\"foo\"}, nil, nil, false, false, nil}],\n  \"78\" => [{ART::Parameters.new({\"_route\" => \"r200\"}), Set{\"foo\"}, nil, nil, false, false, nil}],\n}\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection12.cr",
    "content": "true\n####\nHash(String, Array(ART::RouteProvider::StaticRouteData)).new\n####\n{\n  0 => ART.create_regex \"^(?|(?i:([^\\\\.]++)\\\\.example\\\\.com)\\\\.(?|/abc([^/]++)(?|(*:55))))/?$\",\n}\n####\n{\n  \"55\" => [\n    {ART::Parameters.new({\"_route\" => \"r1\"}), Set{\"foo\", \"foo\"}, nil, nil, false, true, nil},\n    {ART::Parameters.new({\"_route\" => \"r2\"}), Set{\"foo\", \"foo\"}, nil, nil, false, true, nil},\n  ],\n}\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection2.cr",
    "content": "true\n####\n{\n  \"/test/baz\"      => [{ART::Parameters.new({\"_route\" => \"baz\"}), nil, nil, nil, false, false, nil}],\n  \"/test/baz.html\" => [{ART::Parameters.new({\"_route\" => \"baz2\"}), nil, nil, nil, false, false, nil}],\n  \"/test/baz3\"     => [{ART::Parameters.new({\"_route\" => \"baz3\"}), nil, nil, nil, true, false, nil}],\n  \"/foofoo\"        => [{ART::Parameters.new({\"_route\" => \"foofoo\", \"def\" => \"test\"}), nil, nil, nil, false, false, nil}],\n  \"/spa ce\"        => [{ART::Parameters.new({\"_route\" => \"space\"}), nil, nil, nil, false, false, nil}],\n  \"/multi/new\"     => [{ART::Parameters.new({\"_route\" => \"overridden2\"}), nil, nil, nil, false, false, nil}],\n  \"/multi/hey\"     => [{ART::Parameters.new({\"_route\" => \"hey\"}), nil, nil, nil, true, false, nil}],\n  \"/ababa\"         => [{ART::Parameters.new({\"_route\" => \"ababa\"}), nil, nil, nil, false, false, nil}],\n  \"/route1\"        => [{ART::Parameters.new({\"_route\" => \"route1\"}), \"a.example.com\", nil, nil, false, false, nil}],\n  \"/c2/route2\"     => [{ART::Parameters.new({\"_route\" => \"route2\"}), \"a.example.com\", nil, nil, false, false, nil}],\n  \"/route4\"        => [{ART::Parameters.new({\"_route\" => \"route4\"}), \"a.example.com\", nil, nil, false, false, nil}],\n  \"/c2/route3\"     => [{ART::Parameters.new({\"_route\" => \"route3\"}), \"b.example.com\", nil, nil, false, false, nil}],\n  \"/route5\"        => [{ART::Parameters.new({\"_route\" => \"route5\"}), \"c.example.com\", nil, nil, false, false, nil}],\n  \"/route6\"        => [{ART::Parameters.new({\"_route\" => \"route6\"}), nil, nil, nil, false, false, nil}],\n  \"/route11\"       => [{ART::Parameters.new({\"_route\" => \"route11\"}), /^(?P<var1>[^\\.]++)\\.example\\.com$/i, nil, nil, false, false, nil}],\n  \"/route12\"       => [{ART::Parameters.new({\"_route\" => \"route12\", \"var1\" => \"val\"}), /^(?P<var1>[^\\.]++)\\.example\\.com$/i, nil, nil, false, false, nil}],\n  \"/route17\"       => [{ART::Parameters.new({\"_route\" => \"route17\"}), nil, nil, nil, false, false, nil}],\n  \"/secure\"        => [{ART::Parameters.new({\"_route\" => \"secure\"}), nil, nil, Set{\"https\"}, false, false, nil}],\n  \"/nonsecure\"     => [{ART::Parameters.new({\"_route\" => \"nonsecure\"}), nil, nil, Set{\"http\"}, false, false, nil}],\n}\n####\n{\n  0 => ART.create_regex \"^(?|(?:(?:[^./]*+\\\\.)++)(?|/foo/(baz|athenaa)(*:47)|/bar(?|/([^/]++)(*:70)|head/([^/]++)(*:90))|/test/([^/]++)(?|(*:115))|/([']+)(*:131)|/a/(?|b\\\"b/([^/]++)(?|(*:160)|(*:168))|(.*)(*:181)|b\\\"b/([^/]++)(?|(*:204)|(*:212)))|/multi/hello(?:/([^/]++))?(*:248)|/([^/]++)/b/([^/]++)(?|(*:279)|(*:287))|/aba/([^/]++)(*:309))|(?i:([^\\\\.]++)\\\\.example\\\\.com)\\\\.(?|/route1(?|3/([^/]++)(*:371)|4/([^/]++)(*:389)))|(?i:c\\\\.example\\\\.com)\\\\.(?|/route15/([^/]++)(*:441))|(?:(?:[^./]*+\\\\.)++)(?|/route16/([^/]++)(*:489)|/a/(?|a\\\\.\\\\.\\\\.(*:510)|b/(?|([^/]++)(*:531)|c/([^/]++)(*:549)))))/?$\",\n}\n####\n{\n  \"47\"  => [{ART::Parameters.new({\"_route\" => \"foo\", \"def\" => \"test\"}), Set{\"bar\"}, nil, nil, false, true, nil}],\n  \"70\"  => [{ART::Parameters.new({\"_route\" => \"bar\"}), Set{\"foo\"}, Set{\"GET\", \"HEAD\"}, nil, false, true, nil}],\n  \"90\"  => [{ART::Parameters.new({\"_route\" => \"barhead\"}), Set{\"foo\"}, Set{\"GET\"}, nil, false, true, nil}],\n  \"115\" => [\n    {ART::Parameters.new({\"_route\" => \"baz4\"}), Set{\"foo\"}, nil, nil, true, true, nil},\n    {ART::Parameters.new({\"_route\" => \"baz5\"}), Set{\"foo\"}, Set{\"POST\"}, nil, true, true, nil},\n    {ART::Parameters.new({\"_route\" => \"baz.baz6\"}), Set{\"foo\"}, Set{\"PUT\"}, nil, true, true, nil},\n  ],\n  \"131\" => [{ART::Parameters.new({\"_route\" => \"quoter\"}), Set{\"quoter\"}, nil, nil, false, true, nil}],\n  \"160\" => [{ART::Parameters.new({\"_route\" => \"foo1\"}), Set{\"foo\"}, Set{\"PUT\"}, nil, false, true, nil}],\n  \"168\" => [{ART::Parameters.new({\"_route\" => \"bar1\"}), Set{\"bar\"}, nil, nil, false, true, nil}],\n  \"181\" => [{ART::Parameters.new({\"_route\" => \"overridden\"}), Set{\"var\"}, nil, nil, false, true, nil}],\n  \"204\" => [{ART::Parameters.new({\"_route\" => \"foo2\"}), Set{\"foo1\"}, nil, nil, false, true, nil}],\n  \"212\" => [{ART::Parameters.new({\"_route\" => \"bar2\"}), Set{\"bar1\"}, nil, nil, false, true, nil}],\n  \"248\" => [{ART::Parameters.new({\"_route\" => \"helloWorld\", \"who\" => \"World!\"}), Set{\"who\"}, nil, nil, false, true, nil}],\n  \"279\" => [{ART::Parameters.new({\"_route\" => \"foo3\"}), Set{\"_locale\", \"foo\"}, nil, nil, false, true, nil}],\n  \"287\" => [{ART::Parameters.new({\"_route\" => \"bar3\"}), Set{\"_locale\", \"bar\"}, nil, nil, false, true, nil}],\n  \"309\" => [{ART::Parameters.new({\"_route\" => \"foo4\"}), Set{\"foo\"}, nil, nil, false, true, nil}],\n  \"371\" => [{ART::Parameters.new({\"_route\" => \"route13\"}), Set{\"name\", \"var1\"}, nil, nil, false, true, nil}],\n  \"389\" => [{ART::Parameters.new({\"_route\" => \"route14\", \"var1\" => \"val\"}), Set{\"name\", \"var1\"}, nil, nil, false, true, nil}],\n  \"441\" => [{ART::Parameters.new({\"_route\" => \"route15\"}), Set{\"name\"}, nil, nil, false, true, nil}],\n  \"489\" => [{ART::Parameters.new({\"_route\" => \"route16\", \"var1\" => \"val\"}), Set{\"name\"}, nil, nil, false, true, nil}],\n  \"510\" => [{ART::Parameters.new({\"_route\" => \"a\"}), Set(String).new, nil, nil, false, false, nil}],\n  \"531\" => [{ART::Parameters.new({\"_route\" => \"b\"}), Set{\"var\"}, nil, nil, false, true, nil}],\n  \"549\" => [{ART::Parameters.new({\"_route\" => \"c\"}), Set{\"var\"}, nil, nil, false, true, nil}],\n}\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection3.cr",
    "content": "false\n####\n{\n  \"/rootprefix/test\" => [{ART::Parameters.new({\"_route\" => \"static\"}), nil, nil, nil, false, false, nil}],\n  \"/with-condition\"  => [{ART::Parameters.new({\"_route\" => \"with-condition\"}), nil, nil, nil, false, false, 0}],\n}\n####\n{\n  0 => ART.create_regex \"^(?|/rootprefix/([^/]++)(*:27))/?$\",\n}\n####\n{\n  \"27\" => [{ART::Parameters.new({\"_route\" => \"dynamic\"}), Set{\"var\"}, nil, nil, false, true, nil}],\n}\n####\n1\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection4.cr",
    "content": "false\n####\n{\n  \"/just_head\"     => [{ART::Parameters.new({\"_route\" => \"just_head\"}), nil, Set{\"HEAD\"}, nil, false, false, nil}],\n  \"/head_and_get\"  => [{ART::Parameters.new({\"_route\" => \"head_and_get\"}), nil, Set{\"HEAD\", \"GET\"}, nil, false, false, nil}],\n  \"/get_and_head\"  => [{ART::Parameters.new({\"_route\" => \"get_and_head\"}), nil, Set{\"GET\", \"HEAD\"}, nil, false, false, nil}],\n  \"/post_and_head\" => [{ART::Parameters.new({\"_route\" => \"post_and_head\"}), nil, Set{\"POST\", \"HEAD\"}, nil, false, false, nil}],\n  \"/put_and_post\"  => [\n    {ART::Parameters.new({\"_route\" => \"put_and_post\"}), nil, Set{\"PUT\", \"POST\"}, nil, false, false, nil},\n    {ART::Parameters.new({\"_route\" => \"put_and_get_and_head\"}), nil, Set{\"PUT\", \"GET\", \"HEAD\"}, nil, false, false, nil},\n  ],\n}\n####\nHash(Int32, Regex).new\n####\nHash(String, Array(ART::RouteProvider::DynamicRouteData)).new\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection5.cr",
    "content": "false\n####\n{\n  \"/a/11\"            => [{ART::Parameters.new({\"_route\" => \"a_first\"}), nil, nil, nil, false, false, nil}],\n  \"/a/22\"            => [{ART::Parameters.new({\"_route\" => \"a_second\"}), nil, nil, nil, false, false, nil}],\n  \"/a/33\"            => [{ART::Parameters.new({\"_route\" => \"a_third\"}), nil, nil, nil, false, false, nil}],\n  \"/a/44\"            => [{ART::Parameters.new({\"_route\" => \"a_fourth\"}), nil, nil, nil, true, false, nil}],\n  \"/a/55\"            => [{ART::Parameters.new({\"_route\" => \"a_fifth\"}), nil, nil, nil, true, false, nil}],\n  \"/a/66\"            => [{ART::Parameters.new({\"_route\" => \"a_sixth\"}), nil, nil, nil, true, false, nil}],\n  \"/nested/group/a\"  => [{ART::Parameters.new({\"_route\" => \"nested_a\"}), nil, nil, nil, true, false, nil}],\n  \"/nested/group/b\"  => [{ART::Parameters.new({\"_route\" => \"nested_b\"}), nil, nil, nil, true, false, nil}],\n  \"/nested/group/c\"  => [{ART::Parameters.new({\"_route\" => \"nested_c\"}), nil, nil, nil, true, false, nil}],\n  \"/slashed/group\"   => [{ART::Parameters.new({\"_route\" => \"slashed_a\"}), nil, nil, nil, true, false, nil}],\n  \"/slashed/group/b\" => [{ART::Parameters.new({\"_route\" => \"slashed_b\"}), nil, nil, nil, true, false, nil}],\n  \"/slashed/group/c\" => [{ART::Parameters.new({\"_route\" => \"slashed_c\"}), nil, nil, nil, true, false, nil}],\n}\n####\n{\n  0 => ART.create_regex(\"^(?|/([^/]++)(*:16)|/nested/([^/]++)(*:39))/?$\"),\n}\n####\n{\n  \"16\" => [{ART::Parameters.new({\"_route\" => \"a_wildcard\"}), Set{\"param\"}, nil, nil, false, true, nil}],\n  \"39\" => [{ART::Parameters.new({\"_route\" => \"nested_wildcard\"}), Set{\"param\"}, nil, nil, false, true, nil}],\n}\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection6.cr",
    "content": "false\n####\n{\n  \"/trailing/simple/no-methods\"      => [{ART::Parameters.new({\"_route\" => \"simple_trailing_slash_no_methods\"}), nil, nil, nil, true, false, nil}],\n  \"/trailing/simple/get-method\"      => [{ART::Parameters.new({\"_route\" => \"simple_trailing_slash_GET_method\"}), nil, Set{\"GET\"}, nil, true, false, nil}],\n  \"/trailing/simple/head-method\"     => [{ART::Parameters.new({\"_route\" => \"simple_trailing_slash_HEAD_method\"}), nil, Set{\"HEAD\"}, nil, true, false, nil}],\n  \"/trailing/simple/post-method\"     => [{ART::Parameters.new({\"_route\" => \"simple_trailing_slash_POST_method\"}), nil, Set{\"POST\"}, nil, true, false, nil}],\n  \"/not-trailing/simple/no-methods\"  => [{ART::Parameters.new({\"_route\" => \"simple_not_trailing_slash_no_methods\"}), nil, nil, nil, false, false, nil}],\n  \"/not-trailing/simple/get-method\"  => [{ART::Parameters.new({\"_route\" => \"simple_not_trailing_slash_GET_method\"}), nil, Set{\"GET\"}, nil, false, false, nil}],\n  \"/not-trailing/simple/head-method\" => [{ART::Parameters.new({\"_route\" => \"simple_not_trailing_slash_HEAD_method\"}), nil, Set{\"HEAD\"}, nil, false, false, nil}],\n  \"/not-trailing/simple/post-method\" => [{ART::Parameters.new({\"_route\" => \"simple_not_trailing_slash_POST_method\"}), nil, Set{\"POST\"}, nil, false, false, nil}],\n}\n####\n{\n  0 => ART.create_regex \"^(?|/trailing/regex/(?|no\\\\-methods/([^/]++)(*:46)|get\\\\-method/([^/]++)(*:73)|head\\\\-method/([^/]++)(*:101)|post\\\\-method/([^/]++)(*:130))|/not\\\\-trailing/regex/(?|no\\\\-methods/([^/]++)(*:183)|get\\\\-method/([^/]++)(*:211)|head\\\\-method/([^/]++)(*:240)|post\\\\-method/([^/]++)(*:269)))/?$\",\n}\n####\n{\n  \"46\"  => [{ART::Parameters.new({\"_route\" => \"regex_trailing_slash_no_methods\"}), Set{\"param\"}, nil, nil, true, true, nil}],\n  \"73\"  => [{ART::Parameters.new({\"_route\" => \"regex_trailing_slash_GET_method\"}), Set{\"param\"}, Set{\"GET\"}, nil, true, true, nil}],\n  \"101\" => [{ART::Parameters.new({\"_route\" => \"regex_trailing_slash_HEAD_method\"}), Set{\"param\"}, Set{\"HEAD\"}, nil, true, true, nil}],\n  \"130\" => [{ART::Parameters.new({\"_route\" => \"regex_trailing_slash_POST_method\"}), Set{\"param\"}, Set{\"POST\"}, nil, true, true, nil}],\n  \"183\" => [{ART::Parameters.new({\"_route\" => \"regex_not_trailing_slash_no_methods\"}), Set{\"param\"}, nil, nil, false, true, nil}],\n  \"211\" => [{ART::Parameters.new({\"_route\" => \"regex_not_trailing_slash_GET_method\"}), Set{\"param\"}, Set{\"GET\"}, nil, false, true, nil}],\n  \"240\" => [{ART::Parameters.new({\"_route\" => \"regex_not_trailing_slash_HEAD_method\"}), Set{\"param\"}, Set{\"HEAD\"}, nil, false, true, nil}],\n  \"269\" => [{ART::Parameters.new({\"_route\" => \"regex_not_trailing_slash_POST_method\"}), Set{\"param\"}, Set{\"POST\"}, nil, false, true, nil}],\n}\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection7.cr",
    "content": "false\n####\n{\n  \"/trailing/simple/no-methods\"      => [{ART::Parameters.new({\"_route\" => \"simple_trailing_slash_no_methods\"}), nil, nil, nil, true, false, nil}],\n  \"/trailing/simple/get-method\"      => [{ART::Parameters.new({\"_route\" => \"simple_trailing_slash_GET_method\"}), nil, Set{\"GET\"}, nil, true, false, nil}],\n  \"/trailing/simple/head-method\"     => [{ART::Parameters.new({\"_route\" => \"simple_trailing_slash_HEAD_method\"}), nil, Set{\"HEAD\"}, nil, true, false, nil}],\n  \"/trailing/simple/post-method\"     => [{ART::Parameters.new({\"_route\" => \"simple_trailing_slash_POST_method\"}), nil, Set{\"POST\"}, nil, true, false, nil}],\n  \"/not-trailing/simple/no-methods\"  => [{ART::Parameters.new({\"_route\" => \"simple_not_trailing_slash_no_methods\"}), nil, nil, nil, false, false, nil}],\n  \"/not-trailing/simple/get-method\"  => [{ART::Parameters.new({\"_route\" => \"simple_not_trailing_slash_GET_method\"}), nil, Set{\"GET\"}, nil, false, false, nil}],\n  \"/not-trailing/simple/head-method\" => [{ART::Parameters.new({\"_route\" => \"simple_not_trailing_slash_HEAD_method\"}), nil, Set{\"HEAD\"}, nil, false, false, nil}],\n  \"/not-trailing/simple/post-method\" => [{ART::Parameters.new({\"_route\" => \"simple_not_trailing_slash_POST_method\"}), nil, Set{\"POST\"}, nil, false, false, nil}],\n}\n####\n{\n  0 => ART.create_regex \"^(?|/trailing/regex/(?|no\\\\-methods/([^/]++)(*:46)|get\\\\-method/([^/]++)(*:73)|head\\\\-method/([^/]++)(*:101)|post\\\\-method/([^/]++)(*:130))|/not\\\\-trailing/regex/(?|no\\\\-methods/([^/]++)(*:183)|get\\\\-method/([^/]++)(*:211)|head\\\\-method/([^/]++)(*:240)|post\\\\-method/([^/]++)(*:269)))/?$\",\n}\n####\n{\n  \"46\"  => [{ART::Parameters.new({\"_route\" => \"regex_trailing_slash_no_methods\"}), Set{\"param\"}, nil, nil, true, true, nil}],\n  \"73\"  => [{ART::Parameters.new({\"_route\" => \"regex_trailing_slash_GET_method\"}), Set{\"param\"}, Set{\"GET\"}, nil, true, true, nil}],\n  \"101\" => [{ART::Parameters.new({\"_route\" => \"regex_trailing_slash_HEAD_method\"}), Set{\"param\"}, Set{\"HEAD\"}, nil, true, true, nil}],\n  \"130\" => [{ART::Parameters.new({\"_route\" => \"regex_trailing_slash_POST_method\"}), Set{\"param\"}, Set{\"POST\"}, nil, true, true, nil}],\n  \"183\" => [{ART::Parameters.new({\"_route\" => \"regex_not_trailing_slash_no_methods\"}), Set{\"param\"}, nil, nil, false, true, nil}],\n  \"211\" => [{ART::Parameters.new({\"_route\" => \"regex_not_trailing_slash_GET_method\"}), Set{\"param\"}, Set{\"GET\"}, nil, false, true, nil}],\n  \"240\" => [{ART::Parameters.new({\"_route\" => \"regex_not_trailing_slash_HEAD_method\"}), Set{\"param\"}, Set{\"HEAD\"}, nil, false, true, nil}],\n  \"269\" => [{ART::Parameters.new({\"_route\" => \"regex_not_trailing_slash_POST_method\"}), Set{\"param\"}, Set{\"POST\"}, nil, false, true, nil}],\n}\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection8.cr",
    "content": "true\n####\n{\n  \"/\" => [\n    {ART::Parameters.new({\"_route\" => \"a\"}), /^(?P<d>[^\\.]++)\\.e\\.c\\.b\\.a$/i, nil, nil, false, false, nil},\n    {ART::Parameters.new({\"_route\" => \"c\"}), /^(?P<e>[^\\.]++)\\.e\\.c\\.b\\.a$/i, nil, nil, false, false, nil},\n    {ART::Parameters.new({\"_route\" => \"b\"}), \"d.c.b.a\", nil, nil, false, false, nil},\n  ],\n}\n####\nHash(Int32, Regex).new\n####\nHash(String, Array(ART::RouteProvider::DynamicRouteData)).new\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/fixtures/route_provider/route_collection9.cr",
    "content": "false\n####\nHash(String, Array(ART::RouteProvider::StaticRouteData)).new\n####\n{\n      0 => ART.create_regex(\"^(?|/c(?|f(?|cd20/([^/]++)/([^/]++)/([^/]++)/cfcd20(*:54)|e(?|cdb/([^/]++)/([^/]++)/([^/]++)/cfecdb(*:102)|e39/([^/]++)/([^/]++)/([^/]++)/cfee39(*:147))|a086/([^/]++)/([^/]++)/([^/]++)/cfa086(*:194)|004f/([^/]++)/([^/]++)/([^/]++)/cf004f(*:240))|4(?|ca42/([^/]++)/([^/]++)/([^/]++)/c4ca42(*:291)|5147/([^/]++)/([^/]++)/([^/]++)/c45147(*:337)|1000/([^/]++)/([^/]++)/([^/]++)/c41000(*:383))|8(?|1e72/([^/]++)/([^/]++)/([^/]++)/c81e72(*:434)|ffe9/([^/]++)/([^/]++)/([^/]++)/c8ffe9(*:480)|6a7e/([^/]++)/([^/]++)/([^/]++)/c86a7e(*:526))|9(?|f0f8/([^/]++)/([^/]++)/([^/]++)/c9f0f8(*:577)|e107/([^/]++)/([^/]++)/([^/]++)/c9e107(*:623))|2(?|0(?|ad4/([^/]++)/([^/]++)/([^/]++)/c20ad4(*:677)|3d8/([^/]++)/([^/]++)/([^/]++)/c203d8(*:722))|4cd7/([^/]++)/([^/]++)/([^/]++)/c24cd7(*:769))|5(?|1ce4/([^/]++)/([^/]++)/([^/]++)/c51ce4(*:820)|2f1b/([^/]++)/([^/]++)/([^/]++)/c52f1b(*:866)|ff25/([^/]++)/([^/]++)/([^/]++)/c5ff25(*:912))|7(?|4d97/([^/]++)/([^/]++)/([^/]++)/c74d97(*:963)|e124/([^/]++)/([^/]++)/([^/]++)/c7e124(*:1009))|16a53/([^/]++)/([^/]++)/([^/]++)/c16a53(*:1058)|0(?|c7c7/([^/]++)/([^/]++)/([^/]++)/c0c7c7(*:1109)|e190/([^/]++)/([^/]++)/([^/]++)/c0e190(*:1156)|42f4/([^/]++)/([^/]++)/([^/]++)/c042f4(*:1203)|58f5/([^/]++)/([^/]++)/([^/]++)/c058f5(*:1250))|e(?|debb/([^/]++)/([^/]++)/([^/]++)/cedebb(*:1302)|e631/([^/]++)/([^/]++)/([^/]++)/cee631(*:1349))|a(?|46c1/([^/]++)/([^/]++)/([^/]++)/ca46c1(*:1401)|f1a3/([^/]++)/([^/]++)/([^/]++)/caf1a3(*:1448))|b70ab/([^/]++)/([^/]++)/([^/]++)/cb70ab(*:1497)|d0069/([^/]++)/([^/]++)/([^/]++)/cd0069(*:1545)|3(?|e878/([^/]++)/([^/]++)/([^/]++)/c3e878(*:1596)|c59e/([^/]++)/([^/]++)/([^/]++)/c3c59e(*:1643)))|/e(?|c(?|cbc8/([^/]++)/([^/]++)/([^/]++)/eccbc8(*:1701)|8(?|956/([^/]++)/([^/]++)/([^/]++)/ec8956(*:1751)|ce6/([^/]++)/([^/]++)/([^/]++)/ec8ce6(*:1797))|5dec/([^/]++)/([^/]++)/([^/]++)/ec5dec(*:1845))|4(?|da3b/([^/]++)/([^/]++)/([^/]++)/e4da3b(*:1897)|a622/([^/]++)/([^/]++)/([^/]++)/e4a622(*:1944)|6de7/([^/]++)/([^/]++)/([^/]++)/e46de7(*:1991)|4fea/([^/]++)/([^/]++)/([^/]++)/e44fea(*:2038))|3(?|6985/([^/]++)/([^/]++)/([^/]++)/e36985(*:2090)|796a/([^/]++)/([^/]++)/([^/]++)/e3796a(*:2137))|a(?|5d2f/([^/]++)/([^/]++)/([^/]++)/ea5d2f(*:2189)|e27d/([^/]++)/([^/]++)/([^/]++)/eae27d(*:2236))|2(?|c(?|420/([^/]++)/([^/]++)/([^/]++)/e2c420(*:2291)|0be/([^/]++)/([^/]++)/([^/]++)/e2c0be(*:2337))|ef52/([^/]++)/([^/]++)/([^/]++)/e2ef52(*:2385))|d(?|3d2c/([^/]++)/([^/]++)/([^/]++)/ed3d2c(*:2437)|a80a/([^/]++)/([^/]++)/([^/]++)/eda80a(*:2484)|dea8/([^/]++)/([^/]++)/([^/]++)/eddea8(*:2531))|b(?|16(?|0d/([^/]++)/([^/]++)/([^/]++)/eb160d(*:2586)|37/([^/]++)/([^/]++)/([^/]++)/eb1637(*:2631))|a0dc/([^/]++)/([^/]++)/([^/]++)/eba0dc(*:2679))|0(?|0da0/([^/]++)/([^/]++)/([^/]++)/e00da0(*:2731)|c641/([^/]++)/([^/]++)/([^/]++)/e0c641(*:2778))|e(?|cca5/([^/]++)/([^/]++)/([^/]++)/eecca5(*:2830)|d5af/([^/]++)/([^/]++)/([^/]++)/eed5af(*:2877))|96ed4/([^/]++)/([^/]++)/([^/]++)/e96ed4(*:2926)|1(?|6542/([^/]++)/([^/]++)/([^/]++)/e16542(*:2977)|e32e/([^/]++)/([^/]++)/([^/]++)/e1e32e(*:3024))|56954/([^/]++)/([^/]++)/([^/]++)/e56954(*:3073)|f(?|0d39/([^/]++)/([^/]++)/([^/]++)/ef0d39(*:3124)|e937/([^/]++)/([^/]++)/([^/]++)/efe937(*:3171)|575e/([^/]++)/([^/]++)/([^/]++)/ef575e(*:3218))|7b24b/([^/]++)/([^/]++)/([^/]++)/e7b24b(*:3267)|836d8/([^/]++)/([^/]++)/([^/]++)/e836d8(*:3315))|/a(?|8(?|7ff6/([^/]++)/([^/]++)/([^/]++)/a87ff6(*:3372)|baa5/([^/]++)/([^/]++)/([^/]++)/a8baa5(*:3419)|f15e/([^/]++)/([^/]++)/([^/]++)/a8f15e(*:3466)|c88a/([^/]++)/([^/]++)/([^/]++)/a8c88a(*:3513)|abb4/([^/]++)/([^/]++)/([^/]++)/a8abb4(*:3560))|a(?|b323/([^/]++)/([^/]++)/([^/]++)/aab323(*:3612)|942a/([^/]++)/([^/]++)/([^/]++)/aa942a(*:3659))|5(?|bfc9/([^/]++)/([^/]++)/([^/]++)/a5bfc9(*:3711)|771b/([^/]++)/([^/]++)/([^/]++)/a5771b(*:3758)|e001/([^/]++)/([^/]++)/([^/]++)/a5e001(*:3805)|97e5/([^/]++)/([^/]++)/([^/]++)/a597e5(*:3852)|16a8/([^/]++)/([^/]++)/([^/]++)/a516a8(*:3899))|1d0c6/([^/]++)/([^/]++)/([^/]++)/a1d0c6(*:3948)|6(?|84ec/([^/]++)/([^/]++)/([^/]++)/a684ec(*:3999)|6658/([^/]++)/([^/]++)/([^/]++)/a66658(*:4046))|3(?|f390/([^/]++)/([^/]++)/([^/]++)/a3f390(*:4098)|c65c/([^/]++)/([^/]++)/([^/]++)/a3c65c(*:4145))|d(?|61ab/([^/]++)/([^/]++)/([^/]++)/ad61ab(*:4197)|13a2/([^/]++)/([^/]++)/([^/]++)/ad13a2(*:4244)|972f/([^/]++)/([^/]++)/([^/]++)/ad972f(*:4291))|c(?|627a/([^/]++)/([^/]++)/([^/]++)/ac627a(*:4343)|1dd2/([^/]++)/([^/]++)/([^/]++)/ac1dd2(*:4390))|9(?|7da6/([^/]++)/([^/]++)/([^/]++)/a97da6(*:4442)|6b65/([^/]++)/([^/]++)/([^/]++)/a96b65(*:4489))|0(?|a080/([^/]++)/([^/]++)/([^/]++)/a0a080(*:4541)|2ffd/([^/]++)/([^/]++)/([^/]++)/a02ffd(*:4588)|1a03/([^/]++)/([^/]++)/([^/]++)/a01a03(*:4635))|4(?|a042/([^/]++)/([^/]++)/([^/]++)/a4a042(*:4687)|f236/([^/]++)/([^/]++)/([^/]++)/a4f236(*:4734)|9e94/([^/]++)/([^/]++)/([^/]++)/a49e94(*:4781))|2557a/([^/]++)/([^/]++)/([^/]++)/a2557a(*:4830)|b817c/([^/]++)/([^/]++)/([^/]++)/ab817c(*:4878))|/1(?|6(?|7909/([^/]++)/([^/]++)/([^/]++)/167909(*:4935)|a5cd/([^/]++)/([^/]++)/([^/]++)/16a5cd(*:4982)|51cf/([^/]++)/([^/]++)/([^/]++)/1651cf(*:5029))|f(?|0e3d/([^/]++)/([^/]++)/([^/]++)/1f0e3d(*:5081)|f(?|1de/([^/]++)/([^/]++)/([^/]++)/1ff1de(*:5131)|8a7/([^/]++)/([^/]++)/([^/]++)/1ff8a7(*:5177)))|8(?|2be0/([^/]++)/([^/]++)/([^/]++)/182be0(*:5230)|d804/([^/]++)/([^/]++)/([^/]++)/18d804(*:5277)|9977/([^/]++)/([^/]++)/([^/]++)/189977(*:5324))|c(?|383c/([^/]++)/([^/]++)/([^/]++)/1c383c(*:5376)|9ac0/([^/]++)/([^/]++)/([^/]++)/1c9ac0(*:5423))|9(?|ca14/([^/]++)/([^/]++)/([^/]++)/19ca14(*:5475)|f3cd/([^/]++)/([^/]++)/([^/]++)/19f3cd(*:5522))|7(?|e621/([^/]++)/([^/]++)/([^/]++)/17e621(*:5574)|0000/([^/]++)/([^/]++)/([^/]++)/170000(*:5621)|d63b/([^/]++)/([^/]++)/([^/]++)/17d63b(*:5668))|4(?|bfa6/([^/]++)/([^/]++)/([^/]++)/14bfa6(*:5720)|0f69/([^/]++)/([^/]++)/([^/]++)/140f69(*:5767)|9e96/([^/]++)/([^/]++)/([^/]++)/149e96(*:5814)|2949/([^/]++)/([^/]++)/([^/]++)/142949(*:5861))|a(?|fa34/([^/]++)/([^/]++)/([^/]++)/1afa34(*:5913)|5b1e/([^/]++)/([^/]++)/([^/]++)/1a5b1e(*:5960))|3(?|8(?|597/([^/]++)/([^/]++)/([^/]++)/138597(*:6015)|bb0/([^/]++)/([^/]++)/([^/]++)/138bb0(*:6061))|f(?|e9d/([^/]++)/([^/]++)/([^/]++)/13fe9d(*:6112)|989/([^/]++)/([^/]++)/([^/]++)/13f989(*:6158)|3cf/([^/]++)/([^/]++)/([^/]++)/13f3cf(*:6204)))|d7f7a/([^/]++)/([^/]++)/([^/]++)/1d7f7a(*:6254)|5(?|34b7/([^/]++)/([^/]++)/([^/]++)/1534b7(*:6305)|8f30/([^/]++)/([^/]++)/([^/]++)/158f30(*:6352)|4384/([^/]++)/([^/]++)/([^/]++)/154384(*:6399)|d4e8/([^/]++)/([^/]++)/([^/]++)/15d4e8(*:6446))|1(?|5f89/([^/]++)/([^/]++)/([^/]++)/115f89(*:6498)|b984/([^/]++)/([^/]++)/([^/]++)/11b984(*:6545))|068c6/([^/]++)/([^/]++)/([^/]++)/1068c6(*:6594)|be3bc/([^/]++)/([^/]++)/([^/]++)/1be3bc(*:6642))|/8(?|f(?|1(?|4e4/([^/]++)/([^/]++)/([^/]++)/8f14e4(*:6702)|21c/([^/]++)/([^/]++)/([^/]++)/8f121c(*:6748))|8551/([^/]++)/([^/]++)/([^/]++)/8f8551(*:6796)|5329/([^/]++)/([^/]++)/([^/]++)/8f5329(*:6843)|e009/([^/]++)/([^/]++)/([^/]++)/8fe009(*:6890))|e(?|296a/([^/]++)/([^/]++)/([^/]++)/8e296a(*:6942)|98d8/([^/]++)/([^/]++)/([^/]++)/8e98d8(*:6989)|fb10/([^/]++)/([^/]++)/([^/]++)/8efb10(*:7036)|6b42/([^/]++)/([^/]++)/([^/]++)/8e6b42(*:7083))|61398/([^/]++)/([^/]++)/([^/]++)/861398(*:7132)|1(?|2b4b/([^/]++)/([^/]++)/([^/]++)/812b4b(*:7183)|9f46/([^/]++)/([^/]++)/([^/]++)/819f46(*:7230)|6b11/([^/]++)/([^/]++)/([^/]++)/816b11(*:7277))|d(?|5e95/([^/]++)/([^/]++)/([^/]++)/8d5e95(*:7329)|3bba/([^/]++)/([^/]++)/([^/]++)/8d3bba(*:7376)|d48d/([^/]++)/([^/]++)/([^/]++)/8dd48d(*:7423)|7d8e/([^/]++)/([^/]++)/([^/]++)/8d7d8e(*:7470))|2(?|aa4b/([^/]++)/([^/]++)/([^/]++)/82aa4b(*:7522)|1(?|612/([^/]++)/([^/]++)/([^/]++)/821612(*:7572)|fa7/([^/]++)/([^/]++)/([^/]++)/821fa7(*:7618))|cec9/([^/]++)/([^/]++)/([^/]++)/82cec9(*:7666))|5(?|d8ce/([^/]++)/([^/]++)/([^/]++)/85d8ce(*:7718)|4d(?|6f/([^/]++)/([^/]++)/([^/]++)/854d6f(*:7768)|9f/([^/]++)/([^/]++)/([^/]++)/854d9f(*:7813)))|4d9ee/([^/]++)/([^/]++)/([^/]++)/84d9ee(*:7863)|c(?|19f5/([^/]++)/([^/]++)/([^/]++)/8c19f5(*:7914)|b22b/([^/]++)/([^/]++)/([^/]++)/8cb22b(*:7961))|39ab4/([^/]++)/([^/]++)/([^/]++)/839ab4(*:8010)|9f0fd/([^/]++)/([^/]++)/([^/]++)/89f0fd(*:8058)|bf121/([^/]++)/([^/]++)/([^/]++)/8bf121(*:8106)|77a9b/([^/]++)/([^/]++)/([^/]++)/877a9b(*:8154))|/4(?|5(?|c48c/([^/]++)/([^/]++)/([^/]++)/45c48c(*:8211)|fbc6/([^/]++)/([^/]++)/([^/]++)/45fbc6(*:8258))|e732c/([^/]++)/([^/]++)/([^/]++)/4e732c(*:8307)|4f683/([^/]++)/([^/]++)/([^/]++)/44f683(*:8355)|3(?|ec51/([^/]++)/([^/]++)/([^/]++)/43ec51(*:8406)|2aca/([^/]++)/([^/]++)/([^/]++)/432aca(*:8453))|c5(?|6ff/([^/]++)/([^/]++)/([^/]++)/4c56ff(*:8505)|bde/([^/]++)/([^/]++)/([^/]++)/4c5bde(*:8551))|2(?|a0e1/([^/]++)/([^/]++)/([^/]++)/42a0e1(*:8603)|e7aa/([^/]++)/([^/]++)/([^/]++)/42e7aa(*:8650)|998c/([^/]++)/([^/]++)/([^/]++)/42998c(*:8697)|8fca/([^/]++)/([^/]++)/([^/]++)/428fca(*:8744))|7(?|d1e9/([^/]++)/([^/]++)/([^/]++)/47d1e9(*:8796)|34ba/([^/]++)/([^/]++)/([^/]++)/4734ba(*:8843))|6ba9f/([^/]++)/([^/]++)/([^/]++)/46ba9f(*:8892)|8aedb/([^/]++)/([^/]++)/([^/]++)/48aedb(*:8940)|9(?|182f/([^/]++)/([^/]++)/([^/]++)/49182f(*:8991)|6e05/([^/]++)/([^/]++)/([^/]++)/496e05(*:9038)|ae49/([^/]++)/([^/]++)/([^/]++)/49ae49(*:9085))|0008b/([^/]++)/([^/]++)/([^/]++)/40008b(*:9134)|1(?|f1f1/([^/]++)/([^/]++)/([^/]++)/41f1f1(*:9185)|ae36/([^/]++)/([^/]++)/([^/]++)/41ae36(*:9232))|f(?|6ffe/([^/]++)/([^/]++)/([^/]++)/4f6ffe(*:9284)|4adc/([^/]++)/([^/]++)/([^/]++)/4f4adc(*:9331)))|/d(?|3(?|d944/([^/]++)/([^/]++)/([^/]++)/d3d944(*:9389)|9577/([^/]++)/([^/]++)/([^/]++)/d39577(*:9436)|4ab1/([^/]++)/([^/]++)/([^/]++)/d34ab1(*:9483))|6(?|7d8a/([^/]++)/([^/]++)/([^/]++)/d67d8a(*:9535)|4592/([^/]++)/([^/]++)/([^/]++)/d64592(*:9582)|baf6/([^/]++)/([^/]++)/([^/]++)/d6baf6(*:9629)|1e4b/([^/]++)/([^/]++)/([^/]++)/d61e4b(*:9676))|9(?|d4f4/([^/]++)/([^/]++)/([^/]++)/d9d4f4(*:9728)|6409/([^/]++)/([^/]++)/([^/]++)/d96409(*:9775)|47bf/([^/]++)/([^/]++)/([^/]++)/d947bf(*:9822)|fc5b/([^/]++)/([^/]++)/([^/]++)/d9fc5b(*:9869))|8(?|2c8d/([^/]++)/([^/]++)/([^/]++)/d82c8d(*:9921)|1f9c/([^/]++)/([^/]++)/([^/]++)/d81f9c(*:9968))|2(?|ddea/([^/]++)/([^/]++)/([^/]++)/d2ddea(*:10020)|96c1/([^/]++)/([^/]++)/([^/]++)/d296c1(*:10068))|0(?|9bf4/([^/]++)/([^/]++)/([^/]++)/d09bf4(*:10121)|7e70/([^/]++)/([^/]++)/([^/]++)/d07e70(*:10169))|1(?|f(?|e17/([^/]++)/([^/]++)/([^/]++)/d1fe17(*:10225)|491/([^/]++)/([^/]++)/([^/]++)/d1f491(*:10272)|255/([^/]++)/([^/]++)/([^/]++)/d1f255(*:10319))|c38a/([^/]++)/([^/]++)/([^/]++)/d1c38a(*:10368)|8f65/([^/]++)/([^/]++)/([^/]++)/d18f65(*:10416))|a4fb5/([^/]++)/([^/]++)/([^/]++)/da4fb5(*:10466)|b8e1a/([^/]++)/([^/]++)/([^/]++)/db8e1a(*:10515)|709f3/([^/]++)/([^/]++)/([^/]++)/d709f3(*:10564)|c(?|912a/([^/]++)/([^/]++)/([^/]++)/dc912a(*:10616)|6a64/([^/]++)/([^/]++)/([^/]++)/dc6a64(*:10664))|db306/([^/]++)/([^/]++)/([^/]++)/ddb306(*:10714))|/6(?|5(?|12bd/([^/]++)/([^/]++)/([^/]++)/6512bd(*:10772)|b9ee/([^/]++)/([^/]++)/([^/]++)/65b9ee(*:10820)|ded5/([^/]++)/([^/]++)/([^/]++)/65ded5(*:10868))|f(?|4922/([^/]++)/([^/]++)/([^/]++)/6f4922(*:10921)|3ef7/([^/]++)/([^/]++)/([^/]++)/6f3ef7(*:10969)|aa80/([^/]++)/([^/]++)/([^/]++)/6faa80(*:11017))|e(?|a(?|9ab/([^/]++)/([^/]++)/([^/]++)/6ea9ab(*:11073)|2ef/([^/]++)/([^/]++)/([^/]++)/6ea2ef(*:11120))|cbdd/([^/]++)/([^/]++)/([^/]++)/6ecbdd(*:11169))|3(?|64d3/([^/]++)/([^/]++)/([^/]++)/6364d3(*:11222)|dc7e/([^/]++)/([^/]++)/([^/]++)/63dc7e(*:11270)|923f/([^/]++)/([^/]++)/([^/]++)/63923f(*:11318))|c(?|8349/([^/]++)/([^/]++)/([^/]++)/6c8349(*:11371)|4b76/([^/]++)/([^/]++)/([^/]++)/6c4b76(*:11419)|dd60/([^/]++)/([^/]++)/([^/]++)/6cdd60(*:11467)|9882/([^/]++)/([^/]++)/([^/]++)/6c9882(*:11515)|524f/([^/]++)/([^/]++)/([^/]++)/6c524f(*:11563))|7(?|c6a1/([^/]++)/([^/]++)/([^/]++)/67c6a1(*:11616)|f7fb/([^/]++)/([^/]++)/([^/]++)/67f7fb(*:11664))|42e92/([^/]++)/([^/]++)/([^/]++)/642e92(*:11714)|6(?|f041/([^/]++)/([^/]++)/([^/]++)/66f041(*:11766)|808e/([^/]++)/([^/]++)/([^/]++)/66808e(*:11814)|3682/([^/]++)/([^/]++)/([^/]++)/663682(*:11862))|8(?|d30a/([^/]++)/([^/]++)/([^/]++)/68d30a(*:11915)|8396/([^/]++)/([^/]++)/([^/]++)/688396(*:11963)|5545/([^/]++)/([^/]++)/([^/]++)/685545(*:12011)|ce19/([^/]++)/([^/]++)/([^/]++)/68ce19(*:12059))|9(?|74ce/([^/]++)/([^/]++)/([^/]++)/6974ce(*:12112)|8d51/([^/]++)/([^/]++)/([^/]++)/698d51(*:12160)|adc1/([^/]++)/([^/]++)/([^/]++)/69adc1(*:12208)|cb3e/([^/]++)/([^/]++)/([^/]++)/69cb3e(*:12256))|da(?|900/([^/]++)/([^/]++)/([^/]++)/6da900(*:12309)|37d/([^/]++)/([^/]++)/([^/]++)/6da37d(*:12356))|21bf6/([^/]++)/([^/]++)/([^/]++)/621bf6(*:12406)|a9aed/([^/]++)/([^/]++)/([^/]++)/6a9aed(*:12455))|/9(?|b(?|f31c/([^/]++)/([^/]++)/([^/]++)/9bf31c(*:12513)|8619/([^/]++)/([^/]++)/([^/]++)/9b8619(*:12561)|04d1/([^/]++)/([^/]++)/([^/]++)/9b04d1(*:12609)|e40c/([^/]++)/([^/]++)/([^/]++)/9be40c(*:12657)|70e8/([^/]++)/([^/]++)/([^/]++)/9b70e8(*:12705))|8(?|f137/([^/]++)/([^/]++)/([^/]++)/98f137(*:12758)|dce8/([^/]++)/([^/]++)/([^/]++)/98dce8(*:12806)|72ed/([^/]++)/([^/]++)/([^/]++)/9872ed(*:12854)|b297/([^/]++)/([^/]++)/([^/]++)/98b297(*:12902))|a(?|1158/([^/]++)/([^/]++)/([^/]++)/9a1158(*:12955)|9687/([^/]++)/([^/]++)/([^/]++)/9a9687(*:13003))|f(?|6140/([^/]++)/([^/]++)/([^/]++)/9f6140(*:13056)|c3d7/([^/]++)/([^/]++)/([^/]++)/9fc3d7(*:13104)|d818/([^/]++)/([^/]++)/([^/]++)/9fd818(*:13152))|7(?|78d5/([^/]++)/([^/]++)/([^/]++)/9778d5(*:13205)|6652/([^/]++)/([^/]++)/([^/]++)/976652(*:13253)|9d47/([^/]++)/([^/]++)/([^/]++)/979d47(*:13301))|3db85/([^/]++)/([^/]++)/([^/]++)/93db85(*:13351)|2c(?|c22/([^/]++)/([^/]++)/([^/]++)/92cc22(*:13403)|8c9/([^/]++)/([^/]++)/([^/]++)/92c8c9(*:13450))|03ce9/([^/]++)/([^/]++)/([^/]++)/903ce9(*:13500)|6da2f/([^/]++)/([^/]++)/([^/]++)/96da2f(*:13549)|d(?|cb88/([^/]++)/([^/]++)/([^/]++)/9dcb88(*:13601)|fcd5/([^/]++)/([^/]++)/([^/]++)/9dfcd5(*:13649)|e6d1/([^/]++)/([^/]++)/([^/]++)/9de6d1(*:13697))|c(?|fdf1/([^/]++)/([^/]++)/([^/]++)/9cfdf1(*:13750)|838d/([^/]++)/([^/]++)/([^/]++)/9c838d(*:13798))|18(?|890/([^/]++)/([^/]++)/([^/]++)/918890(*:13851)|317/([^/]++)/([^/]++)/([^/]++)/918317(*:13898))|4(?|f6d7/([^/]++)/([^/]++)/([^/]++)/94f6d7(*:13951)|1e1a/([^/]++)/([^/]++)/([^/]++)/941e1a(*:13999)|31c8/([^/]++)/([^/]++)/([^/]++)/9431c8(*:14047)|61cc/([^/]++)/([^/]++)/([^/]++)/9461cc(*:14095))|50a41/([^/]++)/([^/]++)/([^/]++)/950a41(*:14145))|/7(?|0(?|efdf/([^/]++)/([^/]++)/([^/]++)/70efdf(*:14203)|5f21/([^/]++)/([^/]++)/([^/]++)/705f21(*:14251)|c639/([^/]++)/([^/]++)/([^/]++)/70c639(*:14299))|2b32a/([^/]++)/([^/]++)/([^/]++)/72b32a(*:14349)|f(?|39f8/([^/]++)/([^/]++)/([^/]++)/7f39f8(*:14401)|6ffa/([^/]++)/([^/]++)/([^/]++)/7f6ffa(*:14449)|1(?|de2/([^/]++)/([^/]++)/([^/]++)/7f1de2(*:14500)|00b/([^/]++)/([^/]++)/([^/]++)/7f100b(*:14547))|e1f8/([^/]++)/([^/]++)/([^/]++)/7fe1f8(*:14596))|3(?|5b90/([^/]++)/([^/]++)/([^/]++)/735b90(*:14649)|278a/([^/]++)/([^/]++)/([^/]++)/73278a(*:14697)|80ad/([^/]++)/([^/]++)/([^/]++)/7380ad(*:14745))|cbbc4/([^/]++)/([^/]++)/([^/]++)/7cbbc4(*:14795)|6(?|4796/([^/]++)/([^/]++)/([^/]++)/764796(*:14847)|dc61/([^/]++)/([^/]++)/([^/]++)/76dc61(*:14895))|e(?|f605/([^/]++)/([^/]++)/([^/]++)/7ef605(*:14948)|7757/([^/]++)/([^/]++)/([^/]++)/7e7757(*:14996)|a(?|be3/([^/]++)/([^/]++)/([^/]++)/7eabe3(*:15047)|cb5/([^/]++)/([^/]++)/([^/]++)/7eacb5(*:15094)))|5(?|7b50/([^/]++)/([^/]++)/([^/]++)/757b50(*:15148)|8874/([^/]++)/([^/]++)/([^/]++)/758874(*:15196)|fc09/([^/]++)/([^/]++)/([^/]++)/75fc09(*:15244))|4(?|db12/([^/]++)/([^/]++)/([^/]++)/74db12(*:15297)|071a/([^/]++)/([^/]++)/([^/]++)/74071a(*:15345))|a614f/([^/]++)/([^/]++)/([^/]++)/7a614f(*:15395)|d04bb/([^/]++)/([^/]++)/([^/]++)/7d04bb(*:15444))|/3(?|c(?|59dc/([^/]++)/([^/]++)/([^/]++)/3c59dc(*:15502)|ec07/([^/]++)/([^/]++)/([^/]++)/3cec07(*:15550)|7781/([^/]++)/([^/]++)/([^/]++)/3c7781(*:15598)|f166/([^/]++)/([^/]++)/([^/]++)/3cf166(*:15646))|7(?|693c/([^/]++)/([^/]++)/([^/]++)/37693c(*:15699)|a749/([^/]++)/([^/]++)/([^/]++)/37a749(*:15747)|bc2f/([^/]++)/([^/]++)/([^/]++)/37bc2f(*:15795)|1bce/([^/]++)/([^/]++)/([^/]++)/371bce(*:15843))|3(?|e75f/([^/]++)/([^/]++)/([^/]++)/33e75f(*:15896)|5f53/([^/]++)/([^/]++)/([^/]++)/335f53(*:15944))|4(?|1(?|73c/([^/]++)/([^/]++)/([^/]++)/34173c(*:16000)|6a7/([^/]++)/([^/]++)/([^/]++)/3416a7(*:16047))|ed06/([^/]++)/([^/]++)/([^/]++)/34ed06(*:16096))|2(?|95c7/([^/]++)/([^/]++)/([^/]++)/3295c7(*:16149)|bb90/([^/]++)/([^/]++)/([^/]++)/32bb90(*:16197)|0722/([^/]++)/([^/]++)/([^/]++)/320722(*:16245))|5(?|f4a8/([^/]++)/([^/]++)/([^/]++)/35f4a8(*:16298)|7a6f/([^/]++)/([^/]++)/([^/]++)/357a6f(*:16346)|2fe2/([^/]++)/([^/]++)/([^/]++)/352fe2(*:16394)|0510/([^/]++)/([^/]++)/([^/]++)/350510(*:16442))|ef815/([^/]++)/([^/]++)/([^/]++)/3ef815(*:16492)|8(?|b3ef/([^/]++)/([^/]++)/([^/]++)/38b3ef(*:16544)|af86/([^/]++)/([^/]++)/([^/]++)/38af86(*:16592)|db3a/([^/]++)/([^/]++)/([^/]++)/38db3a(*:16640))|d(?|ef18/([^/]++)/([^/]++)/([^/]++)/3def18(*:16693)|d48a/([^/]++)/([^/]++)/([^/]++)/3dd48a(*:16741))|9(?|88c7/([^/]++)/([^/]++)/([^/]++)/3988c7(*:16794)|0597/([^/]++)/([^/]++)/([^/]++)/390597(*:16842)|461a/([^/]++)/([^/]++)/([^/]++)/39461a(*:16890))|6(?|3663/([^/]++)/([^/]++)/([^/]++)/363663(*:16943)|44a6/([^/]++)/([^/]++)/([^/]++)/3644a6(*:16991)|660e/([^/]++)/([^/]++)/([^/]++)/36660e(*:17039))|1(?|fefc/([^/]++)/([^/]++)/([^/]++)/31fefc(*:17092)|0dcb/([^/]++)/([^/]++)/([^/]++)/310dcb(*:17140))|b8a61/([^/]++)/([^/]++)/([^/]++)/3b8a61(*:17190)|fe94a/([^/]++)/([^/]++)/([^/]++)/3fe94a(*:17239)|ad7c2/([^/]++)/([^/]++)/([^/]++)/3ad7c2(*:17288))|/b(?|6(?|d767/([^/]++)/([^/]++)/([^/]++)/b6d767(*:17346)|f047/([^/]++)/([^/]++)/([^/]++)/b6f047(*:17394))|53(?|b3a/([^/]++)/([^/]++)/([^/]++)/b53b3a(*:17447)|4ba/([^/]++)/([^/]++)/([^/]++)/b534ba(*:17494))|3(?|e3e3/([^/]++)/([^/]++)/([^/]++)/b3e3e3(*:17547)|967a/([^/]++)/([^/]++)/([^/]++)/b3967a(*:17595))|7(?|3ce3/([^/]++)/([^/]++)/([^/]++)/b73ce3(*:17648)|b16e/([^/]++)/([^/]++)/([^/]++)/b7b16e(*:17696))|d(?|4c9a/([^/]++)/([^/]++)/([^/]++)/bd4c9a(*:17749)|686f/([^/]++)/([^/]++)/([^/]++)/bd686f(*:17797))|f8229/([^/]++)/([^/]++)/([^/]++)/bf8229(*:17847)|1(?|d10e/([^/]++)/([^/]++)/([^/]++)/b1d10e(*:17899)|a59b/([^/]++)/([^/]++)/([^/]++)/b1a59b(*:17947))|c(?|be33/([^/]++)/([^/]++)/([^/]++)/bcbe33(*:18000)|6dc4/([^/]++)/([^/]++)/([^/]++)/bc6dc4(*:18048)|a82e/([^/]++)/([^/]++)/([^/]++)/bca82e(*:18096))|e(?|83ab/([^/]++)/([^/]++)/([^/]++)/be83ab(*:18149)|ed13/([^/]++)/([^/]++)/([^/]++)/beed13(*:18197))|2eb73/([^/]++)/([^/]++)/([^/]++)/b2eb73(*:18247)|83aac/([^/]++)/([^/]++)/([^/]++)/b83aac(*:18296)|ac916/([^/]++)/([^/]++)/([^/]++)/bac916(*:18345)|b(?|f94b/([^/]++)/([^/]++)/([^/]++)/bbf94b(*:18397)|cbff/([^/]++)/([^/]++)/([^/]++)/bbcbff(*:18445))|9228e/([^/]++)/([^/]++)/([^/]++)/b9228e(*:18495))|/0(?|2(?|e74f/([^/]++)/([^/]++)/([^/]++)/02e74f(*:18553)|522a/([^/]++)/([^/]++)/([^/]++)/02522a(*:18601)|66e3/([^/]++)/([^/]++)/([^/]++)/0266e3(*:18649))|9(?|3f65/([^/]++)/([^/]++)/([^/]++)/093f65(*:18702)|1d58/([^/]++)/([^/]++)/([^/]++)/091d58(*:18750))|7(?|2b03/([^/]++)/([^/]++)/([^/]++)/072b03(*:18803)|e1cd/([^/]++)/([^/]++)/([^/]++)/07e1cd(*:18851)|7(?|7d5/([^/]++)/([^/]++)/([^/]++)/0777d5(*:18902)|e29/([^/]++)/([^/]++)/([^/]++)/077e29(*:18949))|cdfd/([^/]++)/([^/]++)/([^/]++)/07cdfd(*:18998))|3(?|afdb/([^/]++)/([^/]++)/([^/]++)/03afdb(*:19051)|36dc/([^/]++)/([^/]++)/([^/]++)/0336dc(*:19099)|c6b0/([^/]++)/([^/]++)/([^/]++)/03c6b0(*:19147)|53ab/([^/]++)/([^/]++)/([^/]++)/0353ab(*:19195))|6(?|9059/([^/]++)/([^/]++)/([^/]++)/069059(*:19248)|4096/([^/]++)/([^/]++)/([^/]++)/064096(*:19296)|0ad9/([^/]++)/([^/]++)/([^/]++)/060ad9(*:19344)|138b/([^/]++)/([^/]++)/([^/]++)/06138b(*:19392)|eb61/([^/]++)/([^/]++)/([^/]++)/06eb61(*:19440))|1(?|3(?|d40/([^/]++)/([^/]++)/([^/]++)/013d40(*:19496)|86b/([^/]++)/([^/]++)/([^/]++)/01386b(*:19543))|161a/([^/]++)/([^/]++)/([^/]++)/01161a(*:19592)|9d38/([^/]++)/([^/]++)/([^/]++)/019d38(*:19640))|f(?|28b5/([^/]++)/([^/]++)/([^/]++)/0f28b5(*:19693)|49c8/([^/]++)/([^/]++)/([^/]++)/0f49c8(*:19741))|a(?|09c8/([^/]++)/([^/]++)/([^/]++)/0a09c8(*:19794)|a188/([^/]++)/([^/]++)/([^/]++)/0aa188(*:19842))|0(?|6f52/([^/]++)/([^/]++)/([^/]++)/006f52(*:19895)|4114/([^/]++)/([^/]++)/([^/]++)/004114(*:19943)|ec53/([^/]++)/([^/]++)/([^/]++)/00ec53(*:19991))|4(?|5117/([^/]++)/([^/]++)/([^/]++)/045117(*:20044)|0259/([^/]++)/([^/]++)/([^/]++)/040259(*:20092))|84b6f/([^/]++)/([^/]++)/([^/]++)/084b6f(*:20142)|e(?|6597/([^/]++)/([^/]++)/([^/]++)/0e6597(*:20194)|0193/([^/]++)/([^/]++)/([^/]++)/0e0193(*:20242))|bb4ae/([^/]++)/([^/]++)/([^/]++)/0bb4ae(*:20292)|5(?|049e/([^/]++)/([^/]++)/([^/]++)/05049e(*:20344)|84ce/([^/]++)/([^/]++)/([^/]++)/0584ce(*:20392)|f971/([^/]++)/([^/]++)/([^/]++)/05f971(*:20440))|c74b7/([^/]++)/([^/]++)/([^/]++)/0c74b7(*:20490)|d(?|0fd7/([^/]++)/([^/]++)/([^/]++)/0d0fd7(*:20542)|eb1c/([^/]++)/([^/]++)/([^/]++)/0deb1c(*:20590)))|/f(?|7(?|1(?|771/([^/]++)/([^/]++)/([^/]++)/f71771(*:20652)|849/([^/]++)/([^/]++)/([^/]++)/f71849(*:20699))|e6c8/([^/]++)/([^/]++)/([^/]++)/f7e6c8(*:20748)|6640/([^/]++)/([^/]++)/([^/]++)/f76640(*:20796)|3b76/([^/]++)/([^/]++)/([^/]++)/f73b76(*:20844)|4909/([^/]++)/([^/]++)/([^/]++)/f74909(*:20892)|70b6/([^/]++)/([^/]++)/([^/]++)/f770b6(*:20940))|4(?|57c5/([^/]++)/([^/]++)/([^/]++)/f457c5(*:20993)|b9ec/([^/]++)/([^/]++)/([^/]++)/f4b9ec(*:21041)|f6dc/([^/]++)/([^/]++)/([^/]++)/f4f6dc(*:21089))|c(?|490c/([^/]++)/([^/]++)/([^/]++)/fc490c(*:21142)|2213/([^/]++)/([^/]++)/([^/]++)/fc2213(*:21190)|cb60/([^/]++)/([^/]++)/([^/]++)/fccb60(*:21238))|b(?|d793/([^/]++)/([^/]++)/([^/]++)/fbd793(*:21291)|7b9f/([^/]++)/([^/]++)/([^/]++)/fb7b9f(*:21339))|0(?|33ab/([^/]++)/([^/]++)/([^/]++)/f033ab(*:21392)|935e/([^/]++)/([^/]++)/([^/]++)/f0935e(*:21440))|e(?|9fc2/([^/]++)/([^/]++)/([^/]++)/fe9fc2(*:21493)|131d/([^/]++)/([^/]++)/([^/]++)/fe131d(*:21541)|73f6/([^/]++)/([^/]++)/([^/]++)/fe73f6(*:21589))|8(?|9913/([^/]++)/([^/]++)/([^/]++)/f89913(*:21642)|c1f2/([^/]++)/([^/]++)/([^/]++)/f8c1f2(*:21690)|5454/([^/]++)/([^/]++)/([^/]++)/f85454(*:21738))|2(?|2170/([^/]++)/([^/]++)/([^/]++)/f22170(*:21791)|fc99/([^/]++)/([^/]++)/([^/]++)/f2fc99(*:21839))|a(?|7cdf/([^/]++)/([^/]++)/([^/]++)/fa7cdf(*:21892)|a9af/([^/]++)/([^/]++)/([^/]++)/faa9af(*:21940))|340f1/([^/]++)/([^/]++)/([^/]++)/f340f1(*:21990)|9(?|0f2a/([^/]++)/([^/]++)/([^/]++)/f90f2a(*:22042)|b902/([^/]++)/([^/]++)/([^/]++)/f9b902(*:22090))|fd52f/([^/]++)/([^/]++)/([^/]++)/ffd52f(*:22140)|61d69/([^/]++)/([^/]++)/([^/]++)/f61d69(*:22189)|5f859/([^/]++)/([^/]++)/([^/]++)/f5f859(*:22238)|1b6f2/([^/]++)/([^/]++)/([^/]++)/f1b6f2(*:22287))|/2(?|8(?|3802/([^/]++)/([^/]++)/([^/]++)/283802(*:22345)|dd2c/([^/]++)/([^/]++)/([^/]++)/28dd2c(*:22393)|9dff/([^/]++)/([^/]++)/([^/]++)/289dff(*:22441)|f0b8/([^/]++)/([^/]++)/([^/]++)/28f0b8(*:22489))|a(?|38a4/([^/]++)/([^/]++)/([^/]++)/2a38a4(*:22542)|79ea/([^/]++)/([^/]++)/([^/]++)/2a79ea(*:22590))|6(?|657d/([^/]++)/([^/]++)/([^/]++)/26657d(*:22643)|e359/([^/]++)/([^/]++)/([^/]++)/26e359(*:22691)|3373/([^/]++)/([^/]++)/([^/]++)/263373(*:22739))|7(?|23d0/([^/]++)/([^/]++)/([^/]++)/2723d0(*:22792)|4ad4/([^/]++)/([^/]++)/([^/]++)/274ad4(*:22840))|b(?|4492/([^/]++)/([^/]++)/([^/]++)/2b4492(*:22893)|24d4/([^/]++)/([^/]++)/([^/]++)/2b24d4(*:22941))|0(?|2cb9/([^/]++)/([^/]++)/([^/]++)/202cb9(*:22994)|f075/([^/]++)/([^/]++)/([^/]++)/20f075(*:23042)|50e0/([^/]++)/([^/]++)/([^/]++)/2050e0(*:23090))|f(?|2b26/([^/]++)/([^/]++)/([^/]++)/2f2b26(*:23143)|5570/([^/]++)/([^/]++)/([^/]++)/2f5570(*:23191))|4(?|b16f/([^/]++)/([^/]++)/([^/]++)/24b16f(*:23244)|8e84/([^/]++)/([^/]++)/([^/]++)/248e84(*:23292)|21fc/([^/]++)/([^/]++)/([^/]++)/2421fc(*:23340))|5(?|b282/([^/]++)/([^/]++)/([^/]++)/25b282(*:23393)|0cf8/([^/]++)/([^/]++)/([^/]++)/250cf8(*:23441)|ddc0/([^/]++)/([^/]++)/([^/]++)/25ddc0(*:23489))|18a0a/([^/]++)/([^/]++)/([^/]++)/218a0a(*:23539))|/5(?|4229a/([^/]++)/([^/]++)/([^/]++)/54229a(*:23594)|f(?|93f9/([^/]++)/([^/]++)/([^/]++)/5f93f9(*:23646)|d0b3/([^/]++)/([^/]++)/([^/]++)/5fd0b3(*:23694))|ef(?|0(?|59/([^/]++)/([^/]++)/([^/]++)/5ef059(*:23750)|b4/([^/]++)/([^/]++)/([^/]++)/5ef0b4(*:23796))|698/([^/]++)/([^/]++)/([^/]++)/5ef698(*:23844))|8(?|78a7/([^/]++)/([^/]++)/([^/]++)/5878a7(*:23897)|a2fc/([^/]++)/([^/]++)/([^/]++)/58a2fc(*:23945)|238e/([^/]++)/([^/]++)/([^/]++)/58238e(*:23993))|7(?|aeee/([^/]++)/([^/]++)/([^/]++)/57aeee(*:24046)|7(?|ef1/([^/]++)/([^/]++)/([^/]++)/577ef1(*:24097)|bcc/([^/]++)/([^/]++)/([^/]++)/577bcc(*:24144))|37c6/([^/]++)/([^/]++)/([^/]++)/5737c6(*:24193))|3(?|9fd5/([^/]++)/([^/]++)/([^/]++)/539fd5(*:24246)|c3bc/([^/]++)/([^/]++)/([^/]++)/53c3bc(*:24294))|5(?|5d67/([^/]++)/([^/]++)/([^/]++)/555d67(*:24347)|0a14/([^/]++)/([^/]++)/([^/]++)/550a14(*:24395)|9cb9/([^/]++)/([^/]++)/([^/]++)/559cb9(*:24443)|a7cf/([^/]++)/([^/]++)/([^/]++)/55a7cf(*:24491))|02e4a/([^/]++)/([^/]++)/([^/]++)/502e4a(*:24541)|b8add/([^/]++)/([^/]++)/([^/]++)/5b8add(*:24590)|2720e/([^/]++)/([^/]++)/([^/]++)/52720e(*:24639)|a4b25/([^/]++)/([^/]++)/([^/]++)/5a4b25(*:24688)|1d92b/([^/]++)/([^/]++)/([^/]++)/51d92b(*:24737)|98b3e/([^/]++)/([^/]++)/([^/]++)/598b3e(*:24786)))/?$\"),\n  24786 => ART.create_regex(\"^(?|/5(?|b69b9/([^/]++)/([^/]++)/([^/]++)/5b69b9(*:24837)|9(?|b90e/([^/]++)/([^/]++)/([^/]++)/59b90e(*:24889)|c330/([^/]++)/([^/]++)/([^/]++)/59c330(*:24937))|3(?|fde9/([^/]++)/([^/]++)/([^/]++)/53fde9(*:24990)|e3a7/([^/]++)/([^/]++)/([^/]++)/53e3a7(*:25038))|e(?|a164/([^/]++)/([^/]++)/([^/]++)/5ea164(*:25091)|3881/([^/]++)/([^/]++)/([^/]++)/5e3881(*:25139)|9f92/([^/]++)/([^/]++)/([^/]++)/5e9f92(*:25187)|c91a/([^/]++)/([^/]++)/([^/]++)/5ec91a(*:25235))|7(?|3703/([^/]++)/([^/]++)/([^/]++)/573703(*:25288)|51ec/([^/]++)/([^/]++)/([^/]++)/5751ec(*:25336)|05e1/([^/]++)/([^/]++)/([^/]++)/5705e1(*:25384))|8(?|ae74/([^/]++)/([^/]++)/([^/]++)/58ae74(*:25437)|d4d1/([^/]++)/([^/]++)/([^/]++)/58d4d1(*:25485)|07a6/([^/]++)/([^/]++)/([^/]++)/5807a6(*:25533)|e4d4/([^/]++)/([^/]++)/([^/]++)/58e4d4(*:25581))|d(?|44ee/([^/]++)/([^/]++)/([^/]++)/5d44ee(*:25634)|d9db/([^/]++)/([^/]++)/([^/]++)/5dd9db(*:25682))|5(?|b37c/([^/]++)/([^/]++)/([^/]++)/55b37c(*:25735)|743c/([^/]++)/([^/]++)/([^/]++)/55743c(*:25783)|6f39/([^/]++)/([^/]++)/([^/]++)/556f39(*:25831))|c(?|0492/([^/]++)/([^/]++)/([^/]++)/5c0492(*:25884)|572e/([^/]++)/([^/]++)/([^/]++)/5c572e(*:25932)|9362/([^/]++)/([^/]++)/([^/]++)/5c9362(*:25980))|4(?|8731/([^/]++)/([^/]++)/([^/]++)/548731(*:26033)|a367/([^/]++)/([^/]++)/([^/]++)/54a367(*:26081))|0(?|0e75/([^/]++)/([^/]++)/([^/]++)/500e75(*:26134)|c3d7/([^/]++)/([^/]++)/([^/]++)/50c3d7(*:26182))|f(?|2c22/([^/]++)/([^/]++)/([^/]++)/5f2c22(*:26235)|0f5e/([^/]++)/([^/]++)/([^/]++)/5f0f5e(*:26283))|1ef18/([^/]++)/([^/]++)/([^/]++)/51ef18(*:26333))|/b(?|5(?|b41f/([^/]++)/([^/]++)/([^/]++)/b5b41f(*:26391)|dc4e/([^/]++)/([^/]++)/([^/]++)/b5dc4e(*:26439)|6a18/([^/]++)/([^/]++)/([^/]++)/b56a18(*:26487)|5ec2/([^/]++)/([^/]++)/([^/]++)/b55ec2(*:26535))|337e8/([^/]++)/([^/]++)/([^/]++)/b337e8(*:26585)|a(?|2fd3/([^/]++)/([^/]++)/([^/]++)/ba2fd3(*:26637)|3866/([^/]++)/([^/]++)/([^/]++)/ba3866(*:26685))|2(?|eeb7/([^/]++)/([^/]++)/([^/]++)/b2eeb7(*:26738)|f627/([^/]++)/([^/]++)/([^/]++)/b2f627(*:26786))|7(?|3dfe/([^/]++)/([^/]++)/([^/]++)/b73dfe(*:26839)|bb35/([^/]++)/([^/]++)/([^/]++)/b7bb35(*:26887)|ee6f/([^/]++)/([^/]++)/([^/]++)/b7ee6f(*:26935)|892f/([^/]++)/([^/]++)/([^/]++)/b7892f(*:26983)|0683/([^/]++)/([^/]++)/([^/]++)/b70683(*:27031))|4(?|288d/([^/]++)/([^/]++)/([^/]++)/b4288d(*:27084)|a528/([^/]++)/([^/]++)/([^/]++)/b4a528(*:27132))|e(?|3159/([^/]++)/([^/]++)/([^/]++)/be3159(*:27185)|b22f/([^/]++)/([^/]++)/([^/]++)/beb22f(*:27233)|a595/([^/]++)/([^/]++)/([^/]++)/bea595(*:27281))|1(?|eec3/([^/]++)/([^/]++)/([^/]++)/b1eec3(*:27334)|37fd/([^/]++)/([^/]++)/([^/]++)/b137fd(*:27382))|0(?|56eb/([^/]++)/([^/]++)/([^/]++)/b056eb(*:27435)|b183/([^/]++)/([^/]++)/([^/]++)/b0b183(*:27483))|f6276/([^/]++)/([^/]++)/([^/]++)/bf6276(*:27533)|6(?|edc1/([^/]++)/([^/]++)/([^/]++)/b6edc1(*:27585)|a108/([^/]++)/([^/]++)/([^/]++)/b6a108(*:27633))|86e8d/([^/]++)/([^/]++)/([^/]++)/b86e8d(*:27683))|/2(?|8(?|5e19/([^/]++)/([^/]++)/([^/]++)/285e19(*:27741)|2(?|3f4/([^/]++)/([^/]++)/([^/]++)/2823f4(*:27792)|67a/([^/]++)/([^/]++)/([^/]++)/28267a(*:27839))|8cc0/([^/]++)/([^/]++)/([^/]++)/288cc0(*:27888)|7e03/([^/]++)/([^/]++)/([^/]++)/287e03(*:27936))|d(?|6cc4/([^/]++)/([^/]++)/([^/]++)/2d6cc4(*:27989)|ea61/([^/]++)/([^/]++)/([^/]++)/2dea61(*:28037)|ace7/([^/]++)/([^/]++)/([^/]++)/2dace7(*:28085))|b(?|8a61/([^/]++)/([^/]++)/([^/]++)/2b8a61(*:28138)|b232/([^/]++)/([^/]++)/([^/]++)/2bb232(*:28186)|a596/([^/]++)/([^/]++)/([^/]++)/2ba596(*:28234)|cab9/([^/]++)/([^/]++)/([^/]++)/2bcab9(*:28282))|9(?|8f95/([^/]++)/([^/]++)/([^/]++)/298f95(*:28335)|1597/([^/]++)/([^/]++)/([^/]++)/291597(*:28383))|58be1/([^/]++)/([^/]++)/([^/]++)/258be1(*:28433)|3(?|3509/([^/]++)/([^/]++)/([^/]++)/233509(*:28485)|ce18/([^/]++)/([^/]++)/([^/]++)/23ce18(*:28533))|6(?|dd0d/([^/]++)/([^/]++)/([^/]++)/26dd0d(*:28586)|408f/([^/]++)/([^/]++)/([^/]++)/26408f(*:28634))|f(?|37d1/([^/]++)/([^/]++)/([^/]++)/2f37d1(*:28687)|885d/([^/]++)/([^/]++)/([^/]++)/2f885d(*:28735))|2(?|91d2/([^/]++)/([^/]++)/([^/]++)/2291d2(*:28788)|ac3c/([^/]++)/([^/]++)/([^/]++)/22ac3c(*:28836)|fb0c/([^/]++)/([^/]++)/([^/]++)/22fb0c(*:28884))|4(?|6819/([^/]++)/([^/]++)/([^/]++)/246819(*:28937)|896e/([^/]++)/([^/]++)/([^/]++)/24896e(*:28985))|a(?|fe45/([^/]++)/([^/]++)/([^/]++)/2afe45(*:29038)|084e/([^/]++)/([^/]++)/([^/]++)/2a084e(*:29086)|9d12/([^/]++)/([^/]++)/([^/]++)/2a9d12(*:29134)|b564/([^/]++)/([^/]++)/([^/]++)/2ab564(*:29182))|1(?|7eed/([^/]++)/([^/]++)/([^/]++)/217eed(*:29235)|0f76/([^/]++)/([^/]++)/([^/]++)/210f76(*:29283))|e65f2/([^/]++)/([^/]++)/([^/]++)/2e65f2(*:29333)|ca65f/([^/]++)/([^/]++)/([^/]++)/2ca65f(*:29382)|0aee3/([^/]++)/([^/]++)/([^/]++)/20aee3(*:29431))|/e(?|8(?|c065/([^/]++)/([^/]++)/([^/]++)/e8c065(*:29489)|20a4/([^/]++)/([^/]++)/([^/]++)/e820a4(*:29537))|2(?|230b/([^/]++)/([^/]++)/([^/]++)/e2230b(*:29590)|a2dc/([^/]++)/([^/]++)/([^/]++)/e2a2dc(*:29638)|05ee/([^/]++)/([^/]++)/([^/]++)/e205ee(*:29686))|b(?|d962/([^/]++)/([^/]++)/([^/]++)/ebd962(*:29739)|6fdc/([^/]++)/([^/]++)/([^/]++)/eb6fdc(*:29787))|d(?|265b/([^/]++)/([^/]++)/([^/]++)/ed265b(*:29840)|fbe1/([^/]++)/([^/]++)/([^/]++)/edfbe1(*:29888)|e7e2/([^/]++)/([^/]++)/([^/]++)/ede7e2(*:29936))|6(?|b4b2/([^/]++)/([^/]++)/([^/]++)/e6b4b2(*:29989)|cb2a/([^/]++)/([^/]++)/([^/]++)/e6cb2a(*:30037))|5(?|f6ad/([^/]++)/([^/]++)/([^/]++)/e5f6ad(*:30090)|55eb/([^/]++)/([^/]++)/([^/]++)/e555eb(*:30138)|841d/([^/]++)/([^/]++)/([^/]++)/e5841d(*:30186)|7c6b/([^/]++)/([^/]++)/([^/]++)/e57c6b(*:30234))|aae33/([^/]++)/([^/]++)/([^/]++)/eaae33(*:30284)|4(?|bb4c/([^/]++)/([^/]++)/([^/]++)/e4bb4c(*:30336)|9b8b/([^/]++)/([^/]++)/([^/]++)/e49b8b(*:30384))|7(?|0611/([^/]++)/([^/]++)/([^/]++)/e70611(*:30437)|f8a7/([^/]++)/([^/]++)/([^/]++)/e7f8a7(*:30485)|44f9/([^/]++)/([^/]++)/([^/]++)/e744f9(*:30533))|9(?|95f9/([^/]++)/([^/]++)/([^/]++)/e995f9(*:30586)|4550/([^/]++)/([^/]++)/([^/]++)/e94550(*:30634)|7ee2/([^/]++)/([^/]++)/([^/]++)/e97ee2(*:30682))|e(?|fc9e/([^/]++)/([^/]++)/([^/]++)/eefc9e(*:30735)|b69a/([^/]++)/([^/]++)/([^/]++)/eeb69a(*:30783))|0(?|7413/([^/]++)/([^/]++)/([^/]++)/e07413(*:30836)|cf1f/([^/]++)/([^/]++)/([^/]++)/e0cf1f(*:30884)|ec45/([^/]++)/([^/]++)/([^/]++)/e0ec45(*:30932))|f4e3b/([^/]++)/([^/]++)/([^/]++)/ef4e3b(*:30982)|c5aa0/([^/]++)/([^/]++)/([^/]++)/ec5aa0(*:31031))|/f(?|f(?|4d5f/([^/]++)/([^/]++)/([^/]++)/ff4d5f(*:31089)|eabd/([^/]++)/([^/]++)/([^/]++)/ffeabd(*:31137))|3(?|f27a/([^/]++)/([^/]++)/([^/]++)/f3f27a(*:31190)|8762/([^/]++)/([^/]++)/([^/]++)/f38762(*:31238))|4(?|be00/([^/]++)/([^/]++)/([^/]++)/f4be00(*:31291)|5526/([^/]++)/([^/]++)/([^/]++)/f45526(*:31339)|7d0a/([^/]++)/([^/]++)/([^/]++)/f47d0a(*:31387))|0(?|e52b/([^/]++)/([^/]++)/([^/]++)/f0e52b(*:31440)|adc8/([^/]++)/([^/]++)/([^/]++)/f0adc8(*:31488))|de926/([^/]++)/([^/]++)/([^/]++)/fde926(*:31538)|5(?|deae/([^/]++)/([^/]++)/([^/]++)/f5deae(*:31590)|7a2f/([^/]++)/([^/]++)/([^/]++)/f57a2f(*:31638))|7(?|6a89/([^/]++)/([^/]++)/([^/]++)/f76a89(*:31691)|9921/([^/]++)/([^/]++)/([^/]++)/f79921(*:31739)|e905/([^/]++)/([^/]++)/([^/]++)/f7e905(*:31787))|2(?|9c21/([^/]++)/([^/]++)/([^/]++)/f29c21(*:31840)|201f/([^/]++)/([^/]++)/([^/]++)/f2201f(*:31888))|a(?|e0b2/([^/]++)/([^/]++)/([^/]++)/fae0b2(*:31941)|14d4/([^/]++)/([^/]++)/([^/]++)/fa14d4(*:31989)|3a3c/([^/]++)/([^/]++)/([^/]++)/fa3a3c(*:32037)|83a1/([^/]++)/([^/]++)/([^/]++)/fa83a1(*:32085))|c(?|cb3c/([^/]++)/([^/]++)/([^/]++)/fccb3c(*:32138)|8001/([^/]++)/([^/]++)/([^/]++)/fc8001(*:32186)|3cf4/([^/]++)/([^/]++)/([^/]++)/fc3cf4(*:32234)|4930/([^/]++)/([^/]++)/([^/]++)/fc4930(*:32282))|64eac/([^/]++)/([^/]++)/([^/]++)/f64eac(*:32332)|b8970/([^/]++)/([^/]++)/([^/]++)/fb8970(*:32381)|1c159/([^/]++)/([^/]++)/([^/]++)/f1c159(*:32430)|9(?|028f/([^/]++)/([^/]++)/([^/]++)/f9028f(*:32482)|a40a/([^/]++)/([^/]++)/([^/]++)/f9a40a(*:32530))|e(?|8c15/([^/]++)/([^/]++)/([^/]++)/fe8c15(*:32583)|c8d4/([^/]++)/([^/]++)/([^/]++)/fec8d4(*:32631)|7ee8/([^/]++)/([^/]++)/([^/]++)/fe7ee8(*:32679)))|/3(?|8(?|9(?|bc7/([^/]++)/([^/]++)/([^/]++)/389bc7(*:32741)|13e/([^/]++)/([^/]++)/([^/]++)/38913e(*:32788))|71bd/([^/]++)/([^/]++)/([^/]++)/3871bd(*:32837))|d(?|c487/([^/]++)/([^/]++)/([^/]++)/3dc487(*:32890)|2d8c/([^/]++)/([^/]++)/([^/]++)/3d2d8c(*:32938)|8e28/([^/]++)/([^/]++)/([^/]++)/3d8e28(*:32986)|f1d4/([^/]++)/([^/]++)/([^/]++)/3df1d4(*:33034))|7f0e8/([^/]++)/([^/]++)/([^/]++)/37f0e8(*:33084)|3(?|e807/([^/]++)/([^/]++)/([^/]++)/33e807(*:33136)|28bd/([^/]++)/([^/]++)/([^/]++)/3328bd(*:33184))|a(?|0(?|772/([^/]++)/([^/]++)/([^/]++)/3a0772(*:33240)|66b/([^/]++)/([^/]++)/([^/]++)/3a066b(*:33287))|835d/([^/]++)/([^/]++)/([^/]++)/3a835d(*:33336))|0(?|bb38/([^/]++)/([^/]++)/([^/]++)/30bb38(*:33389)|3ed4/([^/]++)/([^/]++)/([^/]++)/303ed4(*:33437)|ef30/([^/]++)/([^/]++)/([^/]++)/30ef30(*:33485)|1ad0/([^/]++)/([^/]++)/([^/]++)/301ad0(*:33533))|4(?|9389/([^/]++)/([^/]++)/([^/]++)/349389(*:33586)|35c3/([^/]++)/([^/]++)/([^/]++)/3435c3(*:33634))|62(?|1f1/([^/]++)/([^/]++)/([^/]++)/3621f1(*:33687)|e80/([^/]++)/([^/]++)/([^/]++)/362e80(*:33734))|5(?|cf86/([^/]++)/([^/]++)/([^/]++)/35cf86(*:33787)|2407/([^/]++)/([^/]++)/([^/]++)/352407(*:33835))|2b30a/([^/]++)/([^/]++)/([^/]++)/32b30a(*:33885)|1839b/([^/]++)/([^/]++)/([^/]++)/31839b(*:33934)|b(?|5dca/([^/]++)/([^/]++)/([^/]++)/3b5dca(*:33986)|3dba/([^/]++)/([^/]++)/([^/]++)/3b3dba(*:34034))|e89eb/([^/]++)/([^/]++)/([^/]++)/3e89eb(*:34084)|cef96/([^/]++)/([^/]++)/([^/]++)/3cef96(*:34133))|/0(?|8(?|7408/([^/]++)/([^/]++)/([^/]++)/087408(*:34191)|b255/([^/]++)/([^/]++)/([^/]++)/08b255(*:34239)|c543/([^/]++)/([^/]++)/([^/]++)/08c543(*:34287)|d986/([^/]++)/([^/]++)/([^/]++)/08d986(*:34335)|419b/([^/]++)/([^/]++)/([^/]++)/08419b(*:34383))|7(?|563a/([^/]++)/([^/]++)/([^/]++)/07563a(*:34436)|6a0c/([^/]++)/([^/]++)/([^/]++)/076a0c(*:34484)|a96b/([^/]++)/([^/]++)/([^/]++)/07a96b(*:34532)|c580/([^/]++)/([^/]++)/([^/]++)/07c580(*:34580)|8719/([^/]++)/([^/]++)/([^/]++)/078719(*:34628))|f(?|cbc6/([^/]++)/([^/]++)/([^/]++)/0fcbc6(*:34681)|9661/([^/]++)/([^/]++)/([^/]++)/0f9661(*:34729)|f(?|39b/([^/]++)/([^/]++)/([^/]++)/0ff39b(*:34780)|803/([^/]++)/([^/]++)/([^/]++)/0ff803(*:34827))|840b/([^/]++)/([^/]++)/([^/]++)/0f840b(*:34876))|1(?|f78b/([^/]++)/([^/]++)/([^/]++)/01f78b(*:34929)|3a00/([^/]++)/([^/]++)/([^/]++)/013a00(*:34977)|8825/([^/]++)/([^/]++)/([^/]++)/018825(*:35025))|6(?|9(?|d3b/([^/]++)/([^/]++)/([^/]++)/069d3b(*:35081)|97f/([^/]++)/([^/]++)/([^/]++)/06997f(*:35128))|1412/([^/]++)/([^/]++)/([^/]++)/061412(*:35177))|4(?|ecb1/([^/]++)/([^/]++)/([^/]++)/04ecb1(*:35230)|3c3d/([^/]++)/([^/]++)/([^/]++)/043c3d(*:35278))|0ac8e/([^/]++)/([^/]++)/([^/]++)/00ac8e(*:35328)|5(?|1e4e/([^/]++)/([^/]++)/([^/]++)/051e4e(*:35380)|37fb/([^/]++)/([^/]++)/([^/]++)/0537fb(*:35428))|d(?|7de1/([^/]++)/([^/]++)/([^/]++)/0d7de1(*:35481)|3180/([^/]++)/([^/]++)/([^/]++)/0d3180(*:35529)|0871/([^/]++)/([^/]++)/([^/]++)/0d0871(*:35577))|cb929/([^/]++)/([^/]++)/([^/]++)/0cb929(*:35627)|2(?|a32a/([^/]++)/([^/]++)/([^/]++)/02a32a(*:35679)|4d7f/([^/]++)/([^/]++)/([^/]++)/024d7f(*:35727))|efe32/([^/]++)/([^/]++)/([^/]++)/0efe32(*:35777)|a113e/([^/]++)/([^/]++)/([^/]++)/0a113e(*:35826)|b8aff/([^/]++)/([^/]++)/([^/]++)/0b8aff(*:35875))|/a(?|7(?|6088/([^/]++)/([^/]++)/([^/]++)/a76088(*:35933)|aeed/([^/]++)/([^/]++)/([^/]++)/a7aeed(*:35981)|33fa/([^/]++)/([^/]++)/([^/]++)/a733fa(*:36029))|9a(?|665/([^/]++)/([^/]++)/([^/]++)/a9a665(*:36082)|1d5/([^/]++)/([^/]++)/([^/]++)/a9a1d5(*:36129))|8(?|6c45/([^/]++)/([^/]++)/([^/]++)/a86c45(*:36182)|849b/([^/]++)/([^/]++)/([^/]++)/a8849b(*:36230)|e(?|864/([^/]++)/([^/]++)/([^/]++)/a8e864(*:36281)|cba/([^/]++)/([^/]++)/([^/]++)/a8ecba(*:36328)))|c(?|c3e0/([^/]++)/([^/]++)/([^/]++)/acc3e0(*:36382)|f4b8/([^/]++)/([^/]++)/([^/]++)/acf4b8(*:36430))|b(?|d815/([^/]++)/([^/]++)/([^/]++)/abd815(*:36483)|233b/([^/]++)/([^/]++)/([^/]++)/ab233b(*:36531)|a3b6/([^/]++)/([^/]++)/([^/]++)/aba3b6(*:36579)|88b1/([^/]++)/([^/]++)/([^/]++)/ab88b1(*:36627))|5(?|3240/([^/]++)/([^/]++)/([^/]++)/a53240(*:36680)|cdd4/([^/]++)/([^/]++)/([^/]++)/a5cdd4(*:36728))|f(?|d(?|483/([^/]++)/([^/]++)/([^/]++)/afd483(*:36784)|a33/([^/]++)/([^/]++)/([^/]++)/afda33(*:36831))|f162/([^/]++)/([^/]++)/([^/]++)/aff162(*:36880))|e(?|0eb3/([^/]++)/([^/]++)/([^/]++)/ae0eb3(*:36933)|b313/([^/]++)/([^/]++)/([^/]++)/aeb313(*:36981))|1(?|d33d/([^/]++)/([^/]++)/([^/]++)/a1d33d(*:37034)|140a/([^/]++)/([^/]++)/([^/]++)/a1140a(*:37082))|ddfa9/([^/]++)/([^/]++)/([^/]++)/addfa9(*:37132)|6(?|7f09/([^/]++)/([^/]++)/([^/]++)/a67f09(*:37184)|4c94/([^/]++)/([^/]++)/([^/]++)/a64c94(*:37232))|a169b/([^/]++)/([^/]++)/([^/]++)/aa169b(*:37282)|4300b/([^/]++)/([^/]++)/([^/]++)/a4300b(*:37331)|3d68b/([^/]++)/([^/]++)/([^/]++)/a3d68b(*:37380))|/1(?|0(?|a(?|7cd/([^/]++)/([^/]++)/([^/]++)/10a7cd(*:37441)|5ab/([^/]++)/([^/]++)/([^/]++)/10a5ab(*:37488))|9a0c/([^/]++)/([^/]++)/([^/]++)/109a0c(*:37537))|3f320/([^/]++)/([^/]++)/([^/]++)/13f320(*:37587)|6(?|c222/([^/]++)/([^/]++)/([^/]++)/16c222(*:37639)|8908/([^/]++)/([^/]++)/([^/]++)/168908(*:37687))|5(?|de21/([^/]++)/([^/]++)/([^/]++)/15de21(*:37740)|95af/([^/]++)/([^/]++)/([^/]++)/1595af(*:37788))|1(?|b921/([^/]++)/([^/]++)/([^/]++)/11b921(*:37841)|4193/([^/]++)/([^/]++)/([^/]++)/114193(*:37889))|bb91f/([^/]++)/([^/]++)/([^/]++)/1bb91f(*:37939)|7(?|28ef/([^/]++)/([^/]++)/([^/]++)/1728ef(*:37991)|c276/([^/]++)/([^/]++)/([^/]++)/17c276(*:38039)|0c94/([^/]++)/([^/]++)/([^/]++)/170c94(*:38087))|85(?|c29/([^/]++)/([^/]++)/([^/]++)/185c29(*:38140)|e65/([^/]++)/([^/]++)/([^/]++)/185e65(*:38187))|9(?|2fc0/([^/]++)/([^/]++)/([^/]++)/192fc0(*:38240)|b(?|c91/([^/]++)/([^/]++)/([^/]++)/19bc91(*:38291)|650/([^/]++)/([^/]++)/([^/]++)/19b650(*:38338))|05ae/([^/]++)/([^/]++)/([^/]++)/1905ae(*:38387))|e(?|cfb4/([^/]++)/([^/]++)/([^/]++)/1ecfb4(*:38440)|fa39/([^/]++)/([^/]++)/([^/]++)/1efa39(*:38488)|056d/([^/]++)/([^/]++)/([^/]++)/1e056d(*:38536))|aa48f/([^/]++)/([^/]++)/([^/]++)/1aa48f(*:38586)|f(?|c214/([^/]++)/([^/]++)/([^/]++)/1fc214(*:38638)|5089/([^/]++)/([^/]++)/([^/]++)/1f5089(*:38686)|4477/([^/]++)/([^/]++)/([^/]++)/1f4477(*:38734))|c(?|c363/([^/]++)/([^/]++)/([^/]++)/1cc363(*:38787)|1d4d/([^/]++)/([^/]++)/([^/]++)/1c1d4d(*:38835)|e927/([^/]++)/([^/]++)/([^/]++)/1ce927(*:38883)))|/6(?|3(?|538f/([^/]++)/([^/]++)/([^/]++)/63538f(*:38942)|2cee/([^/]++)/([^/]++)/([^/]++)/632cee(*:38990)|95eb/([^/]++)/([^/]++)/([^/]++)/6395eb(*:39038))|9(?|421f/([^/]++)/([^/]++)/([^/]++)/69421f(*:39091)|2f93/([^/]++)/([^/]++)/([^/]++)/692f93(*:39139))|5658f/([^/]++)/([^/]++)/([^/]++)/65658f(*:39189)|4(?|7bba/([^/]++)/([^/]++)/([^/]++)/647bba(*:39241)|223c/([^/]++)/([^/]++)/([^/]++)/64223c(*:39289))|e(?|2713/([^/]++)/([^/]++)/([^/]++)/6e2713(*:39342)|0721/([^/]++)/([^/]++)/([^/]++)/6e0721(*:39390)|7b33/([^/]++)/([^/]++)/([^/]++)/6e7b33(*:39438))|0(?|5ff7/([^/]++)/([^/]++)/([^/]++)/605ff7(*:39491)|8159/([^/]++)/([^/]++)/([^/]++)/608159(*:39539))|a(?|ca97/([^/]++)/([^/]++)/([^/]++)/6aca97(*:39592)|10bb/([^/]++)/([^/]++)/([^/]++)/6a10bb(*:39640)|ab12/([^/]++)/([^/]++)/([^/]++)/6aab12(*:39688))|7(?|66aa/([^/]++)/([^/]++)/([^/]++)/6766aa(*:39741)|e103/([^/]++)/([^/]++)/([^/]++)/67e103(*:39789)|d(?|96d/([^/]++)/([^/]++)/([^/]++)/67d96d(*:39840)|16d/([^/]++)/([^/]++)/([^/]++)/67d16d(*:39887))|0e8a/([^/]++)/([^/]++)/([^/]++)/670e8a(*:39936)|7e09/([^/]++)/([^/]++)/([^/]++)/677e09(*:39984))|8(?|264b/([^/]++)/([^/]++)/([^/]++)/68264b(*:40037)|053a/([^/]++)/([^/]++)/([^/]++)/68053a(*:40085))|c(?|2979/([^/]++)/([^/]++)/([^/]++)/6c2979(*:40138)|d67d/([^/]++)/([^/]++)/([^/]++)/6cd67d(*:40186)|3cf7/([^/]++)/([^/]++)/([^/]++)/6c3cf7(*:40234)|fe0e/([^/]++)/([^/]++)/([^/]++)/6cfe0e(*:40282))|bc24f/([^/]++)/([^/]++)/([^/]++)/6bc24f(*:40332)|f2268/([^/]++)/([^/]++)/([^/]++)/6f2268(*:40381)|1b4a6/([^/]++)/([^/]++)/([^/]++)/61b4a6(*:40430)|21461/([^/]++)/([^/]++)/([^/]++)/621461(*:40479)|d0f84/([^/]++)/([^/]++)/([^/]++)/6d0f84(*:40528)|60229/([^/]++)/([^/]++)/([^/]++)/660229(*:40577))|/c(?|f(?|6735/([^/]++)/([^/]++)/([^/]++)/cf6735(*:40635)|bce4/([^/]++)/([^/]++)/([^/]++)/cfbce4(*:40683))|3(?|99(?|86/([^/]++)/([^/]++)/([^/]++)/c39986(*:40739)|2e/([^/]++)/([^/]++)/([^/]++)/c3992e(*:40785))|61bc/([^/]++)/([^/]++)/([^/]++)/c361bc(*:40834)|2d9b/([^/]++)/([^/]++)/([^/]++)/c32d9b(*:40882))|75b6f/([^/]++)/([^/]++)/([^/]++)/c75b6f(*:40932)|c(?|b(?|1d4/([^/]++)/([^/]++)/([^/]++)/ccb1d4(*:40987)|098/([^/]++)/([^/]++)/([^/]++)/ccb098(*:41034))|c0aa/([^/]++)/([^/]++)/([^/]++)/ccc0aa(*:41083)|1aa4/([^/]++)/([^/]++)/([^/]++)/cc1aa4(*:41131))|b(?|cb58/([^/]++)/([^/]++)/([^/]++)/cbcb58(*:41184)|b6a3/([^/]++)/([^/]++)/([^/]++)/cbb6a3(*:41232))|9892a/([^/]++)/([^/]++)/([^/]++)/c9892a(*:41282)|6e19e/([^/]++)/([^/]++)/([^/]++)/c6e19e(*:41331)|dc0d6/([^/]++)/([^/]++)/([^/]++)/cdc0d6(*:41380)|5ab0b/([^/]++)/([^/]++)/([^/]++)/c5ab0b(*:41429)|a(?|9c26/([^/]++)/([^/]++)/([^/]++)/ca9c26(*:41481)|8155/([^/]++)/([^/]++)/([^/]++)/ca8155(*:41529)|7591/([^/]++)/([^/]++)/([^/]++)/ca7591(*:41577))|0(?|6d06/([^/]++)/([^/]++)/([^/]++)/c06d06(*:41630)|f168/([^/]++)/([^/]++)/([^/]++)/c0f168(*:41678))|8(?|ed21/([^/]++)/([^/]++)/([^/]++)/c8ed21(*:41731)|fbbc/([^/]++)/([^/]++)/([^/]++)/c8fbbc(*:41779)|c41c/([^/]++)/([^/]++)/([^/]++)/c8c41c(*:41827))|15da1/([^/]++)/([^/]++)/([^/]++)/c15da1(*:41877)|2(?|626d/([^/]++)/([^/]++)/([^/]++)/c2626d(*:41929)|aee8/([^/]++)/([^/]++)/([^/]++)/c2aee8(*:41977)|2abf/([^/]++)/([^/]++)/([^/]++)/c22abf(*:42025))|e78d1/([^/]++)/([^/]++)/([^/]++)/ce78d1(*:42075)|4(?|015b/([^/]++)/([^/]++)/([^/]++)/c4015b(*:42127)|b31c/([^/]++)/([^/]++)/([^/]++)/c4b31c(*:42175)))|/8(?|5(?|422a/([^/]++)/([^/]++)/([^/]++)/85422a(*:42234)|1ddf/([^/]++)/([^/]++)/([^/]++)/851ddf(*:42282)|fc37/([^/]++)/([^/]++)/([^/]++)/85fc37(*:42330))|1(?|4481/([^/]++)/([^/]++)/([^/]++)/814481(*:42383)|e74d/([^/]++)/([^/]++)/([^/]++)/81e74d(*:42431))|d(?|3(?|420/([^/]++)/([^/]++)/([^/]++)/8d3420(*:42487)|17b/([^/]++)/([^/]++)/([^/]++)/8d317b(*:42534))|f707/([^/]++)/([^/]++)/([^/]++)/8df707(*:42583)|6dc3/([^/]++)/([^/]++)/([^/]++)/8d6dc3(*:42631))|e(?|efcf/([^/]++)/([^/]++)/([^/]++)/8eefcf(*:42684)|bda5/([^/]++)/([^/]++)/([^/]++)/8ebda5(*:42732)|82ab/([^/]++)/([^/]++)/([^/]++)/8e82ab(*:42780))|b(?|16eb/([^/]++)/([^/]++)/([^/]++)/8b16eb(*:42833)|6dd7/([^/]++)/([^/]++)/([^/]++)/8b6dd7(*:42881)|5040/([^/]++)/([^/]++)/([^/]++)/8b5040(*:42929))|c(?|7bbb/([^/]++)/([^/]++)/([^/]++)/8c7bbb(*:42982)|6744/([^/]++)/([^/]++)/([^/]++)/8c6744(*:43030)|235f/([^/]++)/([^/]++)/([^/]++)/8c235f(*:43078))|8(?|4d24/([^/]++)/([^/]++)/([^/]++)/884d24(*:43131)|ae63/([^/]++)/([^/]++)/([^/]++)/88ae63(*:43179))|7(?|5715/([^/]++)/([^/]++)/([^/]++)/875715(*:43232)|2488/([^/]++)/([^/]++)/([^/]++)/872488(*:43280))|4(?|1172/([^/]++)/([^/]++)/([^/]++)/841172(*:43333)|6c26/([^/]++)/([^/]++)/([^/]++)/846c26(*:43381)|f7e6/([^/]++)/([^/]++)/([^/]++)/84f7e6(*:43429)|7cc5/([^/]++)/([^/]++)/([^/]++)/847cc5(*:43477))|f(?|ecb2/([^/]++)/([^/]++)/([^/]++)/8fecb2(*:43530)|7d80/([^/]++)/([^/]++)/([^/]++)/8f7d80(*:43578)|468c/([^/]++)/([^/]++)/([^/]++)/8f468c(*:43626))|a0e11/([^/]++)/([^/]++)/([^/]++)/8a0e11(*:43676)|2(?|f2b3/([^/]++)/([^/]++)/([^/]++)/82f2b3(*:43728)|489c/([^/]++)/([^/]++)/([^/]++)/82489c(*:43776))|6(?|b122/([^/]++)/([^/]++)/([^/]++)/86b122(*:43829)|0320/([^/]++)/([^/]++)/([^/]++)/860320(*:43877))|9(?|2c91/([^/]++)/([^/]++)/([^/]++)/892c91(*:43930)|fcd0/([^/]++)/([^/]++)/([^/]++)/89fcd0(*:43978))|065d0/([^/]++)/([^/]++)/([^/]++)/8065d0(*:44028))|/d(?|6(?|4a34/([^/]++)/([^/]++)/([^/]++)/d64a34(*:44086)|c651/([^/]++)/([^/]++)/([^/]++)/d6c651(*:44134))|f(?|877f/([^/]++)/([^/]++)/([^/]++)/df877f(*:44187)|263d/([^/]++)/([^/]++)/([^/]++)/df263d(*:44235)|7f28/([^/]++)/([^/]++)/([^/]++)/df7f28(*:44283)|6d23/([^/]++)/([^/]++)/([^/]++)/df6d23(*:44331))|b(?|85e2/([^/]++)/([^/]++)/([^/]++)/db85e2(*:44384)|e272/([^/]++)/([^/]++)/([^/]++)/dbe272(*:44432))|d(?|45(?|85/([^/]++)/([^/]++)/([^/]++)/dd4585(*:44488)|04/([^/]++)/([^/]++)/([^/]++)/dd4504(*:44534))|8eb9/([^/]++)/([^/]++)/([^/]++)/dd8eb9(*:44583))|a(?|ca41/([^/]++)/([^/]++)/([^/]++)/daca41(*:44636)|8ce5/([^/]++)/([^/]++)/([^/]++)/da8ce5(*:44684)|0d11/([^/]++)/([^/]++)/([^/]++)/da0d11(*:44732))|4(?|90d7/([^/]++)/([^/]++)/([^/]++)/d490d7(*:44785)|c2e4/([^/]++)/([^/]++)/([^/]++)/d4c2e4(*:44833))|8(?|6ea6/([^/]++)/([^/]++)/([^/]++)/d86ea6(*:44886)|40cc/([^/]++)/([^/]++)/([^/]++)/d840cc(*:44934))|c(?|82d6/([^/]++)/([^/]++)/([^/]++)/dc82d6(*:44987)|6a70/([^/]++)/([^/]++)/([^/]++)/dc6a70(*:45035)|5689/([^/]++)/([^/]++)/([^/]++)/dc5689(*:45083))|7(?|a728/([^/]++)/([^/]++)/([^/]++)/d7a728(*:45136)|0732/([^/]++)/([^/]++)/([^/]++)/d70732(*:45184)|9aac/([^/]++)/([^/]++)/([^/]++)/d79aac(*:45232))|14220/([^/]++)/([^/]++)/([^/]++)/d14220(*:45282)|5(?|cfea/([^/]++)/([^/]++)/([^/]++)/d5cfea(*:45334)|8072/([^/]++)/([^/]++)/([^/]++)/d58072(*:45382)|54f7/([^/]++)/([^/]++)/([^/]++)/d554f7(*:45430)|16b1/([^/]++)/([^/]++)/([^/]++)/d516b1(*:45478)|6b9f/([^/]++)/([^/]++)/([^/]++)/d56b9f(*:45526))|045c5/([^/]++)/([^/]++)/([^/]++)/d045c5(*:45576)|2(?|ed45/([^/]++)/([^/]++)/([^/]++)/d2ed45(*:45628)|40e3/([^/]++)/([^/]++)/([^/]++)/d240e3(*:45676))|93ed5/([^/]++)/([^/]++)/([^/]++)/d93ed5(*:45726))|/7(?|b(?|cdf7/([^/]++)/([^/]++)/([^/]++)/7bcdf7(*:45784)|13b2/([^/]++)/([^/]++)/([^/]++)/7b13b2(*:45832))|dcd34/([^/]++)/([^/]++)/([^/]++)/7dcd34(*:45882)|f(?|24d2/([^/]++)/([^/]++)/([^/]++)/7f24d2(*:45934)|5d04/([^/]++)/([^/]++)/([^/]++)/7f5d04(*:45982)|1171/([^/]++)/([^/]++)/([^/]++)/7f1171(*:46030)|a732/([^/]++)/([^/]++)/([^/]++)/7fa732(*:46078))|6(?|6ebc/([^/]++)/([^/]++)/([^/]++)/766ebc(*:46131)|34ea/([^/]++)/([^/]++)/([^/]++)/7634ea(*:46179))|750ca/([^/]++)/([^/]++)/([^/]++)/7750ca(*:46229)|1(?|a(?|3cb/([^/]++)/([^/]++)/([^/]++)/71a3cb(*:46284)|d16/([^/]++)/([^/]++)/([^/]++)/71ad16(*:46331))|43d7/([^/]++)/([^/]++)/([^/]++)/7143d7(*:46380))|88d98/([^/]++)/([^/]++)/([^/]++)/788d98(*:46430)|2(?|da7f/([^/]++)/([^/]++)/([^/]++)/72da7f(*:46482)|50eb/([^/]++)/([^/]++)/([^/]++)/7250eb(*:46530))|c(?|590f/([^/]++)/([^/]++)/([^/]++)/7c590f(*:46583)|e328/([^/]++)/([^/]++)/([^/]++)/7ce328(*:46631))|a5392/([^/]++)/([^/]++)/([^/]++)/7a5392(*:46681)|95c7a/([^/]++)/([^/]++)/([^/]++)/795c7a(*:46730)|504ad/([^/]++)/([^/]++)/([^/]++)/7504ad(*:46779)|04afe/([^/]++)/([^/]++)/([^/]++)/704afe(*:46828)|4bba2/([^/]++)/([^/]++)/([^/]++)/74bba2(*:46877))|/9(?|b(?|72e3/([^/]++)/([^/]++)/([^/]++)/9b72e3(*:46935)|698e/([^/]++)/([^/]++)/([^/]++)/9b698e(*:46983))|7e852/([^/]++)/([^/]++)/([^/]++)/97e852(*:47033)|4c7bb/([^/]++)/([^/]++)/([^/]++)/94c7bb(*:47082)|9(?|c5e0/([^/]++)/([^/]++)/([^/]++)/99c5e0(*:47134)|6a7f/([^/]++)/([^/]++)/([^/]++)/996a7f(*:47182)|bcfc/([^/]++)/([^/]++)/([^/]++)/99bcfc(*:47230)|0827/([^/]++)/([^/]++)/([^/]++)/990827(*:47278))|a(?|d6aa/([^/]++)/([^/]++)/([^/]++)/9ad6aa(*:47331)|b0d8/([^/]++)/([^/]++)/([^/]++)/9ab0d8(*:47379))|c(?|f81d/([^/]++)/([^/]++)/([^/]++)/9cf81d(*:47432)|c138/([^/]++)/([^/]++)/([^/]++)/9cc138(*:47480)|82c7/([^/]++)/([^/]++)/([^/]++)/9c82c7(*:47528)|0180/([^/]++)/([^/]++)/([^/]++)/9c0180(*:47576))|f(?|396f/([^/]++)/([^/]++)/([^/]++)/9f396f(*:47629)|e859/([^/]++)/([^/]++)/([^/]++)/9fe859(*:47677)|53d8/([^/]++)/([^/]++)/([^/]++)/9f53d8(*:47725))|12d2b/([^/]++)/([^/]++)/([^/]++)/912d2b(*:47775)|59a55/([^/]++)/([^/]++)/([^/]++)/959a55(*:47824)|6(?|ea64/([^/]++)/([^/]++)/([^/]++)/96ea64(*:47876)|b9bf/([^/]++)/([^/]++)/([^/]++)/96b9bf(*:47924))|e3cfc/([^/]++)/([^/]++)/([^/]++)/9e3cfc(*:47974)|2(?|fb0c/([^/]++)/([^/]++)/([^/]++)/92fb0c(*:48026)|262b/([^/]++)/([^/]++)/([^/]++)/92262b(*:48074)|32fe/([^/]++)/([^/]++)/([^/]++)/9232fe(*:48122)|977a/([^/]++)/([^/]++)/([^/]++)/92977a(*:48170))|8d6f5/([^/]++)/([^/]++)/([^/]++)/98d6f5(*:48220)|0794e/([^/]++)/([^/]++)/([^/]++)/90794e(*:48269)|34815/([^/]++)/([^/]++)/([^/]++)/934815(*:48318))|/4(?|e(?|4b5f/([^/]++)/([^/]++)/([^/]++)/4e4b5f(*:48376)|a06f/([^/]++)/([^/]++)/([^/]++)/4ea06f(*:48424)|0(?|928/([^/]++)/([^/]++)/([^/]++)/4e0928(*:48475)|cb6/([^/]++)/([^/]++)/([^/]++)/4e0cb6(*:48522)))|6922a/([^/]++)/([^/]++)/([^/]++)/46922a(*:48573)|4(?|c4c1/([^/]++)/([^/]++)/([^/]++)/44c4c1(*:48625)|3cb0/([^/]++)/([^/]++)/([^/]++)/443cb0(*:48673))|8ab2f/([^/]++)/([^/]++)/([^/]++)/48ab2f(*:48723)|5(?|645a/([^/]++)/([^/]++)/([^/]++)/45645a(*:48775)|58db/([^/]++)/([^/]++)/([^/]++)/4558db(*:48823))|2e77b/([^/]++)/([^/]++)/([^/]++)/42e77b(*:48873)|c27ce/([^/]++)/([^/]++)/([^/]++)/4c27ce(*:48922)|f(?|fce0/([^/]++)/([^/]++)/([^/]++)/4ffce0(*:48974)|ac9b/([^/]++)/([^/]++)/([^/]++)/4fac9b(*:49022))|a47d2/([^/]++)/([^/]++)/([^/]++)/4a47d2(*:49072)|70e7a/([^/]++)/([^/]++)/([^/]++)/470e7a(*:49121)|b(?|0(?|4a6/([^/]++)/([^/]++)/([^/]++)/4b04a6(*:49176)|a59/([^/]++)/([^/]++)/([^/]++)/4b0a59(*:49223)|250/([^/]++)/([^/]++)/([^/]++)/4b0250(*:49270))|6538/([^/]++)/([^/]++)/([^/]++)/4b6538(*:49319))|3(?|f(?|a7f/([^/]++)/([^/]++)/([^/]++)/43fa7f(*:49375)|eae/([^/]++)/([^/]++)/([^/]++)/43feae(*:49422))|0c36/([^/]++)/([^/]++)/([^/]++)/430c36(*:49471)|7d7d/([^/]++)/([^/]++)/([^/]++)/437d7d(*:49519)|1135/([^/]++)/([^/]++)/([^/]++)/431135(*:49567))|d(?|5b99/([^/]++)/([^/]++)/([^/]++)/4d5b99(*:49620)|aa3d/([^/]++)/([^/]++)/([^/]++)/4daa3d(*:49668))|9c9ad/([^/]++)/([^/]++)/([^/]++)/49c9ad(*:49718)))/?$\"),\n}\n####\n{\n  \"54\"    => [{ART::Parameters.new({\"_route\" => \"_0\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"102\"   => [{ART::Parameters.new({\"_route\" => \"_190\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"147\"   => [{ART::Parameters.new({\"_route\" => \"_478\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"194\"   => [{ART::Parameters.new({\"_route\" => \"_259\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"240\"   => [{ART::Parameters.new({\"_route\" => \"_368\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"291\"   => [{ART::Parameters.new({\"_route\" => \"_1\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"337\"   => [{ART::Parameters.new({\"_route\" => \"_116\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"383\"   => [{ART::Parameters.new({\"_route\" => \"_490\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"434\"   => [{ART::Parameters.new({\"_route\" => \"_2\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"480\"   => [{ART::Parameters.new({\"_route\" => \"_124\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"526\"   => [{ART::Parameters.new({\"_route\" => \"_389\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"577\"   => [{ART::Parameters.new({\"_route\" => \"_8\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"623\"   => [{ART::Parameters.new({\"_route\" => \"_104\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"677\"   => [{ART::Parameters.new({\"_route\" => \"_12\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"722\"   => [{ART::Parameters.new({\"_route\" => \"_442\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"769\"   => [{ART::Parameters.new({\"_route\" => \"_253\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"820\"   => [{ART::Parameters.new({\"_route\" => \"_13\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"866\"   => [{ART::Parameters.new({\"_route\" => \"_254\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"912\"   => [{ART::Parameters.new({\"_route\" => \"_347\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"963\"   => [{ART::Parameters.new({\"_route\" => \"_16\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1009\"  => [{ART::Parameters.new({\"_route\" => \"_87\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1058\"  => [{ART::Parameters.new({\"_route\" => \"_31\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1109\"  => [{ART::Parameters.new({\"_route\" => \"_50\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1156\"  => [{ART::Parameters.new({\"_route\" => \"_219\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1203\"  => [{ART::Parameters.new({\"_route\" => \"_332\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1250\"  => [{ART::Parameters.new({\"_route\" => \"_359\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1302\"  => [{ART::Parameters.new({\"_route\" => \"_183\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1349\"  => [{ART::Parameters.new({\"_route\" => \"_500\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1401\"  => [{ART::Parameters.new({\"_route\" => \"_214\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1448\"  => [{ART::Parameters.new({\"_route\" => \"_321\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1497\"  => [{ART::Parameters.new({\"_route\" => \"_243\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1545\"  => [{ART::Parameters.new({\"_route\" => \"_328\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1596\"  => [{ART::Parameters.new({\"_route\" => \"_362\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1643\"  => [{ART::Parameters.new({\"_route\" => \"_488\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1701\"  => [{ART::Parameters.new({\"_route\" => \"_3\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1751\"  => [{ART::Parameters.new({\"_route\" => \"_102\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1797\"  => [{ART::Parameters.new({\"_route\" => \"_220\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1845\"  => [{ART::Parameters.new({\"_route\" => \"_127\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1897\"  => [{ART::Parameters.new({\"_route\" => \"_5\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1944\"  => [{ART::Parameters.new({\"_route\" => \"_242\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"1991\"  => [{ART::Parameters.new({\"_route\" => \"_397\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2038\"  => [{ART::Parameters.new({\"_route\" => \"_454\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2090\"  => [{ART::Parameters.new({\"_route\" => \"_34\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2137\"  => [{ART::Parameters.new({\"_route\" => \"_281\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2189\"  => [{ART::Parameters.new({\"_route\" => \"_64\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2236\"  => [{ART::Parameters.new({\"_route\" => \"_205\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2291\"  => [{ART::Parameters.new({\"_route\" => \"_71\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2337\"  => [{ART::Parameters.new({\"_route\" => \"_203\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2385\"  => [{ART::Parameters.new({\"_route\" => \"_97\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2437\"  => [{ART::Parameters.new({\"_route\" => \"_98\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2484\"  => [{ART::Parameters.new({\"_route\" => \"_267\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2531\"  => [{ART::Parameters.new({\"_route\" => \"_309\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2586\"  => [{ART::Parameters.new({\"_route\" => \"_117\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2631\"  => [{ART::Parameters.new({\"_route\" => \"_211\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2679\"  => [{ART::Parameters.new({\"_route\" => \"_484\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2731\"  => [{ART::Parameters.new({\"_route\" => \"_139\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2778\"  => [{ART::Parameters.new({\"_route\" => \"_421\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2830\"  => [{ART::Parameters.new({\"_route\" => \"_185\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2877\"  => [{ART::Parameters.new({\"_route\" => \"_439\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2926\"  => [{ART::Parameters.new({\"_route\" => \"_218\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"2977\"  => [{ART::Parameters.new({\"_route\" => \"_233\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3024\"  => [{ART::Parameters.new({\"_route\" => \"_483\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3073\"  => [{ART::Parameters.new({\"_route\" => \"_265\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3124\"  => [{ART::Parameters.new({\"_route\" => \"_299\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3171\"  => [{ART::Parameters.new({\"_route\" => \"_351\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3218\"  => [{ART::Parameters.new({\"_route\" => \"_472\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3267\"  => [{ART::Parameters.new({\"_route\" => \"_360\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3315\"  => [{ART::Parameters.new({\"_route\" => \"_466\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3372\"  => [{ART::Parameters.new({\"_route\" => \"_4\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3419\"  => [{ART::Parameters.new({\"_route\" => \"_142\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3466\"  => [{ART::Parameters.new({\"_route\" => \"_151\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3513\"  => [{ART::Parameters.new({\"_route\" => \"_308\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3560\"  => [{ART::Parameters.new({\"_route\" => \"_440\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3612\"  => [{ART::Parameters.new({\"_route\" => \"_14\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3659\"  => [{ART::Parameters.new({\"_route\" => \"_358\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3711\"  => [{ART::Parameters.new({\"_route\" => \"_37\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3758\"  => [{ART::Parameters.new({\"_route\" => \"_38\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3805\"  => [{ART::Parameters.new({\"_route\" => \"_146\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3852\"  => [{ART::Parameters.new({\"_route\" => \"_194\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3899\"  => [{ART::Parameters.new({\"_route\" => \"_487\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3948\"  => [{ART::Parameters.new({\"_route\" => \"_42\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"3999\"  => [{ART::Parameters.new({\"_route\" => \"_54\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4046\"  => [{ART::Parameters.new({\"_route\" => \"_326\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4098\"  => [{ART::Parameters.new({\"_route\" => \"_68\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4145\"  => [{ART::Parameters.new({\"_route\" => \"_108\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4197\"  => [{ART::Parameters.new({\"_route\" => \"_74\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4244\"  => [{ART::Parameters.new({\"_route\" => \"_315\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4291\"  => [{ART::Parameters.new({\"_route\" => \"_374\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4343\"  => [{ART::Parameters.new({\"_route\" => \"_99\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4390\"  => [{ART::Parameters.new({\"_route\" => \"_238\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4442\"  => [{ART::Parameters.new({\"_route\" => \"_107\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4489\"  => [{ART::Parameters.new({\"_route\" => \"_409\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4541\"  => [{ART::Parameters.new({\"_route\" => \"_122\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4588\"  => [{ART::Parameters.new({\"_route\" => \"_379\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4635\"  => [{ART::Parameters.new({\"_route\" => \"_390\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4687\"  => [{ART::Parameters.new({\"_route\" => \"_171\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4734\"  => [{ART::Parameters.new({\"_route\" => \"_260\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4781\"  => [{ART::Parameters.new({\"_route\" => \"_434\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4830\"  => [{ART::Parameters.new({\"_route\" => \"_189\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4878\"  => [{ART::Parameters.new({\"_route\" => \"_467\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4935\"  => [{ART::Parameters.new({\"_route\" => \"_6\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"4982\"  => [{ART::Parameters.new({\"_route\" => \"_286\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5029\"  => [{ART::Parameters.new({\"_route\" => \"_438\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5081\"  => [{ART::Parameters.new({\"_route\" => \"_19\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5131\"  => [{ART::Parameters.new({\"_route\" => \"_24\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5177\"  => [{ART::Parameters.new({\"_route\" => \"_172\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5230\"  => [{ART::Parameters.new({\"_route\" => \"_33\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5277\"  => [{ART::Parameters.new({\"_route\" => \"_400\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5324\"  => [{ART::Parameters.new({\"_route\" => \"_427\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5376\"  => [{ART::Parameters.new({\"_route\" => \"_35\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5423\"  => [{ART::Parameters.new({\"_route\" => \"_156\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5475\"  => [{ART::Parameters.new({\"_route\" => \"_36\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5522\"  => [{ART::Parameters.new({\"_route\" => \"_251\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5574\"  => [{ART::Parameters.new({\"_route\" => \"_43\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5621\"  => [{ART::Parameters.new({\"_route\" => \"_292\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5668\"  => [{ART::Parameters.new({\"_route\" => \"_411\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5720\"  => [{ART::Parameters.new({\"_route\" => \"_69\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5767\"  => [{ART::Parameters.new({\"_route\" => \"_159\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5814\"  => [{ART::Parameters.new({\"_route\" => \"_170\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5861\"  => [{ART::Parameters.new({\"_route\" => \"_376\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5913\"  => [{ART::Parameters.new({\"_route\" => \"_131\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"5960\"  => [{ART::Parameters.new({\"_route\" => \"_446\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6015\"  => [{ART::Parameters.new({\"_route\" => \"_140\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6061\"  => [{ART::Parameters.new({\"_route\" => \"_353\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6112\"  => [{ART::Parameters.new({\"_route\" => \"_224\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6158\"  => [{ART::Parameters.new({\"_route\" => \"_346\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6204\"  => [{ART::Parameters.new({\"_route\" => \"_443\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6254\"  => [{ART::Parameters.new({\"_route\" => \"_154\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6305\"  => [{ART::Parameters.new({\"_route\" => \"_212\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6352\"  => [{ART::Parameters.new({\"_route\" => \"_313\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6399\"  => [{ART::Parameters.new({\"_route\" => \"_395\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6446\"  => [{ART::Parameters.new({\"_route\" => \"_441\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6498\"  => [{ART::Parameters.new({\"_route\" => \"_223\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6545\"  => [{ART::Parameters.new({\"_route\" => \"_303\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6594\"  => [{ART::Parameters.new({\"_route\" => \"_410\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6642\"  => [{ART::Parameters.new({\"_route\" => \"_494\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6702\"  => [{ART::Parameters.new({\"_route\" => \"_7\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6748\"  => [{ART::Parameters.new({\"_route\" => \"_268\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6796\"  => [{ART::Parameters.new({\"_route\" => \"_178\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6843\"  => [{ART::Parameters.new({\"_route\" => \"_179\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6890\"  => [{ART::Parameters.new({\"_route\" => \"_416\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6942\"  => [{ART::Parameters.new({\"_route\" => \"_25\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"6989\"  => [{ART::Parameters.new({\"_route\" => \"_307\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7036\"  => [{ART::Parameters.new({\"_route\" => \"_387\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7083\"  => [{ART::Parameters.new({\"_route\" => \"_471\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7132\"  => [{ART::Parameters.new({\"_route\" => \"_90\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7183\"  => [{ART::Parameters.new({\"_route\" => \"_95\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7230\"  => [{ART::Parameters.new({\"_route\" => \"_338\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7277\"  => [{ART::Parameters.new({\"_route\" => \"_401\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7329\"  => [{ART::Parameters.new({\"_route\" => \"_147\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7376\"  => [{ART::Parameters.new({\"_route\" => \"_319\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7423\"  => [{ART::Parameters.new({\"_route\" => \"_354\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7470\"  => [{ART::Parameters.new({\"_route\" => \"_428\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7522\"  => [{ART::Parameters.new({\"_route\" => \"_162\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7572\"  => [{ART::Parameters.new({\"_route\" => \"_175\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7618\"  => [{ART::Parameters.new({\"_route\" => \"_455\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7666\"  => [{ART::Parameters.new({\"_route\" => \"_355\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7718\"  => [{ART::Parameters.new({\"_route\" => \"_197\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7768\"  => [{ART::Parameters.new({\"_route\" => \"_202\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7813\"  => [{ART::Parameters.new({\"_route\" => \"_489\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7863\"  => [{ART::Parameters.new({\"_route\" => \"_199\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7914\"  => [{ART::Parameters.new({\"_route\" => \"_263\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"7961\"  => [{ART::Parameters.new({\"_route\" => \"_406\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8010\"  => [{ART::Parameters.new({\"_route\" => \"_289\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8058\"  => [{ART::Parameters.new({\"_route\" => \"_325\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8106\"  => [{ART::Parameters.new({\"_route\" => \"_378\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8154\"  => [{ART::Parameters.new({\"_route\" => \"_468\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8211\"  => [{ART::Parameters.new({\"_route\" => \"_9\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8258\"  => [{ART::Parameters.new({\"_route\" => \"_216\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8307\"  => [{ART::Parameters.new({\"_route\" => \"_26\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8355\"  => [{ART::Parameters.new({\"_route\" => \"_62\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8406\"  => [{ART::Parameters.new({\"_route\" => \"_81\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8453\"  => [{ART::Parameters.new({\"_route\" => \"_318\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8505\"  => [{ART::Parameters.new({\"_route\" => \"_121\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8551\"  => [{ART::Parameters.new({\"_route\" => \"_182\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8603\"  => [{ART::Parameters.new({\"_route\" => \"_136\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8650\"  => [{ART::Parameters.new({\"_route\" => \"_415\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8697\"  => [{ART::Parameters.new({\"_route\" => \"_457\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8744\"  => [{ART::Parameters.new({\"_route\" => \"_463\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8796\"  => [{ART::Parameters.new({\"_route\" => \"_148\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8843\"  => [{ART::Parameters.new({\"_route\" => \"_273\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8892\"  => [{ART::Parameters.new({\"_route\" => \"_284\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8940\"  => [{ART::Parameters.new({\"_route\" => \"_288\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"8991\"  => [{ART::Parameters.new({\"_route\" => \"_295\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9038\"  => [{ART::Parameters.new({\"_route\" => \"_305\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9085\"  => [{ART::Parameters.new({\"_route\" => \"_453\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9134\"  => [{ART::Parameters.new({\"_route\" => \"_340\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9185\"  => [{ART::Parameters.new({\"_route\" => \"_371\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9232\"  => [{ART::Parameters.new({\"_route\" => \"_417\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9284\"  => [{ART::Parameters.new({\"_route\" => \"_382\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9331\"  => [{ART::Parameters.new({\"_route\" => \"_404\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9389\"  => [{ART::Parameters.new({\"_route\" => \"_10\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9436\"  => [{ART::Parameters.new({\"_route\" => \"_279\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9483\"  => [{ART::Parameters.new({\"_route\" => \"_377\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9535\"  => [{ART::Parameters.new({\"_route\" => \"_39\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9582\"  => [{ART::Parameters.new({\"_route\" => \"_40\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9629\"  => [{ART::Parameters.new({\"_route\" => \"_264\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9676\"  => [{ART::Parameters.new({\"_route\" => \"_449\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9728\"  => [{ART::Parameters.new({\"_route\" => \"_46\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9775\"  => [{ART::Parameters.new({\"_route\" => \"_257\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9822\"  => [{ART::Parameters.new({\"_route\" => \"_274\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9869\"  => [{ART::Parameters.new({\"_route\" => \"_388\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9921\"  => [{ART::Parameters.new({\"_route\" => \"_53\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"9968\"  => [{ART::Parameters.new({\"_route\" => \"_345\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10020\" => [{ART::Parameters.new({\"_route\" => \"_73\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10068\" => [{ART::Parameters.new({\"_route\" => \"_296\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10121\" => [{ART::Parameters.new({\"_route\" => \"_75\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10169\" => [{ART::Parameters.new({\"_route\" => \"_458\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10225\" => [{ART::Parameters.new({\"_route\" => \"_79\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10272\" => [{ART::Parameters.new({\"_route\" => \"_129\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10319\" => [{ART::Parameters.new({\"_route\" => \"_418\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10368\" => [{ART::Parameters.new({\"_route\" => \"_225\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10416\" => [{ART::Parameters.new({\"_route\" => \"_479\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10466\" => [{ART::Parameters.new({\"_route\" => \"_120\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10515\" => [{ART::Parameters.new({\"_route\" => \"_276\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10564\" => [{ART::Parameters.new({\"_route\" => \"_370\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10616\" => [{ART::Parameters.new({\"_route\" => \"_385\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10664\" => [{ART::Parameters.new({\"_route\" => \"_469\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10714\" => [{ART::Parameters.new({\"_route\" => \"_435\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10772\" => [{ART::Parameters.new({\"_route\" => \"_11\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10820\" => [{ART::Parameters.new({\"_route\" => \"_105\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10868\" => [{ART::Parameters.new({\"_route\" => \"_132\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10921\" => [{ART::Parameters.new({\"_route\" => \"_18\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"10969\" => [{ART::Parameters.new({\"_route\" => \"_210\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11017\" => [{ART::Parameters.new({\"_route\" => \"_329\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11073\" => [{ART::Parameters.new({\"_route\" => \"_29\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11120\" => [{ART::Parameters.new({\"_route\" => \"_480\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11169\" => [{ART::Parameters.new({\"_route\" => \"_426\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11222\" => [{ART::Parameters.new({\"_route\" => \"_32\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11270\" => [{ART::Parameters.new({\"_route\" => \"_217\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11318\" => [{ART::Parameters.new({\"_route\" => \"_275\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11371\" => [{ART::Parameters.new({\"_route\" => \"_45\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11419\" => [{ART::Parameters.new({\"_route\" => \"_157\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11467\" => [{ART::Parameters.new({\"_route\" => \"_184\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11515\" => [{ART::Parameters.new({\"_route\" => \"_250\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11563\" => [{ART::Parameters.new({\"_route\" => \"_356\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11616\" => [{ART::Parameters.new({\"_route\" => \"_47\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11664\" => [{ART::Parameters.new({\"_route\" => \"_445\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11714\" => [{ART::Parameters.new({\"_route\" => \"_48\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11766\" => [{ART::Parameters.new({\"_route\" => \"_58\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11814\" => [{ART::Parameters.new({\"_route\" => \"_414\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11862\" => [{ART::Parameters.new({\"_route\" => \"_431\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11915\" => [{ART::Parameters.new({\"_route\" => \"_84\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"11963\" => [{ART::Parameters.new({\"_route\" => \"_294\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12011\" => [{ART::Parameters.new({\"_route\" => \"_336\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12059\" => [{ART::Parameters.new({\"_route\" => \"_465\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12112\" => [{ART::Parameters.new({\"_route\" => \"_103\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12160\" => [{ART::Parameters.new({\"_route\" => \"_111\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12208\" => [{ART::Parameters.new({\"_route\" => \"_207\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12256\" => [{ART::Parameters.new({\"_route\" => \"_402\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12309\" => [{ART::Parameters.new({\"_route\" => \"_230\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12356\" => [{ART::Parameters.new({\"_route\" => \"_331\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12406\" => [{ART::Parameters.new({\"_route\" => \"_248\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12455\" => [{ART::Parameters.new({\"_route\" => \"_282\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12513\" => [{ART::Parameters.new({\"_route\" => \"_15\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12561\" => [{ART::Parameters.new({\"_route\" => \"_130\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12609\" => [{ART::Parameters.new({\"_route\" => \"_231\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12657\" => [{ART::Parameters.new({\"_route\" => \"_365\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12705\" => [{ART::Parameters.new({\"_route\" => \"_448\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12758\" => [{ART::Parameters.new({\"_route\" => \"_20\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12806\" => [{ART::Parameters.new({\"_route\" => \"_93\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12854\" => [{ART::Parameters.new({\"_route\" => \"_186\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12902\" => [{ART::Parameters.new({\"_route\" => \"_460\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"12955\" => [{ART::Parameters.new({\"_route\" => \"_52\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13003\" => [{ART::Parameters.new({\"_route\" => \"_447\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13056\" => [{ART::Parameters.new({\"_route\" => \"_56\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13104\" => [{ART::Parameters.new({\"_route\" => \"_133\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13152\" => [{ART::Parameters.new({\"_route\" => \"_297\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13205\" => [{ART::Parameters.new({\"_route\" => \"_82\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13253\" => [{ART::Parameters.new({\"_route\" => \"_165\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13301\" => [{ART::Parameters.new({\"_route\" => \"_213\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13351\" => [{ART::Parameters.new({\"_route\" => \"_86\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13403\" => [{ART::Parameters.new({\"_route\" => \"_92\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13450\" => [{ART::Parameters.new({\"_route\" => \"_280\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13500\" => [{ART::Parameters.new({\"_route\" => \"_143\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13549\" => [{ART::Parameters.new({\"_route\" => \"_177\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13601\" => [{ART::Parameters.new({\"_route\" => \"_188\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13649\" => [{ART::Parameters.new({\"_route\" => \"_311\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13697\" => [{ART::Parameters.new({\"_route\" => \"_350\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13750\" => [{ART::Parameters.new({\"_route\" => \"_226\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13798\" => [{ART::Parameters.new({\"_route\" => \"_291\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13851\" => [{ART::Parameters.new({\"_route\" => \"_244\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13898\" => [{ART::Parameters.new({\"_route\" => \"_287\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13951\" => [{ART::Parameters.new({\"_route\" => \"_300\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"13999\" => [{ART::Parameters.new({\"_route\" => \"_451\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14047\" => [{ART::Parameters.new({\"_route\" => \"_452\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14095\" => [{ART::Parameters.new({\"_route\" => \"_481\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14145\" => [{ART::Parameters.new({\"_route\" => \"_312\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14203\" => [{ART::Parameters.new({\"_route\" => \"_17\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14251\" => [{ART::Parameters.new({\"_route\" => \"_227\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14299\" => [{ART::Parameters.new({\"_route\" => \"_393\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14349\" => [{ART::Parameters.new({\"_route\" => \"_57\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14401\" => [{ART::Parameters.new({\"_route\" => \"_61\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14449\" => [{ART::Parameters.new({\"_route\" => \"_112\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14500\" => [{ART::Parameters.new({\"_route\" => \"_135\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14547\" => [{ART::Parameters.new({\"_route\" => \"_271\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14596\" => [{ART::Parameters.new({\"_route\" => \"_459\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14649\" => [{ART::Parameters.new({\"_route\" => \"_67\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14697\" => [{ART::Parameters.new({\"_route\" => \"_113\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14745\" => [{ART::Parameters.new({\"_route\" => \"_497\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14795\" => [{ART::Parameters.new({\"_route\" => \"_70\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14847\" => [{ART::Parameters.new({\"_route\" => \"_89\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14895\" => [{ART::Parameters.new({\"_route\" => \"_128\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14948\" => [{ART::Parameters.new({\"_route\" => \"_150\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"14996\" => [{ART::Parameters.new({\"_route\" => \"_166\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15047\" => [{ART::Parameters.new({\"_route\" => \"_206\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15094\" => [{ART::Parameters.new({\"_route\" => \"_419\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15148\" => [{ART::Parameters.new({\"_route\" => \"_201\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15196\" => [{ART::Parameters.new({\"_route\" => \"_314\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15244\" => [{ART::Parameters.new({\"_route\" => \"_429\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15297\" => [{ART::Parameters.new({\"_route\" => \"_228\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15345\" => [{ART::Parameters.new({\"_route\" => \"_477\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15395\" => [{ART::Parameters.new({\"_route\" => \"_272\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15444\" => [{ART::Parameters.new({\"_route\" => \"_486\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15502\" => [{ART::Parameters.new({\"_route\" => \"_21\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15550\" => [{ART::Parameters.new({\"_route\" => \"_247\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15598\" => [{ART::Parameters.new({\"_route\" => \"_424\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15646\" => [{ART::Parameters.new({\"_route\" => \"_499\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15699\" => [{ART::Parameters.new({\"_route\" => \"_23\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15747\" => [{ART::Parameters.new({\"_route\" => \"_152\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15795\" => [{ART::Parameters.new({\"_route\" => \"_304\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15843\" => [{ART::Parameters.new({\"_route\" => \"_352\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15896\" => [{ART::Parameters.new({\"_route\" => \"_28\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"15944\" => [{ART::Parameters.new({\"_route\" => \"_240\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16000\" => [{ART::Parameters.new({\"_route\" => \"_30\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16047\" => [{ART::Parameters.new({\"_route\" => \"_41\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16096\" => [{ART::Parameters.new({\"_route\" => \"_301\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16149\" => [{ART::Parameters.new({\"_route\" => \"_66\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16197\" => [{ART::Parameters.new({\"_route\" => \"_72\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16245\" => [{ART::Parameters.new({\"_route\" => \"_320\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16298\" => [{ART::Parameters.new({\"_route\" => \"_78\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16346\" => [{ART::Parameters.new({\"_route\" => \"_337\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16394\" => [{ART::Parameters.new({\"_route\" => \"_399\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16442\" => [{ART::Parameters.new({\"_route\" => \"_495\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16492\" => [{ART::Parameters.new({\"_route\" => \"_85\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16544\" => [{ART::Parameters.new({\"_route\" => \"_101\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16592\" => [{ART::Parameters.new({\"_route\" => \"_176\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16640\" => [{ART::Parameters.new({\"_route\" => \"_246\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16693\" => [{ART::Parameters.new({\"_route\" => \"_125\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16741\" => [{ART::Parameters.new({\"_route\" => \"_341\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16794\" => [{ART::Parameters.new({\"_route\" => \"_137\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16842\" => [{ART::Parameters.new({\"_route\" => \"_270\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16890\" => [{ART::Parameters.new({\"_route\" => \"_386\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16943\" => [{ART::Parameters.new({\"_route\" => \"_169\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"16991\" => [{ART::Parameters.new({\"_route\" => \"_200\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17039\" => [{ART::Parameters.new({\"_route\" => \"_262\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17092\" => [{ART::Parameters.new({\"_route\" => \"_187\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17140\" => [{ART::Parameters.new({\"_route\" => \"_333\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17190\" => [{ART::Parameters.new({\"_route\" => \"_215\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17239\" => [{ART::Parameters.new({\"_route\" => \"_316\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17288\" => [{ART::Parameters.new({\"_route\" => \"_343\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17346\" => [{ART::Parameters.new({\"_route\" => \"_22\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17394\" => [{ART::Parameters.new({\"_route\" => \"_420\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17447\" => [{ART::Parameters.new({\"_route\" => \"_55\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17494\" => [{ART::Parameters.new({\"_route\" => \"_496\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17547\" => [{ART::Parameters.new({\"_route\" => \"_153\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17595\" => [{ART::Parameters.new({\"_route\" => \"_344\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17648\" => [{ART::Parameters.new({\"_route\" => \"_160\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17696\" => [{ART::Parameters.new({\"_route\" => \"_398\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17749\" => [{ART::Parameters.new({\"_route\" => \"_161\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17797\" => [{ART::Parameters.new({\"_route\" => \"_193\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17847\" => [{ART::Parameters.new({\"_route\" => \"_174\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17899\" => [{ART::Parameters.new({\"_route\" => \"_209\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"17947\" => [{ART::Parameters.new({\"_route\" => \"_261\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18000\" => [{ART::Parameters.new({\"_route\" => \"_222\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18048\" => [{ART::Parameters.new({\"_route\" => \"_323\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18096\" => [{ART::Parameters.new({\"_route\" => \"_380\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18149\" => [{ART::Parameters.new({\"_route\" => \"_232\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18197\" => [{ART::Parameters.new({\"_route\" => \"_383\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18247\" => [{ART::Parameters.new({\"_route\" => \"_306\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18296\" => [{ART::Parameters.new({\"_route\" => \"_327\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18345\" => [{ART::Parameters.new({\"_route\" => \"_364\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18397\" => [{ART::Parameters.new({\"_route\" => \"_403\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18445\" => [{ART::Parameters.new({\"_route\" => \"_405\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18495\" => [{ART::Parameters.new({\"_route\" => \"_412\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18553\" => [{ART::Parameters.new({\"_route\" => \"_27\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18601\" => [{ART::Parameters.new({\"_route\" => \"_134\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18649\" => [{ART::Parameters.new({\"_route\" => \"_245\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18702\" => [{ART::Parameters.new({\"_route\" => \"_59\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18750\" => [{ART::Parameters.new({\"_route\" => \"_208\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18803\" => [{ART::Parameters.new({\"_route\" => \"_60\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18851\" => [{ART::Parameters.new({\"_route\" => \"_119\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18902\" => [{ART::Parameters.new({\"_route\" => \"_163\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18949\" => [{ART::Parameters.new({\"_route\" => \"_249\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"18998\" => [{ART::Parameters.new({\"_route\" => \"_278\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19051\" => [{ART::Parameters.new({\"_route\" => \"_63\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19099\" => [{ART::Parameters.new({\"_route\" => \"_195\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19147\" => [{ART::Parameters.new({\"_route\" => \"_252\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19195\" => [{ART::Parameters.new({\"_route\" => \"_461\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19248\" => [{ART::Parameters.new({\"_route\" => \"_126\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19296\" => [{ART::Parameters.new({\"_route\" => \"_158\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19344\" => [{ART::Parameters.new({\"_route\" => \"_221\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19392\" => [{ART::Parameters.new({\"_route\" => \"_269\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19440\" => [{ART::Parameters.new({\"_route\" => \"_310\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19496\" => [{ART::Parameters.new({\"_route\" => \"_138\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19543\" => [{ART::Parameters.new({\"_route\" => \"_348\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19592\" => [{ART::Parameters.new({\"_route\" => \"_236\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19640\" => [{ART::Parameters.new({\"_route\" => \"_433\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19693\" => [{ART::Parameters.new({\"_route\" => \"_141\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19741\" => [{ART::Parameters.new({\"_route\" => \"_283\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19794\" => [{ART::Parameters.new({\"_route\" => \"_144\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19842\" => [{ART::Parameters.new({\"_route\" => \"_191\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19895\" => [{ART::Parameters.new({\"_route\" => \"_168\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19943\" => [{ART::Parameters.new({\"_route\" => \"_363\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"19991\" => [{ART::Parameters.new({\"_route\" => \"_381\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20044\" => [{ART::Parameters.new({\"_route\" => \"_180\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20092\" => [{ART::Parameters.new({\"_route\" => \"_339\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20142\" => [{ART::Parameters.new({\"_route\" => \"_196\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20194\" => [{ART::Parameters.new({\"_route\" => \"_198\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20242\" => [{ART::Parameters.new({\"_route\" => \"_285\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20292\" => [{ART::Parameters.new({\"_route\" => \"_349\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20344\" => [{ART::Parameters.new({\"_route\" => \"_367\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20392\" => [{ART::Parameters.new({\"_route\" => \"_384\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20440\" => [{ART::Parameters.new({\"_route\" => \"_498\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20490\" => [{ART::Parameters.new({\"_route\" => \"_369\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20542\" => [{ART::Parameters.new({\"_route\" => \"_408\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20590\" => [{ART::Parameters.new({\"_route\" => \"_413\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20652\" => [{ART::Parameters.new({\"_route\" => \"_44\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20699\" => [{ART::Parameters.new({\"_route\" => \"_256\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20748\" => [{ART::Parameters.new({\"_route\" => \"_173\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20796\" => [{ART::Parameters.new({\"_route\" => \"_266\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20844\" => [{ART::Parameters.new({\"_route\" => \"_392\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20892\" => [{ART::Parameters.new({\"_route\" => \"_430\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20940\" => [{ART::Parameters.new({\"_route\" => \"_482\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"20993\" => [{ART::Parameters.new({\"_route\" => \"_49\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21041\" => [{ART::Parameters.new({\"_route\" => \"_94\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21089\" => [{ART::Parameters.new({\"_route\" => \"_407\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21142\" => [{ART::Parameters.new({\"_route\" => \"_65\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21190\" => [{ART::Parameters.new({\"_route\" => \"_181\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21238\" => [{ART::Parameters.new({\"_route\" => \"_437\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21291\" => [{ART::Parameters.new({\"_route\" => \"_76\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21339\" => [{ART::Parameters.new({\"_route\" => \"_357\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21392\" => [{ART::Parameters.new({\"_route\" => \"_80\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21440\" => [{ART::Parameters.new({\"_route\" => \"_106\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21493\" => [{ART::Parameters.new({\"_route\" => \"_83\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21541\" => [{ART::Parameters.new({\"_route\" => \"_255\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21589\" => [{ART::Parameters.new({\"_route\" => \"_330\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21642\" => [{ART::Parameters.new({\"_route\" => \"_100\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21690\" => [{ART::Parameters.new({\"_route\" => \"_396\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21738\" => [{ART::Parameters.new({\"_route\" => \"_422\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21791\" => [{ART::Parameters.new({\"_route\" => \"_149\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21839\" => [{ART::Parameters.new({\"_route\" => \"_324\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21892\" => [{ART::Parameters.new({\"_route\" => \"_164\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21940\" => [{ART::Parameters.new({\"_route\" => \"_423\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"21990\" => [{ART::Parameters.new({\"_route\" => \"_241\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22042\" => [{ART::Parameters.new({\"_route\" => \"_290\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22090\" => [{ART::Parameters.new({\"_route\" => \"_335\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22140\" => [{ART::Parameters.new({\"_route\" => \"_373\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22189\" => [{ART::Parameters.new({\"_route\" => \"_375\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22238\" => [{ART::Parameters.new({\"_route\" => \"_450\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22287\" => [{ART::Parameters.new({\"_route\" => \"_464\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22345\" => [{ART::Parameters.new({\"_route\" => \"_51\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22393\" => [{ART::Parameters.new({\"_route\" => \"_77\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22441\" => [{ART::Parameters.new({\"_route\" => \"_234\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22489\" => [{ART::Parameters.new({\"_route\" => \"_394\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22542\" => [{ART::Parameters.new({\"_route\" => \"_88\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22590\" => [{ART::Parameters.new({\"_route\" => \"_155\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22643\" => [{ART::Parameters.new({\"_route\" => \"_96\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22691\" => [{ART::Parameters.new({\"_route\" => \"_298\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22739\" => [{ART::Parameters.new({\"_route\" => \"_470\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22792\" => [{ART::Parameters.new({\"_route\" => \"_109\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22840\" => [{ART::Parameters.new({\"_route\" => \"_204\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22893\" => [{ART::Parameters.new({\"_route\" => \"_115\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22941\" => [{ART::Parameters.new({\"_route\" => \"_145\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"22994\" => [{ART::Parameters.new({\"_route\" => \"_123\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23042\" => [{ART::Parameters.new({\"_route\" => \"_277\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23090\" => [{ART::Parameters.new({\"_route\" => \"_473\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23143\" => [{ART::Parameters.new({\"_route\" => \"_334\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23191\" => [{ART::Parameters.new({\"_route\" => \"_493\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23244\" => [{ART::Parameters.new({\"_route\" => \"_372\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23292\" => [{ART::Parameters.new({\"_route\" => \"_432\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23340\" => [{ART::Parameters.new({\"_route\" => \"_436\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23393\" => [{ART::Parameters.new({\"_route\" => \"_425\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23441\" => [{ART::Parameters.new({\"_route\" => \"_456\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23489\" => [{ART::Parameters.new({\"_route\" => \"_474\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23539\" => [{ART::Parameters.new({\"_route\" => \"_485\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23594\" => [{ART::Parameters.new({\"_route\" => \"_91\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23646\" => [{ART::Parameters.new({\"_route\" => \"_110\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23694\" => [{ART::Parameters.new({\"_route\" => \"_114\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23750\" => [{ART::Parameters.new({\"_route\" => \"_118\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23796\" => [{ART::Parameters.new({\"_route\" => \"_475\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23844\" => [{ART::Parameters.new({\"_route\" => \"_366\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23897\" => [{ART::Parameters.new({\"_route\" => \"_167\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23945\" => [{ART::Parameters.new({\"_route\" => \"_192\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"23993\" => [{ART::Parameters.new({\"_route\" => \"_342\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24046\" => [{ART::Parameters.new({\"_route\" => \"_229\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24097\" => [{ART::Parameters.new({\"_route\" => \"_235\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24144\" => [{ART::Parameters.new({\"_route\" => \"_302\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24193\" => [{ART::Parameters.new({\"_route\" => \"_322\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24246\" => [{ART::Parameters.new({\"_route\" => \"_237\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24294\" => [{ART::Parameters.new({\"_route\" => \"_293\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24347\" => [{ART::Parameters.new({\"_route\" => \"_239\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24395\" => [{ART::Parameters.new({\"_route\" => \"_444\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24443\" => [{ART::Parameters.new({\"_route\" => \"_491\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24491\" => [{ART::Parameters.new({\"_route\" => \"_492\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24541\" => [{ART::Parameters.new({\"_route\" => \"_258\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24590\" => [{ART::Parameters.new({\"_route\" => \"_317\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24639\" => [{ART::Parameters.new({\"_route\" => \"_361\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24688\" => [{ART::Parameters.new({\"_route\" => \"_391\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24737\" => [{ART::Parameters.new({\"_route\" => \"_462\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24786\" => [{ART::Parameters.new({\"_route\" => \"_476\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24837\" => [{ART::Parameters.new({\"_route\" => \"_501\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24889\" => [{ART::Parameters.new({\"_route\" => \"_514\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24937\" => [{ART::Parameters.new({\"_route\" => \"_731\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"24990\" => [{ART::Parameters.new({\"_route\" => \"_522\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25038\" => [{ART::Parameters.new({\"_route\" => \"_693\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25091\" => [{ART::Parameters.new({\"_route\" => \"_537\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25139\" => [{ART::Parameters.new({\"_route\" => \"_554\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25187\" => [{ART::Parameters.new({\"_route\" => \"_645\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25235\" => [{ART::Parameters.new({\"_route\" => \"_862\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25288\" => [{ART::Parameters.new({\"_route\" => \"_539\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25336\" => [{ART::Parameters.new({\"_route\" => \"_729\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25384\" => [{ART::Parameters.new({\"_route\" => \"_897\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25437\" => [{ART::Parameters.new({\"_route\" => \"_561\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25485\" => [{ART::Parameters.new({\"_route\" => \"_615\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25533\" => [{ART::Parameters.new({\"_route\" => \"_764\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25581\" => [{ART::Parameters.new({\"_route\" => \"_948\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25634\" => [{ART::Parameters.new({\"_route\" => \"_617\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25682\" => [{ART::Parameters.new({\"_route\" => \"_671\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25735\" => [{ART::Parameters.new({\"_route\" => \"_649\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25783\" => [{ART::Parameters.new({\"_route\" => \"_651\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25831\" => [{ART::Parameters.new({\"_route\" => \"_684\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25884\" => [{ART::Parameters.new({\"_route\" => \"_669\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25932\" => [{ART::Parameters.new({\"_route\" => \"_743\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"25980\" => [{ART::Parameters.new({\"_route\" => \"_962\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26033\" => [{ART::Parameters.new({\"_route\" => \"_694\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26081\" => [{ART::Parameters.new({\"_route\" => \"_985\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26134\" => [{ART::Parameters.new({\"_route\" => \"_707\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26182\" => [{ART::Parameters.new({\"_route\" => \"_718\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26235\" => [{ART::Parameters.new({\"_route\" => \"_720\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26283\" => [{ART::Parameters.new({\"_route\" => \"_745\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26333\" => [{ART::Parameters.new({\"_route\" => \"_874\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26391\" => [{ART::Parameters.new({\"_route\" => \"_502\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26439\" => [{ART::Parameters.new({\"_route\" => \"_667\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26487\" => [{ART::Parameters.new({\"_route\" => \"_911\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26535\" => [{ART::Parameters.new({\"_route\" => \"_942\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26585\" => [{ART::Parameters.new({\"_route\" => \"_504\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26637\" => [{ART::Parameters.new({\"_route\" => \"_524\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26685\" => [{ART::Parameters.new({\"_route\" => \"_732\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26738\" => [{ART::Parameters.new({\"_route\" => \"_596\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26786\" => [{ART::Parameters.new({\"_route\" => \"_601\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26839\" => [{ART::Parameters.new({\"_route\" => \"_620\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26887\" => [{ART::Parameters.new({\"_route\" => \"_631\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26935\" => [{ART::Parameters.new({\"_route\" => \"_771\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"26983\" => [{ART::Parameters.new({\"_route\" => \"_937\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27031\" => [{ART::Parameters.new({\"_route\" => \"_999\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27084\" => [{ART::Parameters.new({\"_route\" => \"_657\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27132\" => [{ART::Parameters.new({\"_route\" => \"_701\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27185\" => [{ART::Parameters.new({\"_route\" => \"_662\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27233\" => [{ART::Parameters.new({\"_route\" => \"_797\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27281\" => [{ART::Parameters.new({\"_route\" => \"_924\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27334\" => [{ART::Parameters.new({\"_route\" => \"_702\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27382\" => [{ART::Parameters.new({\"_route\" => \"_750\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27435\" => [{ART::Parameters.new({\"_route\" => \"_749\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27483\" => [{ART::Parameters.new({\"_route\" => \"_837\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27533\" => [{ART::Parameters.new({\"_route\" => \"_758\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27585\" => [{ART::Parameters.new({\"_route\" => \"_810\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27633\" => [{ART::Parameters.new({\"_route\" => \"_902\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27683\" => [{ART::Parameters.new({\"_route\" => \"_845\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27741\" => [{ART::Parameters.new({\"_route\" => \"_503\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27792\" => [{ART::Parameters.new({\"_route\" => \"_756\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27839\" => [{ART::Parameters.new({\"_route\" => \"_799\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27888\" => [{ART::Parameters.new({\"_route\" => \"_769\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27936\" => [{ART::Parameters.new({\"_route\" => \"_981\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"27989\" => [{ART::Parameters.new({\"_route\" => \"_507\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28037\" => [{ART::Parameters.new({\"_route\" => \"_672\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28085\" => [{ART::Parameters.new({\"_route\" => \"_790\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28138\" => [{ART::Parameters.new({\"_route\" => \"_515\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28186\" => [{ART::Parameters.new({\"_route\" => \"_523\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28234\" => [{ART::Parameters.new({\"_route\" => \"_957\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28282\" => [{ART::Parameters.new({\"_route\" => \"_995\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28335\" => [{ART::Parameters.new({\"_route\" => \"_532\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28383\" => [{ART::Parameters.new({\"_route\" => \"_642\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28433\" => [{ART::Parameters.new({\"_route\" => \"_579\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28485\" => [{ART::Parameters.new({\"_route\" => \"_625\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28533\" => [{ART::Parameters.new({\"_route\" => \"_916\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28586\" => [{ART::Parameters.new({\"_route\" => \"_633\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28634\" => [{ART::Parameters.new({\"_route\" => \"_656\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28687\" => [{ART::Parameters.new({\"_route\" => \"_658\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28735\" => [{ART::Parameters.new({\"_route\" => \"_943\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28788\" => [{ART::Parameters.new({\"_route\" => \"_664\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28836\" => [{ART::Parameters.new({\"_route\" => \"_852\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28884\" => [{ART::Parameters.new({\"_route\" => \"_870\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28937\" => [{ART::Parameters.new({\"_route\" => \"_683\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"28985\" => [{ART::Parameters.new({\"_route\" => \"_915\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29038\" => [{ART::Parameters.new({\"_route\" => \"_719\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29086\" => [{ART::Parameters.new({\"_route\" => \"_859\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29134\" => [{ART::Parameters.new({\"_route\" => \"_912\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29182\" => [{ART::Parameters.new({\"_route\" => \"_978\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29235\" => [{ART::Parameters.new({\"_route\" => \"_738\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29283\" => [{ART::Parameters.new({\"_route\" => \"_883\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29333\" => [{ART::Parameters.new({\"_route\" => \"_741\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29382\" => [{ART::Parameters.new({\"_route\" => \"_760\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29431\" => [{ART::Parameters.new({\"_route\" => \"_895\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29489\" => [{ART::Parameters.new({\"_route\" => \"_505\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29537\" => [{ART::Parameters.new({\"_route\" => \"_935\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29590\" => [{ART::Parameters.new({\"_route\" => \"_509\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29638\" => [{ART::Parameters.new({\"_route\" => \"_820\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29686\" => [{ART::Parameters.new({\"_route\" => \"_910\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29739\" => [{ART::Parameters.new({\"_route\" => \"_518\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29787\" => [{ART::Parameters.new({\"_route\" => \"_618\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29840\" => [{ART::Parameters.new({\"_route\" => \"_546\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29888\" => [{ART::Parameters.new({\"_route\" => \"_740\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29936\" => [{ART::Parameters.new({\"_route\" => \"_867\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"29989\" => [{ART::Parameters.new({\"_route\" => \"_572\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30037\" => [{ART::Parameters.new({\"_route\" => \"_952\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30090\" => [{ART::Parameters.new({\"_route\" => \"_573\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30138\" => [{ART::Parameters.new({\"_route\" => \"_692\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30186\" => [{ART::Parameters.new({\"_route\" => \"_700\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30234\" => [{ART::Parameters.new({\"_route\" => \"_772\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30284\" => [{ART::Parameters.new({\"_route\" => \"_653\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30336\" => [{ART::Parameters.new({\"_route\" => \"_695\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30384\" => [{ART::Parameters.new({\"_route\" => \"_748\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30437\" => [{ART::Parameters.new({\"_route\" => \"_710\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30485\" => [{ART::Parameters.new({\"_route\" => \"_716\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30533\" => [{ART::Parameters.new({\"_route\" => \"_969\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30586\" => [{ART::Parameters.new({\"_route\" => \"_734\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30634\" => [{ART::Parameters.new({\"_route\" => \"_742\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30682\" => [{ART::Parameters.new({\"_route\" => \"_844\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30735\" => [{ART::Parameters.new({\"_route\" => \"_763\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30783\" => [{ART::Parameters.new({\"_route\" => \"_965\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30836\" => [{ART::Parameters.new({\"_route\" => \"_778\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30884\" => [{ART::Parameters.new({\"_route\" => \"_813\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30932\" => [{ART::Parameters.new({\"_route\" => \"_831\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"30982\" => [{ART::Parameters.new({\"_route\" => \"_955\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31031\" => [{ART::Parameters.new({\"_route\" => \"_997\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31089\" => [{ART::Parameters.new({\"_route\" => \"_506\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31137\" => [{ART::Parameters.new({\"_route\" => \"_575\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31190\" => [{ART::Parameters.new({\"_route\" => \"_516\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31238\" => [{ART::Parameters.new({\"_route\" => \"_553\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31291\" => [{ART::Parameters.new({\"_route\" => \"_528\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31339\" => [{ART::Parameters.new({\"_route\" => \"_847\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31387\" => [{ART::Parameters.new({\"_route\" => \"_904\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31440\" => [{ART::Parameters.new({\"_route\" => \"_574\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31488\" => [{ART::Parameters.new({\"_route\" => \"_818\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31538\" => [{ART::Parameters.new({\"_route\" => \"_577\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31590\" => [{ART::Parameters.new({\"_route\" => \"_584\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31638\" => [{ART::Parameters.new({\"_route\" => \"_905\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31691\" => [{ART::Parameters.new({\"_route\" => \"_612\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31739\" => [{ART::Parameters.new({\"_route\" => \"_688\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31787\" => [{ART::Parameters.new({\"_route\" => \"_854\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31840\" => [{ART::Parameters.new({\"_route\" => \"_613\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31888\" => [{ART::Parameters.new({\"_route\" => \"_767\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31941\" => [{ART::Parameters.new({\"_route\" => \"_666\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"31989\" => [{ART::Parameters.new({\"_route\" => \"_759\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32037\" => [{ART::Parameters.new({\"_route\" => \"_827\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32085\" => [{ART::Parameters.new({\"_route\" => \"_840\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32138\" => [{ART::Parameters.new({\"_route\" => \"_680\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32186\" => [{ART::Parameters.new({\"_route\" => \"_784\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32234\" => [{ART::Parameters.new({\"_route\" => \"_842\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32282\" => [{ART::Parameters.new({\"_route\" => \"_860\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32332\" => [{ART::Parameters.new({\"_route\" => \"_704\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32381\" => [{ART::Parameters.new({\"_route\" => \"_727\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32430\" => [{ART::Parameters.new({\"_route\" => \"_777\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32482\" => [{ART::Parameters.new({\"_route\" => \"_838\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32530\" => [{ART::Parameters.new({\"_route\" => \"_861\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32583\" => [{ART::Parameters.new({\"_route\" => \"_849\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32631\" => [{ART::Parameters.new({\"_route\" => \"_982\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32679\" => [{ART::Parameters.new({\"_route\" => \"_986\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32741\" => [{ART::Parameters.new({\"_route\" => \"_508\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32788\" => [{ART::Parameters.new({\"_route\" => \"_517\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32837\" => [{ART::Parameters.new({\"_route\" => \"_622\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32890\" => [{ART::Parameters.new({\"_route\" => \"_513\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32938\" => [{ART::Parameters.new({\"_route\" => \"_655\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"32986\" => [{ART::Parameters.new({\"_route\" => \"_843\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33034\" => [{ART::Parameters.new({\"_route\" => \"_939\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33084\" => [{ART::Parameters.new({\"_route\" => \"_529\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33136\" => [{ART::Parameters.new({\"_route\" => \"_535\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33184\" => [{ART::Parameters.new({\"_route\" => \"_685\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33240\" => [{ART::Parameters.new({\"_route\" => \"_559\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33287\" => [{ART::Parameters.new({\"_route\" => \"_661\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33336\" => [{ART::Parameters.new({\"_route\" => \"_768\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33389\" => [{ART::Parameters.new({\"_route\" => \"_589\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33437\" => [{ART::Parameters.new({\"_route\" => \"_647\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33485\" => [{ART::Parameters.new({\"_route\" => \"_652\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33533\" => [{ART::Parameters.new({\"_route\" => \"_834\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33586\" => [{ART::Parameters.new({\"_route\" => \"_591\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33634\" => [{ART::Parameters.new({\"_route\" => \"_599\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33687\" => [{ART::Parameters.new({\"_route\" => \"_787\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33734\" => [{ART::Parameters.new({\"_route\" => \"_848\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33787\" => [{ART::Parameters.new({\"_route\" => \"_796\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33835\" => [{ART::Parameters.new({\"_route\" => \"_877\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33885\" => [{ART::Parameters.new({\"_route\" => \"_809\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33934\" => [{ART::Parameters.new({\"_route\" => \"_817\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"33986\" => [{ART::Parameters.new({\"_route\" => \"_819\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34034\" => [{ART::Parameters.new({\"_route\" => \"_865\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34084\" => [{ART::Parameters.new({\"_route\" => \"_919\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34133\" => [{ART::Parameters.new({\"_route\" => \"_949\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34191\" => [{ART::Parameters.new({\"_route\" => \"_510\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34239\" => [{ART::Parameters.new({\"_route\" => \"_590\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34287\" => [{ART::Parameters.new({\"_route\" => \"_597\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34335\" => [{ART::Parameters.new({\"_route\" => \"_682\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34383\" => [{ART::Parameters.new({\"_route\" => \"_723\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34436\" => [{ART::Parameters.new({\"_route\" => \"_521\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34484\" => [{ART::Parameters.new({\"_route\" => \"_594\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34532\" => [{ART::Parameters.new({\"_route\" => \"_689\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34580\" => [{ART::Parameters.new({\"_route\" => \"_713\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34628\" => [{ART::Parameters.new({\"_route\" => \"_889\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34681\" => [{ART::Parameters.new({\"_route\" => \"_531\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34729\" => [{ART::Parameters.new({\"_route\" => \"_639\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34780\" => [{ART::Parameters.new({\"_route\" => \"_646\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34827\" => [{ART::Parameters.new({\"_route\" => \"_659\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34876\" => [{ART::Parameters.new({\"_route\" => \"_959\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34929\" => [{ART::Parameters.new({\"_route\" => \"_550\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"34977\" => [{ART::Parameters.new({\"_route\" => \"_833\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35025\" => [{ART::Parameters.new({\"_route\" => \"_899\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35081\" => [{ART::Parameters.new({\"_route\" => \"_580\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35128\" => [{ART::Parameters.new({\"_route\" => \"_762\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35177\" => [{ART::Parameters.new({\"_route\" => \"_896\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35230\" => [{ART::Parameters.new({\"_route\" => \"_595\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35278\" => [{ART::Parameters.new({\"_route\" => \"_933\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35328\" => [{ART::Parameters.new({\"_route\" => \"_610\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35380\" => [{ART::Parameters.new({\"_route\" => \"_629\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35428\" => [{ART::Parameters.new({\"_route\" => \"_744\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35481\" => [{ART::Parameters.new({\"_route\" => \"_674\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35529\" => [{ART::Parameters.new({\"_route\" => \"_726\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35577\" => [{ART::Parameters.new({\"_route\" => \"_929\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35627\" => [{ART::Parameters.new({\"_route\" => \"_696\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35679\" => [{ART::Parameters.new({\"_route\" => \"_841\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35727\" => [{ART::Parameters.new({\"_route\" => \"_890\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35777\" => [{ART::Parameters.new({\"_route\" => \"_885\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35826\" => [{ART::Parameters.new({\"_route\" => \"_888\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35875\" => [{ART::Parameters.new({\"_route\" => \"_996\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35933\" => [{ART::Parameters.new({\"_route\" => \"_511\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"35981\" => [{ART::Parameters.new({\"_route\" => \"_576\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36029\" => [{ART::Parameters.new({\"_route\" => \"_623\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36082\" => [{ART::Parameters.new({\"_route\" => \"_560\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36129\" => [{ART::Parameters.new({\"_route\" => \"_585\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36182\" => [{ART::Parameters.new({\"_route\" => \"_570\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36230\" => [{ART::Parameters.new({\"_route\" => \"_578\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36281\" => [{ART::Parameters.new({\"_route\" => \"_780\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36328\" => [{ART::Parameters.new({\"_route\" => \"_808\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36382\" => [{ART::Parameters.new({\"_route\" => \"_593\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36430\" => [{ART::Parameters.new({\"_route\" => \"_900\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36483\" => [{ART::Parameters.new({\"_route\" => \"_632\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36531\" => [{ART::Parameters.new({\"_route\" => \"_654\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36579\" => [{ART::Parameters.new({\"_route\" => \"_721\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36627\" => [{ART::Parameters.new({\"_route\" => \"_836\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36680\" => [{ART::Parameters.new({\"_route\" => \"_637\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36728\" => [{ART::Parameters.new({\"_route\" => \"_737\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36784\" => [{ART::Parameters.new({\"_route\" => \"_699\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36831\" => [{ART::Parameters.new({\"_route\" => \"_822\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36880\" => [{ART::Parameters.new({\"_route\" => \"_853\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36933\" => [{ART::Parameters.new({\"_route\" => \"_708\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"36981\" => [{ART::Parameters.new({\"_route\" => \"_871\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37034\" => [{ART::Parameters.new({\"_route\" => \"_752\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37082\" => [{ART::Parameters.new({\"_route\" => \"_989\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37132\" => [{ART::Parameters.new({\"_route\" => \"_855\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37184\" => [{ART::Parameters.new({\"_route\" => \"_858\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37232\" => [{ART::Parameters.new({\"_route\" => \"_898\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37282\" => [{ART::Parameters.new({\"_route\" => \"_903\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37331\" => [{ART::Parameters.new({\"_route\" => \"_909\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37380\" => [{ART::Parameters.new({\"_route\" => \"_950\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37441\" => [{ART::Parameters.new({\"_route\" => \"_512\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37488\" => [{ART::Parameters.new({\"_route\" => \"_691\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37537\" => [{ART::Parameters.new({\"_route\" => \"_686\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37587\" => [{ART::Parameters.new({\"_route\" => \"_527\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37639\" => [{ART::Parameters.new({\"_route\" => \"_541\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37687\" => [{ART::Parameters.new({\"_route\" => \"_956\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37740\" => [{ART::Parameters.new({\"_route\" => \"_555\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37788\" => [{ART::Parameters.new({\"_route\" => \"_681\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37841\" => [{ART::Parameters.new({\"_route\" => \"_556\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37889\" => [{ART::Parameters.new({\"_route\" => \"_802\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37939\" => [{ART::Parameters.new({\"_route\" => \"_558\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"37991\" => [{ART::Parameters.new({\"_route\" => \"_564\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38039\" => [{ART::Parameters.new({\"_route\" => \"_670\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38087\" => [{ART::Parameters.new({\"_route\" => \"_884\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38140\" => [{ART::Parameters.new({\"_route\" => \"_627\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38187\" => [{ART::Parameters.new({\"_route\" => \"_746\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38240\" => [{ART::Parameters.new({\"_route\" => \"_668\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38291\" => [{ART::Parameters.new({\"_route\" => \"_712\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38338\" => [{ART::Parameters.new({\"_route\" => \"_863\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38387\" => [{ART::Parameters.new({\"_route\" => \"_801\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38440\" => [{ART::Parameters.new({\"_route\" => \"_709\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38488\" => [{ART::Parameters.new({\"_route\" => \"_850\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38536\" => [{ART::Parameters.new({\"_route\" => \"_918\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38586\" => [{ART::Parameters.new({\"_route\" => \"_803\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38638\" => [{ART::Parameters.new({\"_route\" => \"_864\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38686\" => [{ART::Parameters.new({\"_route\" => \"_880\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38734\" => [{ART::Parameters.new({\"_route\" => \"_927\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38787\" => [{ART::Parameters.new({\"_route\" => \"_930\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38835\" => [{ART::Parameters.new({\"_route\" => \"_951\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38883\" => [{ART::Parameters.new({\"_route\" => \"_963\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38942\" => [{ART::Parameters.new({\"_route\" => \"_519\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"38990\" => [{ART::Parameters.new({\"_route\" => \"_823\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39038\" => [{ART::Parameters.new({\"_route\" => \"_954\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39091\" => [{ART::Parameters.new({\"_route\" => \"_525\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39139\" => [{ART::Parameters.new({\"_route\" => \"_991\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39189\" => [{ART::Parameters.new({\"_route\" => \"_536\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39241\" => [{ART::Parameters.new({\"_route\" => \"_545\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39289\" => [{ART::Parameters.new({\"_route\" => \"_944\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39342\" => [{ART::Parameters.new({\"_route\" => \"_557\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39390\" => [{ART::Parameters.new({\"_route\" => \"_783\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39438\" => [{ART::Parameters.new({\"_route\" => \"_807\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39491\" => [{ART::Parameters.new({\"_route\" => \"_586\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39539\" => [{ART::Parameters.new({\"_route\" => \"_711\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39592\" => [{ART::Parameters.new({\"_route\" => \"_598\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39640\" => [{ART::Parameters.new({\"_route\" => \"_635\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39688\" => [{ART::Parameters.new({\"_route\" => \"_983\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39741\" => [{ART::Parameters.new({\"_route\" => \"_634\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39789\" => [{ART::Parameters.new({\"_route\" => \"_641\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39840\" => [{ART::Parameters.new({\"_route\" => \"_779\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39887\" => [{ART::Parameters.new({\"_route\" => \"_876\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39936\" => [{ART::Parameters.new({\"_route\" => \"_811\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"39984\" => [{ART::Parameters.new({\"_route\" => \"_824\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40037\" => [{ART::Parameters.new({\"_route\" => \"_660\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40085\" => [{ART::Parameters.new({\"_route\" => \"_789\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40138\" => [{ART::Parameters.new({\"_route\" => \"_733\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40186\" => [{ART::Parameters.new({\"_route\" => \"_735\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40234\" => [{ART::Parameters.new({\"_route\" => \"_882\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40282\" => [{ART::Parameters.new({\"_route\" => \"_967\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40332\" => [{ART::Parameters.new({\"_route\" => \"_736\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40381\" => [{ART::Parameters.new({\"_route\" => \"_753\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40430\" => [{ART::Parameters.new({\"_route\" => \"_786\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40479\" => [{ART::Parameters.new({\"_route\" => \"_907\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40528\" => [{ART::Parameters.new({\"_route\" => \"_920\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40577\" => [{ART::Parameters.new({\"_route\" => \"_971\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40635\" => [{ART::Parameters.new({\"_route\" => \"_520\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40683\" => [{ART::Parameters.new({\"_route\" => \"_891\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40739\" => [{ART::Parameters.new({\"_route\" => \"_534\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40785\" => [{ART::Parameters.new({\"_route\" => \"_602\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40834\" => [{ART::Parameters.new({\"_route\" => \"_605\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40882\" => [{ART::Parameters.new({\"_route\" => \"_979\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40932\" => [{ART::Parameters.new({\"_route\" => \"_547\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"40987\" => [{ART::Parameters.new({\"_route\" => \"_549\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41034\" => [{ART::Parameters.new({\"_route\" => \"_755\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41083\" => [{ART::Parameters.new({\"_route\" => \"_922\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41131\" => [{ART::Parameters.new({\"_route\" => \"_977\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41184\" => [{ART::Parameters.new({\"_route\" => \"_565\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41232\" => [{ART::Parameters.new({\"_route\" => \"_926\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41282\" => [{ART::Parameters.new({\"_route\" => \"_571\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41331\" => [{ART::Parameters.new({\"_route\" => \"_581\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41380\" => [{ART::Parameters.new({\"_route\" => \"_619\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41429\" => [{ART::Parameters.new({\"_route\" => \"_636\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41481\" => [{ART::Parameters.new({\"_route\" => \"_679\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41529\" => [{ART::Parameters.new({\"_route\" => \"_866\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41577\" => [{ART::Parameters.new({\"_route\" => \"_973\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41630\" => [{ART::Parameters.new({\"_route\" => \"_690\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41678\" => [{ART::Parameters.new({\"_route\" => \"_775\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41731\" => [{ART::Parameters.new({\"_route\" => \"_722\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41779\" => [{ART::Parameters.new({\"_route\" => \"_906\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41827\" => [{ART::Parameters.new({\"_route\" => \"_946\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41877\" => [{ART::Parameters.new({\"_route\" => \"_788\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41929\" => [{ART::Parameters.new({\"_route\" => \"_828\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"41977\" => [{ART::Parameters.new({\"_route\" => \"_892\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42025\" => [{ART::Parameters.new({\"_route\" => \"_972\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42075\" => [{ART::Parameters.new({\"_route\" => \"_829\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42127\" => [{ART::Parameters.new({\"_route\" => \"_923\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42175\" => [{ART::Parameters.new({\"_route\" => \"_947\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42234\" => [{ART::Parameters.new({\"_route\" => \"_526\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42282\" => [{ART::Parameters.new({\"_route\" => \"_614\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42330\" => [{ART::Parameters.new({\"_route\" => \"_621\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42383\" => [{ART::Parameters.new({\"_route\" => \"_543\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42431\" => [{ART::Parameters.new({\"_route\" => \"_812\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42487\" => [{ART::Parameters.new({\"_route\" => \"_548\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42534\" => [{ART::Parameters.new({\"_route\" => \"_747\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42583\" => [{ART::Parameters.new({\"_route\" => \"_715\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42631\" => [{ART::Parameters.new({\"_route\" => \"_940\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42684\" => [{ART::Parameters.new({\"_route\" => \"_563\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42732\" => [{ART::Parameters.new({\"_route\" => \"_611\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42780\" => [{ART::Parameters.new({\"_route\" => \"_830\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42833\" => [{ART::Parameters.new({\"_route\" => \"_569\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42881\" => [{ART::Parameters.new({\"_route\" => \"_908\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42929\" => [{ART::Parameters.new({\"_route\" => \"_913\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"42982\" => [{ART::Parameters.new({\"_route\" => \"_644\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43030\" => [{ART::Parameters.new({\"_route\" => \"_776\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43078\" => [{ART::Parameters.new({\"_route\" => \"_856\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43131\" => [{ART::Parameters.new({\"_route\" => \"_650\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43179\" => [{ART::Parameters.new({\"_route\" => \"_761\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43232\" => [{ART::Parameters.new({\"_route\" => \"_663\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43280\" => [{ART::Parameters.new({\"_route\" => \"_754\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43333\" => [{ART::Parameters.new({\"_route\" => \"_665\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43381\" => [{ART::Parameters.new({\"_route\" => \"_805\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43429\" => [{ART::Parameters.new({\"_route\" => \"_846\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43477\" => [{ART::Parameters.new({\"_route\" => \"_857\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43530\" => [{ART::Parameters.new({\"_route\" => \"_675\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43578\" => [{ART::Parameters.new({\"_route\" => \"_839\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43626\" => [{ART::Parameters.new({\"_route\" => \"_968\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43676\" => [{ART::Parameters.new({\"_route\" => \"_697\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43728\" => [{ART::Parameters.new({\"_route\" => \"_725\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43776\" => [{ART::Parameters.new({\"_route\" => \"_794\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43829\" => [{ART::Parameters.new({\"_route\" => \"_773\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43877\" => [{ART::Parameters.new({\"_route\" => \"_992\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43930\" => [{ART::Parameters.new({\"_route\" => \"_901\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"43978\" => [{ART::Parameters.new({\"_route\" => \"_970\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44028\" => [{ART::Parameters.new({\"_route\" => \"_964\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44086\" => [{ART::Parameters.new({\"_route\" => \"_530\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44134\" => [{ART::Parameters.new({\"_route\" => \"_703\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44187\" => [{ART::Parameters.new({\"_route\" => \"_533\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44235\" => [{ART::Parameters.new({\"_route\" => \"_739\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44283\" => [{ART::Parameters.new({\"_route\" => \"_791\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44331\" => [{ART::Parameters.new({\"_route\" => \"_987\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44384\" => [{ART::Parameters.new({\"_route\" => \"_566\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44432\" => [{ART::Parameters.new({\"_route\" => \"_592\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44488\" => [{ART::Parameters.new({\"_route\" => \"_568\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44534\" => [{ART::Parameters.new({\"_route\" => \"_868\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44583\" => [{ART::Parameters.new({\"_route\" => \"_878\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44636\" => [{ART::Parameters.new({\"_route\" => \"_588\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44684\" => [{ART::Parameters.new({\"_route\" => \"_793\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44732\" => [{ART::Parameters.new({\"_route\" => \"_917\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44785\" => [{ART::Parameters.new({\"_route\" => \"_600\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44833\" => [{ART::Parameters.new({\"_route\" => \"_728\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44886\" => [{ART::Parameters.new({\"_route\" => \"_603\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44934\" => [{ART::Parameters.new({\"_route\" => \"_765\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"44987\" => [{ART::Parameters.new({\"_route\" => \"_607\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45035\" => [{ART::Parameters.new({\"_route\" => \"_676\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45083\" => [{ART::Parameters.new({\"_route\" => \"_804\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45136\" => [{ART::Parameters.new({\"_route\" => \"_609\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45184\" => [{ART::Parameters.new({\"_route\" => \"_961\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45232\" => [{ART::Parameters.new({\"_route\" => \"_980\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45282\" => [{ART::Parameters.new({\"_route\" => \"_714\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45334\" => [{ART::Parameters.new({\"_route\" => \"_730\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45382\" => [{ART::Parameters.new({\"_route\" => \"_806\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45430\" => [{ART::Parameters.new({\"_route\" => \"_825\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45478\" => [{ART::Parameters.new({\"_route\" => \"_879\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45526\" => [{ART::Parameters.new({\"_route\" => \"_893\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45576\" => [{ART::Parameters.new({\"_route\" => \"_928\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45628\" => [{ART::Parameters.new({\"_route\" => \"_932\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45676\" => [{ART::Parameters.new({\"_route\" => \"_958\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45726\" => [{ART::Parameters.new({\"_route\" => \"_984\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45784\" => [{ART::Parameters.new({\"_route\" => \"_538\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45832\" => [{ART::Parameters.new({\"_route\" => \"_993\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45882\" => [{ART::Parameters.new({\"_route\" => \"_542\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45934\" => [{ART::Parameters.new({\"_route\" => \"_551\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"45982\" => [{ART::Parameters.new({\"_route\" => \"_687\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46030\" => [{ART::Parameters.new({\"_route\" => \"_724\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46078\" => [{ART::Parameters.new({\"_route\" => \"_925\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46131\" => [{ART::Parameters.new({\"_route\" => \"_587\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46179\" => [{ART::Parameters.new({\"_route\" => \"_914\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46229\" => [{ART::Parameters.new({\"_route\" => \"_616\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46284\" => [{ART::Parameters.new({\"_route\" => \"_677\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46331\" => [{ART::Parameters.new({\"_route\" => \"_815\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46380\" => [{ART::Parameters.new({\"_route\" => \"_781\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46430\" => [{ART::Parameters.new({\"_route\" => \"_717\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46482\" => [{ART::Parameters.new({\"_route\" => \"_782\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46530\" => [{ART::Parameters.new({\"_route\" => \"_832\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46583\" => [{ART::Parameters.new({\"_route\" => \"_795\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46631\" => [{ART::Parameters.new({\"_route\" => \"_887\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46681\" => [{ART::Parameters.new({\"_route\" => \"_800\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46730\" => [{ART::Parameters.new({\"_route\" => \"_826\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46779\" => [{ART::Parameters.new({\"_route\" => \"_881\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46828\" => [{ART::Parameters.new({\"_route\" => \"_886\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46877\" => [{ART::Parameters.new({\"_route\" => \"_938\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46935\" => [{ART::Parameters.new({\"_route\" => \"_540\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"46983\" => [{ART::Parameters.new({\"_route\" => \"_643\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47033\" => [{ART::Parameters.new({\"_route\" => \"_544\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47082\" => [{ART::Parameters.new({\"_route\" => \"_552\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47134\" => [{ART::Parameters.new({\"_route\" => \"_567\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47182\" => [{ART::Parameters.new({\"_route\" => \"_608\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47230\" => [{ART::Parameters.new({\"_route\" => \"_698\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47278\" => [{ART::Parameters.new({\"_route\" => \"_988\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47331\" => [{ART::Parameters.new({\"_route\" => \"_583\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47379\" => [{ART::Parameters.new({\"_route\" => \"_998\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47432\" => [{ART::Parameters.new({\"_route\" => \"_604\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47480\" => [{ART::Parameters.new({\"_route\" => \"_630\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47528\" => [{ART::Parameters.new({\"_route\" => \"_706\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47576\" => [{ART::Parameters.new({\"_route\" => \"_976\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47629\" => [{ART::Parameters.new({\"_route\" => \"_673\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47677\" => [{ART::Parameters.new({\"_route\" => \"_678\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47725\" => [{ART::Parameters.new({\"_route\" => \"_931\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47775\" => [{ART::Parameters.new({\"_route\" => \"_751\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47824\" => [{ART::Parameters.new({\"_route\" => \"_766\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47876\" => [{ART::Parameters.new({\"_route\" => \"_792\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47924\" => [{ART::Parameters.new({\"_route\" => \"_814\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"47974\" => [{ART::Parameters.new({\"_route\" => \"_798\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48026\" => [{ART::Parameters.new({\"_route\" => \"_851\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48074\" => [{ART::Parameters.new({\"_route\" => \"_941\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48122\" => [{ART::Parameters.new({\"_route\" => \"_953\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48170\" => [{ART::Parameters.new({\"_route\" => \"_975\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48220\" => [{ART::Parameters.new({\"_route\" => \"_873\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48269\" => [{ART::Parameters.new({\"_route\" => \"_936\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48318\" => [{ART::Parameters.new({\"_route\" => \"_994\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48376\" => [{ART::Parameters.new({\"_route\" => \"_562\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48424\" => [{ART::Parameters.new({\"_route\" => \"_770\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48475\" => [{ART::Parameters.new({\"_route\" => \"_774\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48522\" => [{ART::Parameters.new({\"_route\" => \"_966\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48573\" => [{ART::Parameters.new({\"_route\" => \"_582\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48625\" => [{ART::Parameters.new({\"_route\" => \"_606\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48673\" => [{ART::Parameters.new({\"_route\" => \"_648\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48723\" => [{ART::Parameters.new({\"_route\" => \"_624\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48775\" => [{ART::Parameters.new({\"_route\" => \"_626\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48823\" => [{ART::Parameters.new({\"_route\" => \"_821\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48873\" => [{ART::Parameters.new({\"_route\" => \"_628\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48922\" => [{ART::Parameters.new({\"_route\" => \"_638\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"48974\" => [{ART::Parameters.new({\"_route\" => \"_640\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49022\" => [{ART::Parameters.new({\"_route\" => \"_990\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49072\" => [{ART::Parameters.new({\"_route\" => \"_705\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49121\" => [{ART::Parameters.new({\"_route\" => \"_757\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49176\" => [{ART::Parameters.new({\"_route\" => \"_785\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49223\" => [{ART::Parameters.new({\"_route\" => \"_875\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49270\" => [{ART::Parameters.new({\"_route\" => \"_894\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49319\" => [{ART::Parameters.new({\"_route\" => \"_945\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49375\" => [{ART::Parameters.new({\"_route\" => \"_816\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49422\" => [{ART::Parameters.new({\"_route\" => \"_872\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49471\" => [{ART::Parameters.new({\"_route\" => \"_921\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49519\" => [{ART::Parameters.new({\"_route\" => \"_960\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49567\" => [{ART::Parameters.new({\"_route\" => \"_974\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49620\" => [{ART::Parameters.new({\"_route\" => \"_835\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49668\" => [{ART::Parameters.new({\"_route\" => \"_934\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n  \"49718\" => [{ART::Parameters.new({\"_route\" => \"_869\"}), Set{\"a\", \"b\", \"c\"}, nil, nil, false, false, nil}],\n}\n####\n0\n"
  },
  {
    "path": "src/components/routing/spec/generator/url_generator_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct URLGeneratorTest < ASPEC::TestCase\n  def test_generate_default_port : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test\")))\n      .generate(\"test\", reference_type: :absolute_url).should eq \"http://localhost/base/test\"\n  end\n\n  def test_generate_secure_default_port : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test\")), context: ART::RequestContext.new(base_url: \"/base\", scheme: \"https\"))\n      .generate(\"test\", reference_type: :absolute_url).should eq \"https://localhost/base/test\"\n  end\n\n  def test_generate_non_standard_port : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test\")), context: ART::RequestContext.new(base_url: \"/base\", http_port: 8080))\n      .generate(\"test\", reference_type: :absolute_url).should eq \"http://localhost:8080/base/test\"\n  end\n\n  def test_generate_secure_non_standard_port : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test\")), context: ART::RequestContext.new(base_url: \"/base\", scheme: \"https\", https_port: 8080))\n      .generate(\"test\", reference_type: :absolute_url).should eq \"https://localhost:8080/base/test\"\n  end\n\n  def test_generate_no_parameters : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test\")))\n      .generate(\"test\").should eq \"/base/test\"\n  end\n\n  def test_generate_with_parameters : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test/{foo}\")))\n      .generate(\"test\", {\"foo\" => \"bar\"}).should eq \"/base/test/bar\"\n  end\n\n  def test_generate_nil_parameter : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test.{format}\", {\"format\" => nil})))\n      .generate(\"test\").should eq \"/base/test\"\n  end\n\n  def test_generate_nil_parameter_required : Nil\n    generator = self.generator self.routes ART::Route.new \"/test/{foo}/bar\", {\"foo\" => nil}\n\n    expect_raises ART::Exception::InvalidParameter do\n      generator.generate \"test\"\n    end\n  end\n\n  def test_generate_not_passed_optional_parameter_in_between : Nil\n    generator = self.generator self.routes ART::Route.new \"/{slug}/{page}\", {\"slug\" => \"index\", \"page\" => \"0\"}\n\n    generator.generate(\"test\", {\"page\" => 1}).should eq \"/base/index/1\"\n    generator.generate(\"test\").should eq \"/base/\"\n  end\n\n  @[DataProvider(\"query_param_provider\")]\n  def test_generate_extra_params(expected : String, key : String, value) : Nil\n    self\n      .generator(self.routes ART::Route.new \"/test\")\n      .generate(\"test\", {key => value}, reference_type: :absolute_url).should eq \"http://localhost/base/test#{expected}\"\n  end\n\n  def query_param_provider : Hash\n    {\n      \"nil value\"    => {\"\", \"foo\", nil},\n      \"string value\" => {\"?foo=bar\", \"foo\", \"bar\"},\n    }\n  end\n\n  def test_generate_extra_param_from_globals : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test\")), context: ART::RequestContext.new(base_url: \"/base\").set_parameter(\"bar\", \"bar\"))\n      .generate(\"test\", {\"foo\" => \"bar\"}).should eq \"/base/test?foo=bar\"\n  end\n\n  def test_generate_param_from_globals : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test/{foo}\")), context: ART::RequestContext.new(base_url: \"/base\").set_parameter(\"foo\", \"bar\"))\n      .generate(\"test\").should eq \"/base/test/bar\"\n  end\n\n  def test_generate_param_from_globals_overrides_defaults : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/{_locale}\", {\"_locale\" => \"en\"})), context: ART::RequestContext.new(base_url: \"/base\").set_parameter(\"_locale\", \"de\"))\n      .generate(\"test\").should eq \"/base/de\"\n  end\n\n  def test_generate_localized_routes_preserve_the_good_locale_in_url : Nil\n    routes = ART::RouteCollection.new\n\n    routes.add \"foo.en\", ART::Route.new \"/{_locale}/fork\", {\"_locale\" => \"en\", \"_canonical_route\" => \"foo\"}, {\"_locale\" => /en/}\n    routes.add \"foo.fr\", ART::Route.new \"/{_locale}/fourchette\", {\"_locale\" => \"fr\", \"_canonical_route\" => \"foo\"}, {\"_locale\" => /fr/}\n    routes.add \"fun.en\", ART::Route.new \"/fun\", {\"_locale\" => \"en\", \"_canonical_route\" => \"fun\"}, {\"_locale\" => /en/}\n    routes.add \"fun.fr\", ART::Route.new \"/amusant\", {\"_locale\" => \"fr\", \"_canonical_route\" => \"fun\"}, {\"_locale\" => /fr/}\n\n    ART.compile routes\n\n    generator = self.generator routes\n    generator.context.set_parameter \"_locale\", \"fr\"\n\n    generator.generate(\"foo\").should eq \"/base/fr/fourchette\"\n    generator.generate(\"foo.en\").should eq \"/base/en/fork\"\n    generator.generate(\"foo\", {\"_locale\" => \"en\"}).should eq \"/base/en/fork\"\n    generator.generate(\"foo.fr\", {\"_locale\" => \"en\"}).should eq \"/base/fr/fourchette\"\n\n    generator.generate(\"fun\").should eq \"/base/amusant\"\n    generator.generate(\"fun.en\").should eq \"/base/fun\"\n    generator.generate(\"fun\", {\"_locale\" => \"en\"}).should eq \"/base/fun\"\n    generator.generate(\"fun.fr\", {\"_locale\" => \"en\"}).should eq \"/base/amusant\"\n  end\n\n  def test_generate_invalid_locale : Nil\n    routes = ART::RouteCollection.new\n    name = \"test\"\n\n    {\"hr\" => \"/foo\", \"en\" => \"/bar\"}.each do |locale, path|\n      routes.add \"#{name}.#{locale}\", ART::Route.new path, {\"_locale\" => locale, \"_canonical_route\" => name}, {\"_locale\" => locale}\n    end\n\n    ART.compile routes\n\n    generator = self.generator routes, default_locale: \"fr\"\n\n    expect_raises ART::Exception::RouteNotFound do\n      generator.generate name\n    end\n  end\n\n  def test_generate_default_locale : Nil\n    routes = ART::RouteCollection.new\n    name = \"test\"\n\n    {\"hr\" => \"/foo\", \"en\" => \"/bar\"}.each do |locale, path|\n      routes.add \"#{name}.#{locale}\", ART::Route.new path, {\"_locale\" => locale, \"_canonical_route\" => name}, {\"_locale\" => locale}\n    end\n\n    ART.compile routes\n\n    self\n      .generator(routes, default_locale: \"hr\")\n      .generate(name, reference_type: :absolute_url).should eq \"http://localhost/base/foo\"\n  end\n\n  def test_generate_overridden_locale : Nil\n    routes = ART::RouteCollection.new\n    name = \"test\"\n\n    {\"hr\" => \"/foo\", \"en\" => \"/bar\"}.each do |locale, path|\n      routes.add \"#{name}.#{locale}\", ART::Route.new path, {\"_locale\" => locale, \"_canonical_route\" => name}, {\"_locale\" => locale}\n    end\n\n    ART.compile routes\n\n    self\n      .generator(routes, default_locale: \"hr\")\n      .generate(name, {\"_locale\" => \"en\"}, :absolute_url).should eq \"http://localhost/base/bar\"\n  end\n\n  def test_generate_overridden_via_request_context_locale : Nil\n    routes = ART::RouteCollection.new\n    name = \"test\"\n\n    {\"hr\" => \"/foo\", \"en\" => \"/bar\"}.each do |locale, path|\n      routes.add \"#{name}.#{locale}\", ART::Route.new path, {\"_locale\" => locale, \"_canonical_route\" => name}, {\"_locale\" => locale}\n    end\n\n    ART.compile routes\n\n    self\n      .generator(routes, context: ART::RequestContext.new(base_url: \"/base\").set_parameter(\"_locale\", \"en\"), default_locale: \"hr\")\n      .generate(name, reference_type: :absolute_url).should eq \"http://localhost/base/bar\"\n  end\n\n  def test_generate_no_routes : Nil\n    generator = self.generator self.routes ART::Route.new \"/test\"\n\n    expect_raises ART::Exception::RouteNotFound do\n      generator.generate(\"foo\", reference_type: :absolute_url)\n    end\n  end\n\n  def test_generate_missing_required_param : Nil\n    generator = self.generator self.routes ART::Route.new \"/test/{foo}\"\n\n    expect_raises ART::Exception::MissingRequiredParameters, %(Cannot generate URL for route 'test'. Missing required parameters: 'foo'.) do\n      generator.generate(\"test\", reference_type: :absolute_url)\n    end\n  end\n\n  def test_generate_invalid_optional_param : Nil\n    generator = self.generator self.routes ART::Route.new \"/test/{foo}\", {\"foo\" => \"1\"}, {\"foo\" => /\\d+/}\n\n    expect_raises ART::Exception::InvalidParameter, \"Parameter 'foo' for route 'test' must match '(?-imsx:\\\\d+)' (got 'bar') to generate the corresponding URL.\" do\n      generator.generate(\"test\", {\"foo\" => \"bar\"}, :absolute_url)\n    end\n  end\n\n  def test_generate_invalid_param : Nil\n    generator = self.generator self.routes ART::Route.new \"/test/{foo}\", requirements: {\"foo\" => /1|2/}\n\n    expect_raises ART::Exception::InvalidParameter, \"Parameter 'foo' for route 'test' must match '(?-imsx:1|2)' (got '0') to generate the corresponding URL.\" do\n      generator.generate(\"test\", {\"foo\" => \"0\"}, :absolute_url)\n    end\n  end\n\n  def test_generate_invalid_optional_param_non_strict : Nil\n    generator = self.generator self.routes ART::Route.new \"/test/{foo}\", {\"foo\" => \"1\"}, {\"foo\" => /\\d+/}\n    generator.strict_requirements = false\n\n    generator.generate(\"test\", {\"foo\" => \"bar\"}, :absolute_url).should eq \"\"\n  end\n\n  def test_generate_invalid_param_disabled_checks : Nil\n    generator = self.generator self.routes ART::Route.new \"/test/{foo}\", {\"foo\" => \"1\"}, {\"foo\" => /\\d+/}\n    generator.strict_requirements = nil\n\n    generator.generate(\"test\", {\"foo\" => \"bar\"}).should eq \"/base/test/bar\"\n  end\n\n  def test_generate_invalid_required_param : Nil\n    generator = self.generator self.routes ART::Route.new \"/test/{foo}\", requirements: {\"foo\" => /1|2/}\n\n    expect_raises ART::Exception::InvalidParameter do\n      generator.generate(\"test\", {\"foo\" => \"0\"}, :absolute_url)\n    end\n  end\n\n  def test_generate_required_param_empty_string : Nil\n    generator = self.generator self.routes ART::Route.new \"/{slug}\", requirements: {\"slug\" => /.+/}\n\n    expect_raises ART::Exception::InvalidParameter do\n      generator.generate \"test\", {\"slug\" => \"\"}\n    end\n  end\n\n  def test_generate_scheme_requirement_does_nothing_if_same_as_current_scheme : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/\", schemes: \"http\")), context: ART::RequestContext.new base_url: \"/base\", scheme: \"http\")\n      .generate(\"test\").should eq \"/base/\"\n  end\n\n  def test_generate_scheme_requirement_does_nothing_if_same_as_current_scheme_secure : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/\", schemes: \"https\")), context: ART::RequestContext.new base_url: \"/base\", scheme: \"https\")\n      .generate(\"test\").should eq \"/base/\"\n  end\n\n  def test_generate_scheme_requirement_forces_absolute_url : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/\", schemes: \"http\")), context: ART::RequestContext.new base_url: \"/base\", scheme: \"https\")\n      .generate(\"test\").should eq \"http://localhost/base/\"\n  end\n\n  def test_generate_scheme_requirement_forces_absolute_url_secure : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/\", schemes: \"https\")))\n      .generate(\"test\").should eq \"https://localhost/base/\"\n  end\n\n  def test_generate_scheme_requirement_creates_url_for_first_required_scheme : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/\", schemes: {\"Ftp\", \"https\"})))\n      .generate(\"test\").should eq \"ftp://localhost/base/\"\n  end\n\n  def test_path_with_two_starting_slashes : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"//path-and-not-domain\")), context: ART::RequestContext.new)\n      .generate(\"test\").should eq \"/path-and-not-domain\"\n  end\n\n  def test_generate_no_trailing_slash_for_multiple_optional_parameters : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/category/{slug1}/{slug2}/{slug3}\", {\"slug2\" => nil, \"slug3\" => nil})))\n      .generate(\"test\", {\"slug1\" => \"foo\"}).should eq \"/base/category/foo\"\n  end\n\n  def test_generate_nil_for_optional_parameter_is_ignored : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/test/{default}\", {\"default\" => \"0\"})))\n      .generate(\"test\", {\"default\" => nil}).should eq \"/base/test\"\n  end\n\n  def test_generate_query_param_same_as_default : Nil\n    generator = self.generator self.routes ART::Route.new \"/test\", {\"page\" => 1}\n\n    generator.generate(\"test\", page: 2).should eq \"/base/test?page=2\"\n    generator.generate(\"test\", page: 1).should eq \"/base/test\"\n    generator.generate(\"test\", page: \"1\").should eq \"/base/test\"\n    generator.generate(\"test\").should eq \"/base/test\"\n  end\n\n  # TODO: Also support array defaults.\n\n  def test_generate_special_route_name : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/bar\"), name: \"$péß^a|\"))\n      .generate(\"$péß^a|\").should eq \"/base/bar\"\n  end\n\n  def test_generate_url_encoding : Nil\n    expected_path = \"/base/@:%5B%5D/%28%29*%27%22%20+,;-._~%26%24%3C%3E|%7B%7D%25%5C%5E%60!%3Ffoo=bar%23id/@:%5B%5D/%28%29*%27%22%20+,;-._~%26%24%3C%3E|%7B%7D%25%5C%5E%60!%3Ffoo=bar%23id?query=@:%5B%5D/%28%29*%27%22+%2B,;-._~%26%24%3C%3E%7C%7B%7D%25%5C%5E%60!?foo%3Dbar%23id\"\n    chars = \"@:[]/()*'\\\" +,;-._~&$<>|{}%\\\\^`!?foo=bar#id\"\n\n    self\n      .generator(self.routes(ART::Route.new(\"/#{chars}/{path}\", requirements: {\"path\" => /.+/})))\n      .generate(\"test\", {\"path\" => chars, \"query\" => chars}).should eq expected_path\n  end\n\n  def test_generate_encoding_of_relative_path_segments_double_dot : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/dir/../dir/..\")))\n      .generate(\"test\").should eq \"/base/dir/%2E%2E/dir/%2E%2E\"\n  end\n\n  def test_generate_encoding_of_relative_path_segments_single_dot : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/dir/./dir/.\")))\n      .generate(\"test\").should eq \"/base/dir/%2E/dir/%2E\"\n  end\n\n  def test_generate_encoding_of_relative_path_segments_unencoded_dots : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/a./.a/a../..a/...\")))\n      .generate(\"test\").should eq \"/base/a./.a/a../..a/...\"\n  end\n\n  def test_generate_encoding_of_slash_in_path : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/dir/{path}/dir2\", requirements: {\"path\" => /.+/})))\n      .generate(\"test\", path: \"foo/bar%2Fbaz\").should eq \"/base/dir/foo/bar%2Fbaz/dir2\"\n  end\n\n  def test_generate_encoding_of_slash_in_query_params : Nil\n    generator = self.generator(self.routes(ART::Route.new(\"/foo\")))\n\n    generator.generate(\"test\", query: \"foo/bar\").should eq \"/base/foo?query=foo/bar\"\n    generator.generate(\"test\", query: \"foo%2Fbar\").should eq \"/base/foo?query=foo%2Fbar\"\n  end\n\n  def test_generate_adjacent_variables : Nil\n    generator = self.generator(self.routes(ART::Route.new(\"{x}{y}{z}.{_format}\", {\"z\" => \"default-z\", \"_format\" => \"html\"}, {\"y\" => /\\d+/})))\n\n    generator.generate(\"test\", x: \"foo\", y: 123).should eq \"/base/foo123\"\n    generator.generate(\"test\", x: \"foo\", y: 123, z: \"bar\", \"_format\": \"xml\").should eq \"/base/foo123bar.xml\"\n  end\n\n  def test_generate_optional_variable_with_no_real_separator : Nil\n    generator = self.generator(self.routes(ART::Route.new(\"/get{what}\", {\"what\" => \"All\"})))\n\n    generator.generate(\"test\").should eq \"/base/get\"\n    generator.generate(\"test\", what: \"Sites\").should eq \"/base/getSites\"\n  end\n\n  def test_generate_required_variable_with_no_real_separator : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/get{what}Suffix\")))\n      .generate(\"test\", what: \"Sites\").should eq \"/base/getSitesSuffix\"\n  end\n\n  def test_generate_default_requirement_of_variable : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/{page}.{_format}\")))\n      .generate(\"test\", page: \"index\", \"_format\": \"mobile.html\").should eq \"/base/index.mobile.html\"\n  end\n\n  def test_generate_important_variable : Nil\n    generator = self.generator(self.routes(ART::Route.new(\"/{page}.{!_format}\", {\"_format\" => \"mobile.html\"})))\n\n    generator.generate(\"test\", page: \"index\", \"_format\": \"xml\").should eq \"/base/index.xml\"\n    generator.generate(\"test\", page: \"index\", \"_format\": \"mobile.html\").should eq \"/base/index.mobile.html\"\n    generator.generate(\"test\", page: \"index\").should eq \"/base/index.mobile.html\"\n  end\n\n  def test_generate_important_variable_no_default : Nil\n    generator = self.generator self.routes ART::Route.new \"/{page}.{!_format}\"\n\n    expect_raises ART::Exception::MissingRequiredParameters do\n      generator.generate \"test\", page: \"index\"\n    end\n  end\n\n  def test_generate_default_requirement_of_variable_disallows_slash : Nil\n    generator = self.generator self.routes ART::Route.new \"/{page}.{!_format}\"\n\n    expect_raises ART::Exception::InvalidParameter do\n      generator.generate \"test\", page: \"index\", \"_format\": \"sl/ash\"\n    end\n  end\n\n  def test_generate_default_requirement_of_variable_disallows_next_separator : Nil\n    generator = self.generator self.routes ART::Route.new \"/{page}.{!_format}\"\n\n    expect_raises ART::Exception::InvalidParameter do\n      generator.generate \"test\", page: \"do.it\", \"_format\": \"html\"\n    end\n  end\n\n  def test_generate_host_different_than_context : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/{name}\", host: \"{locale}.example.com\")))\n      .generate(\"test\", name: \"George\", locale: \"fr\").should eq \"//fr.example.com/base/George\"\n  end\n\n  def test_generate_host_same_as_context : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/{name}\", host: \"{locale}.example.com\")), context: ART::RequestContext.new(base_url: \"/base\", host: \"fr.example.com\"))\n      .generate(\"test\", name: \"George\", locale: \"fr\").should eq \"/base/George\"\n  end\n\n  def test_generate_host_same_as_context_absolute_url : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/{name}\", host: \"{locale}.example.com\")), context: ART::RequestContext.new(base_url: \"/base\", host: \"fr.example.com\"))\n      .generate(\"test\", {\"name\" => \"George\", \"locale\" => \"fr\"}, reference_type: :absolute_url).should eq \"http://fr.example.com/base/George\"\n  end\n\n  def test_url_invalid_parameter_in_host : Nil\n    generator = self.generator self.routes ART::Route.new \"/\", requirements: {\"foo\" => /bar/}, host: \"{foo}.example.com\"\n\n    expect_raises ART::Exception::InvalidParameter do\n      generator.generate \"test\", foo: \"baz\"\n    end\n  end\n\n  def test_url_invalid_parameter_in_host_with_default : Nil\n    generator = self.generator self.routes ART::Route.new \"/\", {\"foo\" => \"bar\"}, {\"foo\" => /bar/}, host: \"{foo}.example.com\"\n\n    expect_raises ART::Exception::InvalidParameter do\n      generator.generate \"test\", foo: \"baz\"\n    end\n  end\n\n  def test_url_invalid_parameter_in_host_with_default_and_matches_default : Nil\n    generator = self.generator self.routes ART::Route.new \"/\", {\"foo\" => \"baz\"}, {\"foo\" => /bar/}, host: \"{foo}.example.com\"\n\n    expect_raises ART::Exception::InvalidParameter do\n      generator.generate \"test\", foo: \"baz\"\n    end\n  end\n\n  def test_url_invalid_parameter_in_host_non_strict_mode : Nil\n    generator = self.generator self.routes ART::Route.new \"/\", {\"foo\" => \"bar\"}, {\"foo\" => /bar/}, host: \"{foo}.example.com\"\n    generator.strict_requirements = false\n\n    generator.generate(\"test\", foo: \"baz\").should be_empty\n  end\n\n  def test_generate_host_is_case_insensitive : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/\", requirements: {\"locale\" => /en|de|fr/}, host: \"{locale}.example.com\")))\n      .generate(\"test\", locale: \"EN\", reference_type: :network_path).should eq \"//EN.example.com/base/\"\n  end\n\n  def test_generate_default_host_is_used_when_context_host_is_empty : Nil\n    generator = self.generator(self.routes(ART::Route.new(\"/path\", {\"domain\" => \"my.fallback.host\"}, {\"domain\" => /.+/}, host: \"{domain}\")))\n    generator.context.host = \"\"\n\n    generator.generate(\"test\", reference_type: :absolute_url).should eq \"http://my.fallback.host/base/path\"\n  end\n\n  def test_generate_default_host_is_used_when_context_host_is_empty_and_path_reference_type : Nil\n    generator = self.generator(self.routes(ART::Route.new(\"/path\", {\"domain\" => \"my.fallback.host\"}, {\"domain\" => /.+/}, host: \"{domain}\")))\n    generator.context.host = \"\"\n\n    generator.generate(\"test\").should eq \"//my.fallback.host/base/path\"\n  end\n\n  def test_generate_absolute_url_fallback_to_path_if_host_is_empty_and_scheme_is_https : Nil\n    generator = self.generator self.routes ART::Route.new \"/route\"\n    generator.context.host = \"\"\n    generator.context.scheme = \"https\"\n\n    generator.generate(\"test\", reference_type: :absolute_url).should eq \"/base/route\"\n  end\n\n  def test_generate_absolute_url_fallback_to_network_if_scheme_is_empty_and_host_is_not : Nil\n    generator = self.generator self.routes ART::Route.new \"/route\"\n    generator.context.host = \"example.com\"\n    generator.context.scheme = \"\"\n\n    generator.generate(\"test\", reference_type: :absolute_url).should eq \"//example.com/base/route\"\n  end\n\n  def test_generate_absolute_url_fallback_to_path_if_scheme_and_host_are_empty : Nil\n    generator = self.generator self.routes ART::Route.new \"/route\"\n    generator.context.host = \"\"\n    generator.context.scheme = \"\"\n\n    generator.generate(\"test\", reference_type: :absolute_url).should eq \"/base/route\"\n  end\n\n  def test_generate_absolute_url_non_http_scheme_and_empty_host : Nil\n    generator = self.generator self.routes ART::Route.new \"/route\", schemes: \"file\"\n    generator.context.base_url = \"\"\n    generator.context.host = \"\"\n\n    generator.generate(\"test\", reference_type: :absolute_url).should eq \"file:///route\"\n  end\n\n  def test_generate_network_paths : Nil\n    routes = self.routes ART::Route.new \"/{name}\", host: \"{locale}.example.com\", schemes: \"http\"\n\n    self\n      .generator(routes)\n      .generate(\"test\", name: \"George\", locale: \"de\", reference_type: :network_path).should eq \"//de.example.com/base/George\"\n\n    self\n      .generator(routes, context: ART::RequestContext.new base_url: \"/base\", host: \"de.example.com\")\n      .generate(\"test\", name: \"George\", locale: \"de\", query: \"string\", reference_type: :network_path).should eq \"//de.example.com/base/George?query=string\"\n\n    self\n      .generator(routes, context: ART::RequestContext.new base_url: \"/base\", scheme: \"https\")\n      .generate(\"test\", name: \"George\", locale: \"de\", reference_type: :network_path).should eq \"http://de.example.com/base/George\"\n\n    self\n      .generator(routes)\n      .generate(\"test\", name: \"George\", locale: \"de\", reference_type: :absolute_url).should eq \"http://de.example.com/base/George\"\n  end\n\n  def test_generate_relative_path : Nil\n    routes = ART::RouteCollection.new\n    routes.add \"article\", ART::Route.new \"/{author}/{article}/\"\n    routes.add \"comments\", ART::Route.new \"/{author}/{article}/comments\"\n    routes.add \"host\", ART::Route.new \"/{article}\", host: \"{author}.example.com\"\n    routes.add \"scheme\", ART::Route.new \"/{author}/blog\", schemes: \"https\"\n    routes.add \"unrelated\", ART::Route.new \"/about\"\n\n    ART.compile routes\n\n    generator = self.generator routes, context: ART::RequestContext.new base_url: \"/base\", host: \"example.com\", path: \"/George/athena-is-great/\"\n\n    generator.generate(\"comments\", author: \"George\", article: \"athena-is-great\", reference_type: :relative_path).should eq \"comments\"\n    generator.generate(\"comments\", author: \"George\", article: \"athena-is-great\", page: 2, reference_type: :relative_path).should eq \"comments?page=2\"\n    generator.generate(\"article\", author: \"George\", article: \"crystal-is-great\", reference_type: :relative_path).should eq \"../crystal-is-great/\"\n    generator.generate(\"article\", author: \"foo\", article: \"shards-is-great\", reference_type: :relative_path).should eq \"../../foo/shards-is-great/\"\n    generator.generate(\"host\", author: \"George\", article: \"crystal-is-great\", reference_type: :relative_path).should eq \"//George.example.com/base/crystal-is-great\"\n    generator.generate(\"scheme\", author: \"George\", reference_type: :relative_path).should eq \"https://example.com/base/George/blog\"\n    generator.generate(\"unrelated\", reference_type: :relative_path).should eq \"../../about\"\n  end\n\n  # This is primarily just sanity checking the stdlib logic to ensure the correct methods are being used.\n  def test_generate_relative_path_internal : Nil\n    routes = ART::RouteCollection.new\n    routes.add \"one\", ART::Route.new \"/a/b/c/d\"\n    routes.add \"two\", ART::Route.new \"/a/b/c/\"\n    routes.add \"three\", ART::Route.new \"/a/b/\"\n    routes.add \"four\", ART::Route.new \"/a/b/c/other\"\n    routes.add \"five\", ART::Route.new \"/a/x/y\"\n\n    ART.compile routes\n\n    generator = self.generator routes, context: ART::RequestContext.new path: \"/a/b/c/d\"\n\n    generator.generate(\"one\", reference_type: :relative_path).should eq \"\"\n    generator.generate(\"two\", reference_type: :relative_path).should eq \"./\"\n    generator.generate(\"three\", reference_type: :relative_path).should eq \"../\"\n    generator.generate(\"four\", reference_type: :relative_path).should eq \"other\"\n    generator.generate(\"five\", reference_type: :relative_path).should eq \"../../x/y\"\n  end\n\n  def test_generate_with_fragment : Nil\n    generator = self.generator(self.routes(ART::Route.new(\"/\")))\n\n    generator.generate(\"test\", \"_fragment\": \"some text\").should eq \"/base/#some%20text\"\n    generator.generate(\"test\", \"_fragment\": \"0\").should eq \"/base/#0\"\n  end\n\n  def test_generate_with_fragment_does_not_escape_valid_chars : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/\")))\n      .generate(\"test\", \"_fragment\": \"?/\").should eq \"/base/#?/\"\n  end\n\n  def test_generate_with_fragment_via_default : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/\", {\"_fragment\" => \"fragment\"})))\n      .generate(\"test\").should eq \"/base/#fragment\"\n  end\n\n  @[DataProvider(\"look_around_provider\")]\n  def test_generate_look_around_requirements_in_path(expected : String, path : String, requirement : Regex) : Nil\n    self\n      .generator(self.routes(ART::Route.new(path, requirements: {\"foo\" => requirement, \"baz\" => /.+?/})))\n      .generate(\"test\", foo: \"a/b\", baz: \"c/d/e\").should eq expected\n  end\n\n  def look_around_provider : Tuple\n    {\n      {\"/base/a/b/b%28ar/c/d/e\", \"/{foo}/b(ar/{baz}\", /.+(?=\\/b\\(ar\\/)/},\n      {\"/base/a/b/bar/c/d/e\", \"/{foo}/bar/{baz}\", /.+(?!$)/},\n      {\"/base/bar/a/b/bam/c/d/e\", \"/bar/{foo}/bam/{baz}\", /(?<=\\/bar\\/).+/},\n      {\"/base/bar/a/b/bam/c/d/e\", \"/bar/{foo}/bam/{baz}\", /(?<!^).+/},\n    }\n  end\n\n  def test_generate_explicit_query_parameter : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/user/{username}\")))\n      .generate(\n        \"test\",\n        {\n          \"username\" => \"John\",\n          \"a\"        => \"foo\",\n          \"b\"        => \"bar\",\n          \"c\"        => \"baz\",\n          \"_query\"   => {\n            \"a\" => \"123\",\n            \"d\" => \"789\",\n          },\n        }\n      ).should eq \"/base/user/John?a=123&b=bar&c=baz&d=789\"\n  end\n\n  def test_generate_route_host_parameter_and_query_parameter_with_same_name : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/admin/stats\", requirements: {\"domain\" => /.+/}, host: \"{siteCode}.{domain}\")))\n      .generate(\n        \"test\",\n        {\n          \"siteCode\" => \"fr\",\n          \"domain\"   => \"example.com\",\n          \"_query\"   => {\n            \"siteCode\" => \"us\",\n          },\n        },\n        :network_path\n      ).should eq \"//fr.example.com/base/admin/stats?siteCode=us\"\n  end\n\n  def test_generate_route_path_parameter_and_query_parameter_with_same_name : Nil\n    self\n      .generator(self.routes(ART::Route.new(\"/user/{id}\")))\n      .generate(\n        \"test\",\n        {\n          \"id\"     => \"123\",\n          \"_query\" => {\n            \"id\" => \"456\",\n          },\n        }\n      ).should eq \"/base/user/123?id=456\"\n  end\n\n  def test_generate_query_parameter_cannot_substitute_route_parameter : Nil\n    expect_raises ART::Exception::MissingRequiredParameters, \"Cannot generate URL for route 'test'. Missing required parameters: 'id'.\" do\n      self\n        .generator(self.routes ART::Route.new \"/user/{id}\")\n        .generate(\"test\", {\"_query\" => {\"id\" => 456}})\n    end\n  end\n\n  private def generator(routes : ART::RouteCollection, *, context : ART::RequestContext? = nil, default_locale : String? = nil) : ART::Generator::URLGenerator\n    context = context || ART::RequestContext.new \"/base\"\n\n    ART::Generator::URLGenerator.new context, default_locale\n  end\n\n  private def routes(route : ART::Route, *, name : String = \"test\") : ART::RouteCollection\n    routes = ART::RouteCollection.new\n    routes.add name, route\n\n    ART.compile routes\n\n    routes\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/matcher/abstract_url_matcher_test_case.cr",
    "content": "require \"../spec_helper\"\n\nabstract struct AbstractURLMatcherTestCase < ASPEC::TestCase\n  private abstract def get_matcher(routes : ART::RouteCollection, context : ART::RequestContext = ART::RequestContext.new) : ART::Matcher::URLMatcher\n\n  def test_match_no_method : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\"\n    end\n\n    self.get_matcher(routes).match(\"/foo\").should eq({\"_route\" => \"foo\"})\n  end\n\n  def test_match_request : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\"\n    end\n\n    self.get_matcher(routes).match(::HTTP::Request.new \"GET\", \"/foo\").should eq({\"_route\" => \"foo\"})\n  end\n\n  def test_match_method_not_allowed : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\", methods: \"post\"\n    end\n\n    ex = expect_raises ART::Exception::MethodNotAllowed do\n      self.get_matcher(routes).match \"/foo\"\n    end\n\n    ex.allowed_methods.should eq [\"POST\"]\n  end\n\n  def test_nilable_match_method_not_allowed : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\", methods: \"post\"\n    end\n\n    self.get_matcher(routes).match?(\"/foo\").should be_nil\n  end\n\n  def test_nilable_match_request_method_not_allowed : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\", methods: \"post\"\n    end\n\n    self.get_matcher(routes).match?(::HTTP::Request.new(\"GET\", \"/foo\")).should be_nil\n  end\n\n  def test_match_method_not_allowed_root : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/\", methods: \"get\"\n    end\n\n    ex = expect_raises ART::Exception::MethodNotAllowed do\n      self.get_matcher(routes, ART::RequestContext.new method: \"POST\").match \"/\"\n    end\n\n    ex.allowed_methods.should eq [\"GET\"]\n  end\n\n  def test_match_head_allowed_when_requirements_includes_get : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\", methods: \"get\"\n    end\n\n    self.get_matcher(routes, ART::RequestContext.new method: \"HEAD\").match(\"/foo\").should eq({\"_route\" => \"foo\"})\n  end\n\n  def test_match_method_not_allowed_aggregates_allowed_methods : Nil\n    routes = self.build_collection do\n      add \"foo1\", ART::Route.new \"/foo\", methods: \"post\"\n      add \"foo2\", ART::Route.new \"/foo\", methods: {\"PUT\", \"DELETE\"}\n    end\n\n    ex = expect_raises ART::Exception::MethodNotAllowed do\n      self.get_matcher(routes).match \"/foo\"\n    end\n\n    ex.allowed_methods.should eq [\"POST\", \"PUT\", \"DELETE\"]\n  end\n\n  def test_nilable_match_returns_matched_pattern : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{bar}\"\n    end\n\n    self.get_matcher(routes).match?(\"/no-match\").should be_nil\n  end\n\n  def test_nilable_match_request_returns_matched_pattern : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{bar}\"\n    end\n\n    self.get_matcher(routes).match?(::HTTP::Request.new \"GET\", \"/no-match\").should be_nil\n  end\n\n  def test_match_returns_matched_pattern : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{bar}\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/no-match\"\n    end\n\n    self.get_matcher(routes).match(\"/foo/baz\").should eq({\"_route\" => \"foo\", \"bar\" => \"baz\"})\n  end\n\n  def test_match_defaults_are_merged : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{bar}\", {\"def\" => \"test\"}\n    end\n\n    self.get_matcher(routes).match(\"/foo/baz\").should eq({\"_route\" => \"foo\", \"bar\" => \"baz\", \"def\" => \"test\"})\n  end\n\n  def test_match_returned_results_do_not_mutate_the_original_static_route : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\", {\"def\" => \"test\"}\n    end\n\n    matcher = self.get_matcher routes\n\n    parameters = matcher.match(\"/foo\")\n    parameters.should eq({\"_route\" => \"foo\", \"def\" => \"test\"})\n\n    parameters.delete \"_route\"\n\n    matcher.match(\"/foo\").should eq({\"_route\" => \"foo\", \"def\" => \"test\"})\n  end\n\n  def test_match_returned_results_do_not_mutate_the_original_dynamic_route : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{id}\"\n    end\n\n    matcher = self.get_matcher routes\n\n    parameters = matcher.match(\"/foo/10\")\n    parameters.should eq({\"_route\" => \"foo\", \"id\" => \"10\"})\n\n    parameters.delete \"_route\"\n\n    matcher.match(\"/foo/10\").should eq({\"_route\" => \"foo\", \"id\" => \"10\"})\n  end\n\n  def test_match_method_is_ignored_if_none_are_provided : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\", methods: {\"GET\", \"HEAD\"}\n    end\n\n    self.get_matcher(routes).match(\"/foo\").should eq({\"_route\" => \"foo\"})\n\n    expect_raises ART::Exception::MethodNotAllowed do\n      self.get_matcher(routes, ART::RequestContext.new method: \"POST\").match \"/foo\"\n    end\n\n    self.get_matcher(routes).match(\"/foo\").should eq({\"_route\" => \"foo\"})\n    self.get_matcher(routes, ART::RequestContext.new method: \"HEAD\").match(\"/foo\").should eq({\"_route\" => \"foo\"})\n  end\n\n  def test_match_optional_variable_as_first_segment : Nil\n    routes = self.build_collection do\n      add \"bar\", ART::Route.new \"/{bar}/foo\", {\"bar\" => \"bar\"}, {\"bar\" => /foo|bar/}\n    end\n\n    matcher = self.get_matcher routes\n    matcher.match(\"/bar/foo\").should eq({\"_route\" => \"bar\", \"bar\" => \"bar\"})\n    matcher.match(\"/foo/foo\").should eq({\"_route\" => \"bar\", \"bar\" => \"foo\"})\n\n    routes = self.build_collection do\n      add \"bar\", ART::Route.new \"/{bar}\", {\"bar\" => \"bar\"}, {\"bar\" => /foo|bar/}\n    end\n\n    ART::RouteProvider.reset\n\n    matcher = self.get_matcher routes\n    matcher.match(\"/foo\").should eq({\"_route\" => \"bar\", \"bar\" => \"foo\"})\n    matcher.match(\"/\").should eq({\"_route\" => \"bar\", \"bar\" => \"bar\"})\n  end\n\n  def test_match_only_optional_variable : Nil\n    routes = self.build_collection do\n      add \"bar\", ART::Route.new \"/{foo}/{bar}\", {\"bar\" => \"bar\", \"foo\" => \"foo\"}\n    end\n\n    matcher = self.get_matcher routes\n    matcher.match(\"/\").should eq({\"_route\" => \"bar\", \"bar\" => \"bar\", \"foo\" => \"foo\"})\n    matcher.match(\"/a\").should eq({\"_route\" => \"bar\", \"bar\" => \"bar\", \"foo\" => \"a\"})\n    matcher.match(\"/a/b\").should eq({\"_route\" => \"bar\", \"bar\" => \"b\", \"foo\" => \"a\"})\n  end\n\n  def test_match_with_prefix : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/{foo}\"\n      add_prefix \"/b\"\n      add_prefix \"/a\"\n    end\n\n    self.get_matcher(routes).match(\"/a/b/foo\").should eq({\"_route\" => \"foo\", \"foo\" => \"foo\"})\n  end\n\n  def test_match_with_dynamic_prefix : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/{foo}\"\n      add_prefix \"/b\"\n      add_prefix \"/{_locale}\"\n    end\n\n    self.get_matcher(routes).match(\"/de/b/foo\").should eq({\"_route\" => \"foo\", \"_locale\" => \"de\", \"foo\" => \"foo\"})\n  end\n\n  def test_match_special_route_name : Nil\n    routes = self.build_collection do\n      add \"$péß^a|\", ART::Route.new \"/bar\"\n    end\n\n    self.get_matcher(routes).match(\"/bar\").should eq({\"_route\" => \"$péß^a|\"})\n  end\n\n  def test_match_important_variables : Nil\n    routes = self.build_collection do\n      add \"index\", ART::Route.new \"/index.{!_format}\", {\"_format\" => \"xml\"}\n    end\n\n    self.get_matcher(routes).match(\"/index.xml\").should eq({\"_route\" => \"index\", \"_format\" => \"xml\"})\n  end\n\n  def test_match_short_path_does_not_match_important_variable : Nil\n    routes = self.build_collection do\n      add \"index\", ART::Route.new \"/index.{!_format}\", {\"_format\" => \"xml\"}\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/index\"\n    end\n  end\n\n  def test_match_short_path_matches_non_important_variable : Nil\n    routes = self.build_collection do\n      add \"index\", ART::Route.new \"/index.{_format}\", {\"_format\" => \"xml\"}\n    end\n\n    self.get_matcher(routes).match(\"/index.xml\").should eq({\"_route\" => \"index\", \"_format\" => \"xml\"})\n  end\n\n  def test_match_trailing_encoded_new_line_is_not_overlooked : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/foo%0a\"\n    end\n  end\n\n  def test_match_non_alphanum : Nil\n    chars = \"!\\\"$%éà &'()*+,./:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\\\[]^_`abcdefghijklmnopqrstuvwxyz{|}~-\"\n\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/{foo}/bar\", requirements: {\"foo\" => /#{Regex.escape chars}/}\n    end\n\n    matcher = self.get_matcher routes\n    matcher.match(\"/#{URI.encode_path_segment chars}/bar\").should eq({\"_route\" => \"foo\", \"foo\" => chars})\n    matcher.match(%(/#{chars.tr \"%\", \"%25\"}/bar)).should eq({\"_route\" => \"foo\", \"foo\" => chars})\n  end\n\n  def test_match_with_dot_in_requirements : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/{foo}/bar\", requirements: {\"foo\" => /.+/}\n    end\n\n    self.get_matcher(routes).match(\"/#{URI.encode_path_segment \"\\n\"}/bar\").should eq({\"_route\" => \"foo\", \"foo\" => \"\\n\"})\n  end\n\n  def test_match_regression : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{foo}\"\n      add \"bar\", ART::Route.new \"/foo/bar/{foo}\"\n    end\n\n    self.get_matcher(routes).match(\"/foo/bar/bar\").should eq({\"_route\" => \"bar\", \"foo\" => \"bar\"})\n\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/{bar}\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/\"\n    end\n  end\n\n  def test_match_multiple_params : Nil\n    routes = self.build_collection do\n      add \"foo1\", ART::Route.new \"/foo/{a}/{b}\"\n      add \"foo2\", ART::Route.new \"/foo/{a}/test/test/{b}\"\n      add \"foo3\", ART::Route.new \"/foo/{a}/{b}/{c}/{d}\"\n    end\n\n    self.get_matcher(routes).match(\"/foo/test/test/test/bar\").should eq({\"_route\" => \"foo2\", \"a\" => \"test\", \"b\" => \"bar\"})\n  end\n\n  def test_match_default_requirements_for_optional_variables : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/{page}.{_format}\", {\"page\" => \"index\", \"_format\" => \"html\"}\n    end\n\n    self.get_matcher(routes).match(\"/my-page.xml\").should eq({\"_route\" => \"test\", \"page\" => \"my-page\", \"_format\" => \"xml\"})\n  end\n\n  def test_match_match_overridden_route : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\"\n    end\n\n    routes2 = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo1\"\n    end\n\n    routes.add routes2\n\n    self.get_matcher(routes).match(\"/foo1\").should eq({\"_route\" => \"foo\"})\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/foo\"\n    end\n  end\n\n  def test_match_matching_is_eager : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/{foo}-{bar}-\", requirements: {\"foo\" => /.+/, \"bar\" => \".+\"}\n    end\n\n    self.get_matcher(routes).match(\"/text1-text2-text3-text4-\").should eq({\"_route\" => \"test\", \"foo\" => \"text1-text2-text3\", \"bar\" => \"text4\"})\n  end\n\n  def test_match_adjacent_variables : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/{w}{x}{y}{z}.{_format}\", {\"z\" => \"default-z\", \"_format\" => \"html\"}, {\"y\" => /y|Y/}\n    end\n\n    matcher = self.get_matcher routes\n\n    matcher.match(\"/wwwwwxYZ.xml\").should eq({\"_route\" => \"test\", \"_format\" => \"xml\", \"w\" => \"wwwww\", \"x\" => \"x\", \"y\" => \"Y\", \"z\" => \"Z\"})\n    matcher.match(\"/wwwwwxyZZZ\").should eq({\"_route\" => \"test\", \"_format\" => \"html\", \"w\" => \"wwwww\", \"x\" => \"x\", \"y\" => \"y\", \"z\" => \"ZZZ\"})\n    matcher.match(\"/wwwwwxy\").should eq({\"_route\" => \"test\", \"_format\" => \"html\", \"w\" => \"wwwww\", \"x\" => \"x\", \"y\" => \"y\", \"z\" => \"default-z\"})\n\n    expect_raises ART::Exception::ResourceNotFound do\n      matcher.match \"/wxy.html\"\n    end\n  end\n\n  def test_match_optional_variable_with_no_real_separator : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/get{what}\", {\"what\" => \"All\"}\n    end\n\n    matcher = self.get_matcher routes\n\n    matcher.match(\"/get\").should eq({\"_route\" => \"test\", \"what\" => \"All\"})\n    matcher.match(\"/getSites\").should eq({\"_route\" => \"test\", \"what\" => \"Sites\"})\n  end\n\n  def test_match_required_variable_with_no_real_separator : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/get{what}Suffix\"\n    end\n\n    self.get_matcher(routes).match(\"/getSitesSuffix\").should eq({\"_route\" => \"test\", \"what\" => \"Sites\"})\n  end\n\n  def test_match_default_requirement_of_variable : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/{page}.{_format}\"\n    end\n\n    self.get_matcher(routes).match(\"/index.mobile.html\").should eq({\"_route\" => \"test\", \"page\" => \"index\", \"_format\" => \"mobile.html\"})\n  end\n\n  def test_match_default_requirement_of_variable_disallows_slash : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/{page}.{_format}\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/index.sl/ash\"\n    end\n  end\n\n  def test_match_default_requirement_of_variable_disallows_next_separator : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/{page}.{_format}\", requirements: {\"_format\" => /html|xml/}\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/do.t.html\"\n    end\n  end\n\n  def test_match_missing_trailing_slash : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo/\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/foo\"\n    end\n  end\n\n  def test_match_extra_trailing_slash : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/foo/\"\n    end\n  end\n\n  def test_match_missing_trailing_slash_non_safe_method : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo/\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes, ART::RequestContext.new method: \"POST\").match \"/foo\"\n    end\n  end\n\n  def test_match_extra_trailing_slash_non_safe_method : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes, ART::RequestContext.new method: \"POST\").match \"/foo/\"\n    end\n  end\n\n  def test_match_scheme_requirement : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo\", schemes: \"https\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/foo\"\n    end\n  end\n\n  def test_match_scheme_requirement_non_safe_method : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo\", schemes: \"https\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes, ART::RequestContext.new method: \"POST\").match \"/foo\"\n    end\n  end\n\n  def test_match_same_path_with_different_scheme : Nil\n    routes = self.build_collection do\n      add \"https_route\", ART::Route.new \"/\", schemes: \"https\"\n      add \"http_route\", ART::Route.new \"/\", schemes: \"http\"\n    end\n\n    self.get_matcher(routes).match(\"/\").should eq({\"_route\" => \"http_route\"})\n  end\n\n  def test_match_condition : Nil\n    routes = self.build_collection do\n      route = ART::Route.new \"/foo\"\n      route.condition do |ctx|\n        \"POST\" == ctx.method\n      end\n\n      add \"foo\", route\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/foo\"\n    end\n  end\n\n  def test_match_request_condition : Nil\n    routes = self.build_collection do\n      route = ART::Route.new \"/foo/{bar}\"\n      route.condition do |_, request|\n        request.path.starts_with? \"/foo\"\n      end\n\n      add \"foo\", route\n\n      route = ART::Route.new \"/foo/{bar}\"\n      route.condition do |_, request|\n        \"/foo/foo\" == request.path\n      end\n\n      add \"bar\", route\n    end\n\n    self.get_matcher(routes).match(\"/foo/bar\").should eq({\"_route\" => \"foo\", \"bar\" => \"bar\"})\n  end\n\n  def test_match_decode_once : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{bar}\"\n    end\n\n    self.get_matcher(routes).match(\"/foo/bar%2523\").should eq({\"_route\" => \"foo\", \"bar\" => \"bar%23\"})\n  end\n\n  def test_match_cannot_rely_on_prefix : Nil\n    routes = self.build_collection do\n      sub_routes = self.build_collection do\n        add \"bar\", ART::Route.new \"/bar\"\n        add_prefix \"/prefix\"\n\n        itself[\"bar\"].path = \"/new\"\n      end\n\n      add sub_routes\n    end\n\n    self.get_matcher(routes).match(\"/new\").should eq({\"_route\" => \"bar\"})\n  end\n\n  def test_match_with_host : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{foo}\", host: \"{locale}.example.com\"\n    end\n\n    self.get_matcher(routes, ART::RequestContext.new host: \"de.example.com\").match(\"/foo/bar\").should eq({\"_route\" => \"foo\", \"foo\" => \"bar\", \"locale\" => \"de\"})\n  end\n\n  def test_match_with_host_on_collection : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{foo}\"\n      add \"bar\", ART::Route.new \"/bar/{foo}\", host: \"{locale}.example.com\"\n      set_host \"{locale}.example.com\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"en.example.com\"\n    matcher.match(\"/foo/bar\").should eq({\"_route\" => \"foo\", \"foo\" => \"bar\", \"locale\" => \"en\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"en.example.com\"\n    matcher.match(\"/bar/bar\").should eq({\"_route\" => \"bar\", \"foo\" => \"bar\", \"locale\" => \"en\"})\n  end\n\n  def test_match_variation_in_trailing_slash_with_host : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/\", host: \"foo.example.com\"\n      add \"bar\", ART::Route.new \"/foo\", host: \"bar.example.com\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"foo.example.com\"\n    matcher.match(\"/foo/\").should eq({\"_route\" => \"foo\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"bar.example.com\"\n    matcher.match(\"/foo\").should eq({\"_route\" => \"bar\"})\n  end\n\n  def test_match_variation_in_trailing_slash_with_host_reversed : Nil\n    routes = self.build_collection do\n      add \"bar\", ART::Route.new \"/foo\", host: \"bar.example.com\"\n      add \"foo\", ART::Route.new \"/foo/\", host: \"foo.example.com\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"foo.example.com\"\n    matcher.match(\"/foo/\").should eq({\"_route\" => \"foo\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"bar.example.com\"\n    matcher.match(\"/foo\").should eq({\"_route\" => \"bar\"})\n  end\n\n  def test_match_variation_in_trailing_slash_with_host_and_variable : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/{foo}/\", host: \"foo.example.com\"\n      add \"bar\", ART::Route.new \"/{foo}\", host: \"bar.example.com\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"foo.example.com\"\n    matcher.match(\"/bar/\").should eq({\"_route\" => \"foo\", \"foo\" => \"bar\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"bar.example.com\"\n    matcher.match(\"/bar\").should eq({\"_route\" => \"bar\", \"foo\" => \"bar\"})\n  end\n\n  def test_match_variation_in_trailing_slash_with_host_and_variable_reversed : Nil\n    routes = self.build_collection do\n      add \"bar\", ART::Route.new \"/{foo}\", host: \"bar.example.com\"\n      add \"foo\", ART::Route.new \"/{foo}/\", host: \"foo.example.com\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"foo.example.com\"\n    matcher.match(\"/bar/\").should eq({\"_route\" => \"foo\", \"foo\" => \"bar\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"bar.example.com\"\n    matcher.match(\"/bar\").should eq({\"_route\" => \"bar\", \"foo\" => \"bar\"})\n  end\n\n  def test_match_variation_in_trailing_slash_with_host_and_method : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/\", methods: \"POST\"\n      add \"bar\", ART::Route.new \"/foo\", methods: \"GET\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"POST\"\n    matcher.match(\"/foo/\").should eq({\"_route\" => \"foo\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"GET\"\n    matcher.match(\"/foo\").should eq({\"_route\" => \"bar\"})\n  end\n\n  def test_match_variation_in_trailing_slash_with_host_and_method_reversed : Nil\n    routes = self.build_collection do\n      add \"bar\", ART::Route.new \"/foo\", methods: \"GET\"\n      add \"foo\", ART::Route.new \"/foo/\", methods: \"POST\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"POST\"\n    matcher.match(\"/foo/\").should eq({\"_route\" => \"foo\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"GET\"\n    matcher.match(\"/foo\").should eq({\"_route\" => \"bar\"})\n  end\n\n  def test_match_variable_variation_in_trailing_slash_with_method : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/{foo}/\", methods: \"POST\"\n      add \"bar\", ART::Route.new \"/{foo}\", methods: \"GET\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"POST\"\n    matcher.match(\"/bar/\").should eq({\"_route\" => \"foo\", \"foo\" => \"bar\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"GET\"\n    # pp ART::RouteProvider\n    matcher.match(\"/bar\").should eq({\"_route\" => \"bar\", \"foo\" => \"bar\"})\n  end\n\n  def test_match_variable_variation_in_trailing_slash_with_method_reversed : Nil\n    routes = self.build_collection do\n      add \"bar\", ART::Route.new \"/{foo}\", methods: \"GET\"\n      add \"foo\", ART::Route.new \"/{foo}/\", methods: \"POST\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"POST\"\n    matcher.match(\"/bar/\").should eq({\"_route\" => \"foo\", \"foo\" => \"bar\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"GET\"\n    matcher.match(\"/bar\").should eq({\"_route\" => \"bar\", \"foo\" => \"bar\"})\n  end\n\n  def test_match_mix_of_static_and_variable_variation_in_trailing_slash_with_hosts : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/\", host: \"foo.example.com\"\n      add \"bar\", ART::Route.new \"/{foo}\", host: \"bar.example.com\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"foo.example.com\"\n    matcher.match(\"/foo/\").should eq({\"_route\" => \"foo\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new host: \"bar.example.com\"\n    matcher.match(\"/bar\").should eq({\"_route\" => \"bar\", \"foo\" => \"bar\"})\n  end\n\n  def test_match_mix_of_static_and_variable_variation_in_trailing_slash_with_methods : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/\", methods: \"POST\"\n      add \"bar\", ART::Route.new \"/{foo}\", methods: \"GET\"\n    end\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"POST\"\n    matcher.match(\"/foo/\").should eq({\"_route\" => \"foo\"})\n\n    matcher = self.get_matcher routes, ART::RequestContext.new method: \"GET\"\n    matcher.match(\"/foo\").should eq({\"_route\" => \"bar\", \"foo\" => \"foo\"})\n    matcher.match(\"/bar\").should eq({\"_route\" => \"bar\", \"foo\" => \"bar\"})\n  end\n\n  def test_match_with_host_does_not_match\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/{foo}\", host: \"{locale}.example.com\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes, ART::RequestContext.new host: \"example.com\").match \"/foo/bar\"\n    end\n  end\n\n  def test_match_path_is_case_sensitive\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/{locale}\", requirements: {\"locale\" => /EN|FR|DE/}\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/en\"\n    end\n  end\n\n  def test_match_host_is_case_insensitive\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/\", requirements: {\"locale\" => /EN|FR|DE/}, host: \"{locale}.example.com\"\n    end\n\n    self.get_matcher(routes, ART::RequestContext.new host: \"en.example.com\").match(\"/\").should eq({\"_route\" => \"foo\", \"locale\" => \"en\"})\n  end\n\n  def test_match_no_configuration : Nil\n    expect_raises ART::Exception::NoConfiguration do\n      self.get_matcher(ART::RouteCollection.new).match \"/\"\n    end\n  end\n\n  def test_match_nested_collection : Nil\n    routes = self.build_collection do\n      sub_collection = self.build_collection do\n        add \"a\", ART::Route.new \"/a\"\n        add \"b\", ART::Route.new \"/b\"\n        add \"c\", ART::Route.new \"/c\"\n        add_prefix \"/p\"\n      end\n\n      add sub_collection\n\n      add \"baz\", ART::Route.new \"/{baz}\"\n\n      sub_collection = self.build_collection do\n        add \"buz\", ART::Route.new \"/buz\"\n        add_prefix \"/prefix\"\n      end\n\n      add sub_collection\n    end\n\n    matcher = self.get_matcher routes\n    matcher.match(\"/p/a\").should eq({\"_route\" => \"a\"})\n    matcher.match(\"/p\").should eq({\"_route\" => \"baz\", \"baz\" => \"p\"})\n    matcher.match(\"/prefix/buz\").should eq({\"_route\" => \"buz\"})\n  end\n\n  def test_match_scheme_and_method_mismatch : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/\", schemes: \"https\", methods: \"POST\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/\"\n    end\n  end\n\n  def test_sibling_routes : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/a{a}\", methods: \"POST\"\n      add \"b\", ART::Route.new \"/a{a}\", methods: \"PUT\"\n      add \"c\", ART::Route.new \"/a{a}\"\n\n      add \"d\", ART::Route.new(\"/b{a}\").condition { false }\n      add \"e\", ART::Route.new(\"/{b}{a}\").condition { false }\n\n      add \"f\", ART::Route.new \"/{b}{a}\", requirements: {\"b\" => /b/}\n    end\n\n    matcher = self.get_matcher routes\n    matcher.match(\"/aa\").should eq({\"_route\" => \"c\", \"a\" => \"a\"})\n    matcher.match(\"/be\").should eq({\"_route\" => \"f\", \"a\" => \"e\", \"b\" => \"b\"})\n  end\n\n  def test_match_requirements_with_capture_groups : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/{a}/{b}\", requirements: {\"a\" => /(a|b)/}\n    end\n\n    self.get_matcher(routes).match(\"/a/b\").should eq({\"_route\" => \"a\", \"a\" => \"a\", \"b\" => \"b\"})\n  end\n\n  def test_match_dot_all_with_catch_all : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/{id}.html\", requirements: {\"id\" => /.+/}\n      add \"b\", ART::Route.new \"/{all}\", requirements: {\"all\" => /.+/}\n    end\n\n    self.get_matcher(routes).match(\"/foo/bar.html\").should eq({\"_route\" => \"a\", \"id\" => \"foo/bar\"})\n  end\n\n  def test_match_host_pattern : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/{app}/{action}/{unused}\", host: \"{host}\"\n    end\n\n    self\n      .get_matcher(routes, ART::RequestContext.new host: \"foo\")\n      .match(\"/app/action/unused\").should eq({\"_route\" => \"a\", \"app\" => \"app\", \"action\" => \"action\", \"unused\" => \"unused\", \"host\" => \"foo\"})\n  end\n\n  def test_match_host_with_dot : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/foo\", host: \"foo.example.com\"\n      add \"b\", ART::Route.new \"/bar/{baz}\"\n    end\n\n    self.get_matcher(routes).match(\"/bar/abc.123\").should eq({\"_route\" => \"b\", \"baz\" => \"abc.123\"})\n  end\n\n  def test_match_slash_variant : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/foo/{bar}\", requirements: {\"bar\" => /.*/}\n    end\n\n    self.get_matcher(routes).match(\"/foo/\").should eq({\"_route\" => \"a\", \"bar\" => \"\"})\n    self.get_matcher(routes).match(\"/foo/bar/\").should eq({\"_route\" => \"a\", \"bar\" => \"bar/\"})\n  end\n\n  def test_match_slash_with_verb : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/{foo}\", methods: {\"put\", \"delete\"}\n      add \"b\", ART::Route.new \"/bar/\"\n    end\n\n    self.get_matcher(routes).match(\"/bar/\").should eq({\"_route\" => \"b\"})\n  end\n\n  def test_match_slash_with_verb_match_all : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/dav/{foo}\", requirements: {\"foo\" => /.*/}, methods: {\"get\", \"options\"}\n    end\n\n    self\n      .get_matcher(routes, ART::RequestContext.new method: \"OPTIONS\")\n      .match(\"/dav/files/bar/\").should eq({\"_route\" => \"a\", \"foo\" => \"files/bar/\"})\n  end\n\n  def test_match_slash_and_verb_precedence : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/api/customers/{customerId}/contactpersons/\", methods: \"POST\"\n      add \"b\", ART::Route.new \"/api/customers/{customerId}/contactpersons\", methods: \"GET\"\n    end\n\n    self.get_matcher(routes).match(\"/api/customers/123/contactpersons\").should eq({\"_route\" => \"b\", \"customerId\" => \"123\"})\n  end\n\n  def test_match_slash_and_verb_precedence_reversed : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/api/customers/{customerId}/contactpersons/\", methods: \"GET\"\n      add \"b\", ART::Route.new \"/api/customers/{customerId}/contactpersons\", methods: \"POST\"\n    end\n\n    self.get_matcher(routes, ART::RequestContext.new method: \"POST\").match(\"/api/customers/123/contactpersons\").should eq({\"_route\" => \"b\", \"customerId\" => \"123\"})\n  end\n\n  def test_match_greedy_trailing_requirement : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/{a}\", requirements: {\"a\" => /.+/}\n    end\n\n    self.get_matcher(routes).match(\"/foo\").should eq({\"_route\" => \"a\", \"a\" => \"foo\"})\n    self.get_matcher(routes).match(\"/foo/\").should eq({\"_route\" => \"a\", \"a\" => \"foo/\"})\n  end\n\n  def test_match_greedy_trailing_requirement_with_default : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/fr-fr/{a}\", {\"a\" => \"aaa\"}, {\"a\" => /.+/}\n      add \"b\", ART::Route.new \"/en-en/{b}\", {\"b\" => \"bbb\"}, {\"b\" => /.+/}\n    end\n\n    self.get_matcher(routes).match(\"/fr-fr\").should eq({\"_route\" => \"a\", \"a\" => \"aaa\"})\n    self.get_matcher(routes).match(\"/fr-fr/AAA\").should eq({\"_route\" => \"a\", \"a\" => \"AAA\"})\n\n    self.get_matcher(routes).match(\"/en-en\").should eq({\"_route\" => \"b\", \"b\" => \"bbb\"})\n    self.get_matcher(routes).match(\"/en-en/BBB\").should eq({\"_route\" => \"b\", \"b\" => \"BBB\"})\n  end\n\n  def test_match_greedy_trailing_requirement_default1 : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/fr-fr/{a}\", {\"a\" => \"aaa\"}, {\"a\" => /.+/}\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes).match \"/fr-fr/\"\n    end\n  end\n\n  def test_match_greedy_trailing_requirement_default2 : Nil\n    routes = self.build_collection do\n      add \"b\", ART::Route.new \"/en-en/{b}\", {\"b\" => \"bbb\"}, {\"b\" => /.*/}\n    end\n\n    self.get_matcher(routes).match(\"/en-en/\").should eq({\"_route\" => \"b\", \"b\" => \"\"})\n  end\n\n  def test_match_restrictive_trailing_requirement_with_static_route_after : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/hello{_}\", requirements: {\"_\" => /\\/(?!\\/)/}\n      add \"b\", ART::Route.new \"/hello\"\n    end\n\n    self.get_matcher(routes).match(\"/hello/\").should eq({\"_route\" => \"a\", \"_\" => \"/\"})\n  end\n\n  private def build_collection(&) : ART::RouteCollection\n    routes = ART::RouteCollection.new\n\n    with routes yield\n\n    routes\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/matcher/redirectable_url_matcher_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./abstract_url_matcher_test_case\"\n\nprivate class MockRedirectableURLMatcher < ART::Matcher::URLMatcher\n  include ART::Matcher::RedirectableURLMatcherInterface\n\n  class_setter return_value : ART::Parameters = ART::Parameters.new\n  class_setter was_called : Bool = true\n  class_setter expected_path : String? = nil\n  class_setter expected_route : String? = nil\n  class_setter expected_scheme : String? = nil\n\n  def redirect(path : String, route : String, scheme : String? = nil) : ART::Parameters?\n    @@was_called.should be_true\n\n    if ep = @@expected_path\n      path.should eq ep\n    end\n\n    if er = @@expected_route\n      route.should eq er\n    end\n\n    if es = @@expected_scheme\n      scheme.should eq es\n    end\n\n    @@return_value.dup\n  ensure\n    @@return_value = ART::Parameters.new\n    @@was_called = true\n    @@expected_path = nil\n    @@expected_route = nil\n    @@expected_scheme = nil\n  end\nend\n\nstruct RedirectableURLMatcherTest < AbstractURLMatcherTestCase\n  def test_match_missing_trailing_slash : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo/\"\n    end\n\n    self.get_matcher(routes).match(\"/foo\").to_h.should eq({\"_route\" => \"test\"})\n  end\n\n  def test_match_extra_trailing_slash : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo\"\n    end\n\n    self.get_matcher(routes).match(\"/foo/\").to_h.should eq({\"_route\" => \"test\"})\n  end\n\n  def test_redirect_when_no_slash_for_non_safe_method : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo/\"\n    end\n\n    expect_raises ART::Exception::ResourceNotFound do\n      self.get_matcher(routes, ART::RequestContext.new method: \"POST\").match \"/foo\"\n    end\n  end\n\n  # TODO: Uncomment scheme check when supported\n  def test_scheme_redirect_redirects_to_first_scheme : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo\", schemes: {\"FTP\", \"HTTPS\"}\n    end\n\n    MockRedirectableURLMatcher.expected_path = \"/foo\"\n    MockRedirectableURLMatcher.expected_route = \"test\"\n    # MockRedirectableURLMatcher.expected_scheme = \"ftp\"\n\n    self.get_matcher(routes).match(\"/foo\").to_h.should eq({\"_route\" => \"test\"})\n  end\n\n  # TODO: Enable when schemes are supported\n  def ptest_no_schema_redirect_if_one_of_multiple_schemas_matches : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo\", schemes: {\"https\", \"http\"}\n    end\n\n    MockRedirectableURLMatcher.was_called = false\n\n    self.get_matcher(routes).match(\"/foo\").to_h.should eq({\"_route\" => \"test\"})\n  end\n\n  # TODO: Enable when schemes are supported\n  def ptest_scheme_redirect_with_params : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo/{bar}\", schemes: \"https\"\n    end\n\n    rv = ART::Parameters.new\n    rv[\"redirect\"] = \"value\"\n    MockRedirectableURLMatcher.return_value = rv\n    MockRedirectableURLMatcher.expected_path = \"/foo/baz\"\n    MockRedirectableURLMatcher.expected_route = \"test\"\n    # MockRedirectableURLMatcher.expected_scheme = \"https\"\n\n    self.get_matcher(routes).match(\"/foo/baz\").to_h.should eq({\"_route\" => \"test\", \"bar\" => \"baz\", \"redirect\" => \"value\"})\n  end\n\n  def test_scheme_redirect_for_root : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/\", schemes: \"https\"\n    end\n\n    rv = ART::Parameters.new\n    rv[\"redirect\"] = \"value\"\n    MockRedirectableURLMatcher.return_value = rv\n    MockRedirectableURLMatcher.expected_path = \"/\"\n    MockRedirectableURLMatcher.expected_route = \"test\"\n    # MockRedirectableURLMatcher.expected_scheme = \"https\"\n\n    self.get_matcher(routes).match(\"/\").to_h.should eq({\"_route\" => \"test\", \"redirect\" => \"value\"})\n  end\n\n  def test_slash_redirect_with_params : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo/{bar}/\"\n    end\n\n    rv = ART::Parameters.new\n    rv[\"redirect\"] = \"value\"\n    MockRedirectableURLMatcher.return_value = rv\n    MockRedirectableURLMatcher.expected_path = \"/foo/baz/\"\n    MockRedirectableURLMatcher.expected_route = \"test\"\n\n    self.get_matcher(routes).match(\"/foo/baz\").to_h.should eq({\"_route\" => \"test\", \"bar\" => \"baz\", \"redirect\" => \"value\"})\n  end\n\n  def test_redirect_preserves_url_encoding : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo:bar/\"\n    end\n\n    MockRedirectableURLMatcher.expected_path = \"/foo%3Abar/\"\n\n    self.get_matcher(routes).match(\"/foo%3Abar\").to_h.should eq({\"_route\" => \"test\"})\n  end\n\n  def test_match_scheme_requirement : Nil\n    routes = self.build_collection do\n      add \"test\", ART::Route.new \"/foo\", schemes: \"https\"\n    end\n\n    self.get_matcher(routes).match(\"/foo\").to_h.should eq({\"_route\" => \"test\"})\n  end\n\n  def test_fallback_page1 : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/\"\n      add \"bar\", ART::Route.new \"/{name}\"\n    end\n\n    MockRedirectableURLMatcher.expected_path = \"/foo/\"\n    MockRedirectableURLMatcher.expected_route = \"foo\"\n\n    self.get_matcher(routes).match(\"/foo\").to_h.should eq({\"_route\" => \"foo\"})\n  end\n\n  def test_fallback_page2 : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\"\n      add \"bar\", ART::Route.new \"/{name}/\"\n    end\n\n    MockRedirectableURLMatcher.expected_path = \"/foo\"\n    MockRedirectableURLMatcher.expected_route = \"foo\"\n\n    self.get_matcher(routes).match(\"/foo/\").to_h.should eq({\"_route\" => \"foo\"})\n  end\n\n  def test_missing_trailing_slash_and_scheme : Nil\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo/\", schemes: \"https\"\n    end\n\n    MockRedirectableURLMatcher.expected_path = \"/foo/\"\n    MockRedirectableURLMatcher.expected_route = \"foo\"\n    # MockRedirectableURLMatcher.expected_scheme = \"https\"\n\n    self.get_matcher(routes).match(\"/foo\").to_h.should eq({\"_route\" => \"foo\"})\n  end\n\n  def test_slash_and_verb_precedence_with_redirection : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/api/customers/{customerId}/contactpersons\", methods: \"POST\"\n      add \"b\", ART::Route.new \"/api/customers/{customerId}/contactpersons/\", methods: \"GET\"\n    end\n\n    matcher = self.get_matcher routes\n    expected = {\"_route\" => \"b\", \"customerId\" => \"123\"}\n\n    matcher.match(\"/api/customers/123/contactpersons/\").to_h.should eq expected\n\n    MockRedirectableURLMatcher.expected_path = \"/api/customers/123/contactpersons/\"\n\n    matcher.match(\"/api/customers/123/contactpersons\").to_h.should eq expected\n  end\n\n  def test_non_greedy_trailing_requirement : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/{a}\", requirements: {\"a\" => /\\d+/}\n    end\n\n    MockRedirectableURLMatcher.expected_path = \"/123\"\n\n    self.get_matcher(routes).match(\"/123/\").to_h.should eq({\"_route\" => \"a\", \"a\" => \"123\"})\n  end\n\n  def test_match_greedy_trailing_requirement_default1 : Nil\n    routes = self.build_collection do\n      add \"a\", ART::Route.new \"/fr-fr/{a}\", {\"a\" => \"aaa\"}, {\"a\" => /.+/}\n    end\n\n    self.get_matcher(routes).match(\"/fr-fr/\").to_h.should eq({\"_route\" => \"a\", \"a\" => \"aaa\"})\n  end\n\n  private def get_matcher(routes : ART::RouteCollection, context : ART::RequestContext = ART::RequestContext.new) : ART::Matcher::URLMatcher\n    ART.compile routes\n    MockRedirectableURLMatcher.new context\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/matcher/traceable_url_matcher_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./abstract_url_matcher_test_case\"\n\nstruct TraceableURLMatcherTest < AbstractURLMatcherTestCase\n  private def get_matcher(routes : ART::RouteCollection, context : ART::RequestContext = ART::RequestContext.new) : ART::Matcher::URLMatcher\n    ART::Matcher::TraceableURLMatcher.new routes, context\n  end\n\n  def test_traces : Nil\n    condition_route = ART::Route.new \"/foo2\", host: \"baz\"\n    condition_route.condition do |ctx|\n      \"GET\" == ctx.method\n    end\n\n    routes = self.build_collection do\n      add \"foo\", ART::Route.new \"/foo\", methods: \"POST\"\n      add \"bar\", ART::Route.new \"/bar/{id}\", requirements: {\"id\" => /\\d+/}\n      add \"bar1\", ART::Route.new \"/bar/{name}\", requirements: {\"id\" => /\\w+/}, methods: \"POST\"\n      add \"bar2\", ART::Route.new \"/foo\", host: \"baz\"\n      add \"bar3\", ART::Route.new \"/foo1\", host: \"baz\"\n      add \"bar4\", condition_route\n    end\n\n    context = ART::RequestContext.new host: \"baz\"\n\n    matcher = ART::Matcher::TraceableURLMatcher.new routes, context\n\n    traces = matcher.traces(\"/babar\")\n    self.get_levels(traces).should eq [0, 0, 0, 0, 0, 0]\n\n    traces = matcher.traces(\"/foo\")\n    self.get_levels(traces).should eq [1, 0, 0, 2]\n\n    traces = matcher.traces(\"/bar/12\")\n    self.get_levels(traces).should eq [0, 2]\n\n    traces = matcher.traces(\"/bar/dd\")\n    self.get_levels(traces).should eq [0, 1, 1, 0, 0, 0]\n\n    traces = matcher.traces(\"/foo1\")\n    self.get_levels(traces).should eq [0, 0, 0, 0, 2]\n\n    context.method = \"POST\"\n\n    traces = matcher.traces(\"/foo\")\n    self.get_levels(traces).should eq [2]\n\n    traces = matcher.traces(\"/bar/dd\")\n    self.get_levels(traces).should eq [0, 1, 2]\n\n    # Test Request overload (method is set via context)\n    traces = matcher.traces(::HTTP::Request.new \"GET\", \"/bar/dd\")\n    self.get_levels(traces).should eq [0, 1, 2]\n\n    traces = matcher.traces(\"/foo2\")\n    self.get_levels(traces).should eq [0, 0, 0, 0, 0, 1]\n  end\n\n  def test_traces_match_route_on_multiple_hosts : Nil\n    routes = self.build_collection do\n      add \"first\", ART::Route.new \"/mypath/\", {\"_controller\" => \"SomeController#first\"}, host: \"some.example.com\"\n      add \"second\", ART::Route.new \"/mypath/\", {\"_controller\" => \"SomeController#second\"}, host: \"another.example.com\"\n    end\n\n    context = ART::RequestContext.new host: \"baz\"\n\n    matcher = ART::Matcher::TraceableURLMatcher.new routes, context\n\n    traces = matcher.traces(\"/mypath/\")\n    self.get_levels(traces).should eq [1, 1]\n  end\n\n  private def get_levels(traces : Array(ART::Matcher::TraceableURLMatcher::Trace)) : Array(Int32)\n    traces.map &.level.value\n  end\n\n  private def build_collection(&) : ART::RouteCollection\n    routes = ART::RouteCollection.new\n\n    with routes yield\n\n    routes\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/matcher/url_matcher_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./abstract_url_matcher_test_case\"\n\nstruct URLMatcherTest < AbstractURLMatcherTestCase\n  private def get_matcher(routes : ART::RouteCollection, context : ART::RequestContext = ART::RequestContext.new) : ART::Matcher::URLMatcher\n    ART.compile routes\n    ART::Matcher::URLMatcher.new context\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/parameters_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct ParametersTest < ASPEC::TestCase\n  # Constructors\n\n  def test_initialize_empty : Nil\n    params = ART::Parameters.new\n    params.empty?.should be_true\n    params.size.should eq 0\n  end\n\n  def test_new_from_hash : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\", \"baz\" => \"qux\"})\n    params.size.should eq 2\n    params[\"foo\"].should eq \"bar\"\n    params[\"baz\"].should eq \"qux\"\n  end\n\n  def test_new_from_hash_with_different_types : Nil\n    params = ART::Parameters.new({\"count\" => 42, \"enabled\" => true})\n    params.size.should eq 2\n    params.get(\"count\", Int32).should eq 42\n    params.get(\"enabled\", Bool).should be_true\n  end\n\n  def test_new_from_parameters : Nil\n    original = ART::Parameters.new({\"foo\" => \"bar\"})\n    copy = ART::Parameters.new(original)\n    copy.should eq original\n  end\n\n  # has_key?\n\n  def test_has_key_with_existing_key : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params.has_key?(\"foo\").should be_true\n  end\n\n  def test_has_key_with_missing_key : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params.has_key?(\"missing\").should be_false\n  end\n\n  def test_has_key_empty : Nil\n    params = ART::Parameters.new\n    params.has_key?(\"anything\").should be_false\n  end\n\n  # []? (returns String?)\n\n  def test_bracket_question_with_string_value : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params[\"foo\"]?.should eq \"bar\"\n  end\n\n  def test_bracket_question_with_missing_key : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params[\"missing\"]?.should be_nil\n  end\n\n  def test_bracket_question_with_non_string_value : Nil\n    params = ART::Parameters.new\n    params[\"count\"] = 42\n    params[\"count\"]?.should be_nil\n  end\n\n  # [] (returns String, raises KeyError)\n\n  def test_bracket_with_string_value : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params[\"foo\"].should eq \"bar\"\n  end\n\n  def test_bracket_with_missing_key : Nil\n    params = ART::Parameters.new\n    expect_raises(KeyError, \"No parameter exists with the name 'missing'.\") do\n      params[\"missing\"]\n    end\n  end\n\n  def test_bracket_with_non_string_value : Nil\n    params = ART::Parameters.new\n    params[\"count\"] = 42\n    expect_raises(TypeCastError) do\n      params[\"count\"]\n    end\n  end\n\n  # raw?\n\n  def test_raw_with_string_value : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params.raw?(\"foo\").should eq \"bar\"\n  end\n\n  def test_raw_with_int_value : Nil\n    params = ART::Parameters.new\n    params[\"count\"] = 42\n    params.raw?(\"count\").should eq 42\n  end\n\n  def test_raw_with_bool_value : Nil\n    params = ART::Parameters.new\n    params[\"enabled\"] = true\n    params.raw?(\"enabled\").should be_true\n  end\n\n  def test_raw_with_missing_key : Nil\n    params = ART::Parameters.new\n    params.raw?(\"missing\").should be_nil\n  end\n\n  # get? (typed retrieval, returns T?)\n\n  def test_get_question_with_correct_type : Nil\n    params = ART::Parameters.new\n    params[\"count\"] = 42\n    params.get?(\"count\", Int32).should eq 42\n  end\n\n  def test_get_question_with_wrong_type : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params.get?(\"foo\", Int32).should be_nil\n  end\n\n  def test_get_question_with_missing_key : Nil\n    params = ART::Parameters.new\n    params.get?(\"missing\", String).should be_nil\n  end\n\n  def test_get_question_string : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params.get?(\"foo\", String).should eq \"bar\"\n  end\n\n  # get (typed retrieval, raises KeyError)\n\n  def test_get_with_correct_type : Nil\n    params = ART::Parameters.new\n    params[\"count\"] = 42\n    params.get(\"count\", Int32).should eq 42\n  end\n\n  def test_get_with_missing_key : Nil\n    params = ART::Parameters.new\n    expect_raises(KeyError, \"No parameter exists with the name 'missing'.\") do\n      params.get(\"missing\", Int32)\n    end\n  end\n\n  def test_get_with_wrong_type : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    expect_raises(TypeCastError) do\n      params.get(\"foo\", Int32)\n    end\n  end\n\n  # keys\n\n  def test_keys : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\", \"baz\" => \"qux\"})\n    params.keys.should eq [\"foo\", \"baz\"]\n  end\n\n  def test_keys_empty : Nil\n    params = ART::Parameters.new\n    params.keys.should be_empty\n  end\n\n  # empty?\n\n  def test_empty_true : Nil\n    params = ART::Parameters.new\n    params.empty?.should be_true\n  end\n\n  def test_empty_false : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params.empty?.should be_false\n  end\n\n  # size\n\n  def test_size_empty : Nil\n    params = ART::Parameters.new\n    params.size.should eq 0\n  end\n\n  def test_size_with_values : Nil\n    params = ART::Parameters.new({\"a\" => \"1\", \"b\" => \"2\", \"c\" => \"3\"})\n    params.size.should eq 3\n  end\n\n  # []=\n\n  def test_set_string_value : Nil\n    params = ART::Parameters.new\n    params[\"foo\"] = \"bar\"\n    params[\"foo\"].should eq \"bar\"\n  end\n\n  def test_set_int_value : Nil\n    params = ART::Parameters.new\n    params[\"count\"] = 42\n    params.get(\"count\", Int32).should eq 42\n  end\n\n  def test_set_overwrites_existing : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params[\"foo\"] = \"updated\"\n    params[\"foo\"].should eq \"updated\"\n  end\n\n  def test_set_nil_value : Nil\n    params = ART::Parameters.new\n    params[\"nullable\"] = nil\n    params.raw?(\"nullable\").should be_nil\n    params.has_key?(\"nullable\").should be_true\n  end\n\n  # delete\n\n  def test_delete_existing_key : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\", \"baz\" => \"qux\"})\n    params.delete(\"foo\")\n    params.has_key?(\"foo\").should be_false\n    params.size.should eq 1\n  end\n\n  def test_delete_missing_key : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    params.delete(\"missing\")\n    params.size.should eq 1\n  end\n\n  # merge!\n\n  def test_merge : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    other = ART::Parameters.new({\"baz\" => \"qux\"})\n    result = params.merge!(other)\n    result.should eq params\n    params[\"foo\"].should eq \"bar\"\n    params[\"baz\"].should eq \"qux\"\n    params.size.should eq 2\n  end\n\n  def test_merge_overwrites : Nil\n    params = ART::Parameters.new({\"foo\" => \"original\"})\n    other = ART::Parameters.new({\"foo\" => \"updated\"})\n    params.merge!(other)\n    params[\"foo\"].should eq \"updated\"\n  end\n\n  def test_merge_nil : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    result = params.merge!(nil)\n    result.should eq params\n    params.size.should eq 1\n  end\n\n  # each\n\n  def test_each : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\", \"baz\" => \"qux\"})\n    collected = {} of String => String\n    params.each do |key, value|\n      collected[key] = value.as(String)\n    end\n    collected.should eq({\"foo\" => \"bar\", \"baz\" => \"qux\"})\n  end\n\n  def test_each_with_different_types : Nil\n    params = ART::Parameters.new\n    params[\"name\"] = \"test\"\n    params[\"count\"] = 42\n    keys = [] of String\n    params.each do |key, _|\n      keys << key\n    end\n    keys.should eq [\"name\", \"count\"]\n  end\n\n  # dup\n\n  def test_dup : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    copy = params.dup\n    copy[\"foo\"].should eq \"bar\"\n    copy[\"new\"] = \"value\"\n    params.has_key?(\"new\").should be_false\n  end\n\n  # clone\n\n  def test_clone : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    copy = params.clone\n    copy[\"foo\"].should eq \"bar\"\n    copy[\"new\"] = \"value\"\n    params.has_key?(\"new\").should be_false\n  end\n\n  # to_h\n\n  def test_to_h_with_strings : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\", \"baz\" => \"qux\"})\n    params.to_h.should eq({\"foo\" => \"bar\", \"baz\" => \"qux\"})\n  end\n\n  def test_to_h_with_non_string_values : Nil\n    params = ART::Parameters.new\n    params[\"count\"] = 42\n    params[\"enabled\"] = true\n    hash = params.to_h\n    hash[\"count\"].should eq \"42\"\n    hash[\"enabled\"].should eq \"true\"\n  end\n\n  def test_to_h_with_nil_value : Nil\n    params = ART::Parameters.new\n    params[\"nullable\"] = nil\n    hash = params.to_h\n    hash[\"nullable\"].should be_nil\n  end\n\n  def test_to_h_empty : Nil\n    params = ART::Parameters.new\n    params.to_h.should be_empty\n  end\n\n  # == (Parameters)\n\n  def test_equality_same_values : Nil\n    params1 = ART::Parameters.new({\"foo\" => \"bar\"})\n    params2 = ART::Parameters.new({\"foo\" => \"bar\"})\n    (params1 == params2).should be_true\n  end\n\n  def test_equality_different_values : Nil\n    params1 = ART::Parameters.new({\"foo\" => \"bar\"})\n    params2 = ART::Parameters.new({\"foo\" => \"different\"})\n    (params1 == params2).should be_false\n  end\n\n  def test_equality_different_keys : Nil\n    params1 = ART::Parameters.new({\"foo\" => \"bar\"})\n    params2 = ART::Parameters.new({\"baz\" => \"bar\"})\n    (params1 == params2).should be_false\n  end\n\n  def test_equality_different_sizes : Nil\n    params1 = ART::Parameters.new({\"foo\" => \"bar\"})\n    params2 = ART::Parameters.new({\"foo\" => \"bar\", \"extra\" => \"value\"})\n    (params1 == params2).should be_false\n  end\n\n  def test_equality_empty : Nil\n    params1 = ART::Parameters.new\n    params2 = ART::Parameters.new\n    (params1 == params2).should be_true\n  end\n\n  # == (Hash)\n\n  def test_equality_with_hash_same : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    (params == {\"foo\" => \"bar\"}).should be_true\n  end\n\n  def test_equality_with_hash_different : Nil\n    params = ART::Parameters.new({\"foo\" => \"bar\"})\n    (params == {\"foo\" => \"different\"}).should be_false\n  end\n\n  def test_equality_with_hash_converts_types : Nil\n    params = ART::Parameters.new\n    params[\"count\"] = 42\n    (params == {\"count\" => \"42\"}).should be_true\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/request_context_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct RequestContextTest < ASPEC::TestCase\n  def test_constructor : Nil\n    request_context = ART::RequestContext.new(\n      \"foo\",\n      \"post\",\n      \"foo.bar\",\n      \"HTTPS\",\n      8080,\n      444,\n      \"/baz\",\n      \"bar=foobar\",\n    )\n\n    request_context.base_url.should eq \"foo\"\n    request_context.method.should eq \"POST\"\n    request_context.host.should eq \"foo.bar\"\n    request_context.scheme.should eq \"https\"\n    request_context.http_port.should eq 8080\n    request_context.https_port.should eq 444\n    request_context.path.should eq \"/baz\"\n    request_context.query_string.should eq \"bar=foobar\"\n  end\n\n  def test_getters_setters : Nil\n    request_context = ART::RequestContext.new\n\n    request_context.base_url = \"foo\"\n    request_context.method = \"POST\"\n    request_context.host = \"foo.bar\"\n    request_context.scheme = \"https\"\n    request_context.http_port = 8080\n    request_context.https_port = 444\n    request_context.path = \"/baz\"\n    request_context.query_string = \"bar=foobar\"\n\n    request_context.base_url.should eq \"foo\"\n    request_context.method.should eq \"POST\"\n    request_context.host.should eq \"foo.bar\"\n    request_context.scheme.should eq \"https\"\n    request_context.http_port.should eq 8080\n    request_context.https_port.should eq 444\n    request_context.path.should eq \"/baz\"\n    request_context.query_string.should eq \"bar=foobar\"\n  end\n\n  def test_from_uri_with_base_url : Nil\n    request_context = ART::RequestContext.from_uri \"https://test.com:444/index.html\"\n\n    request_context.method.should eq \"GET\"\n    request_context.host.should eq \"test.com\"\n    request_context.scheme.should eq \"https\"\n    request_context.http_port.should eq 80\n    request_context.https_port.should eq 444\n    request_context.base_url.should eq \"/index.html\"\n    request_context.path.should eq \"/\"\n  end\n\n  def test_from_uri_trailing_slash : Nil\n    request_context = ART::RequestContext.from_uri \"http://test.com:8080/\"\n\n    request_context.scheme.should eq \"http\"\n    request_context.host.should eq \"test.com\"\n    request_context.http_port.should eq 8080\n    request_context.https_port.should eq 443\n    request_context.base_url.should eq \"/\"\n    request_context.path.should eq \"/\"\n  end\n\n  def test_from_uri_without_trailing_slash : Nil\n    request_context = ART::RequestContext.from_uri \"https://test.com\"\n\n    request_context.scheme.should eq \"https\"\n    request_context.host.should eq \"test.com\"\n    request_context.base_url.should be_empty\n    request_context.path.should eq \"/\"\n  end\n\n  def test_from_uri_empty : Nil\n    request_context = ART::RequestContext.from_uri \"\"\n\n    request_context.scheme.should eq \"http\"\n    request_context.host.should eq \"localhost\"\n    request_context.base_url.should be_empty\n    request_context.path.should eq \"/\"\n  end\n\n  @[TestWith(\n    {\"http://foo.com\\\\bar\"},\n    {\"\\\\\\\\foo.com/bar\"},\n    {\"a\\rb\"},\n    {\"a\\nb\"},\n    {\"a\\tb\"},\n    {\"\\0foo\"},\n    {\"foo\\0\"},\n    {\" foo\"},\n    {\"foo \"},\n    # {\":\"},\n  )]\n  def test_from_uri_invalid(uri : String) : Nil\n    request_context = ART::RequestContext.from_uri uri\n\n    request_context.scheme.should eq \"http\"\n    request_context.host.should eq \"localhost\"\n    request_context.base_url.should be_empty\n    request_context.path.should eq \"/\"\n  end\n\n  def test_from_request : Nil\n    request = ART::Request.new \"GET\", \"/foo?bar=baz\", headers: ::HTTP::Headers{\"host\" => \"test.com:444\"}\n\n    request_context = ART::RequestContext.new\n    request_context.apply request\n\n    request_context.base_url.should be_empty\n    request_context.method.should eq \"GET\"\n    request_context.host.should eq \"test.com\"\n    request_context.path.should eq \"/foo\"\n    request_context.query_string.should eq \"bar=baz\"\n\n    # Don't really have a way to determine these via `::HTTP::Request` at the moment :/\n    request_context.scheme.should eq \"http\"\n    request_context.http_port.should eq 80\n    request_context.https_port.should eq 443\n  end\n\n  def test_parameters : Nil\n    request_context = ART::RequestContext.new\n    request_context.parameters.should be_empty\n\n    request_context.parameters = {\"foo\" => \"bar\"} of String => String?\n    request_context.parameters.should eq({\"foo\" => \"bar\"})\n\n    request_context.parameter(\"foo\").should eq \"bar\"\n  end\n\n  def test_has_parameter : Nil\n    request_context = ART::RequestContext.new\n    request_context.has_parameter?(\"foo\").should be_false\n    request_context.set_parameter \"foo\", \"bar\"\n    request_context.has_parameter?(\"foo\").should be_true\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/requirement/enum_spec.cr",
    "content": "require \"../spec_helper\"\n\nenum EnumRequirementEnum\n  A\n  B\n  C\nend\n\n@[Flags]\nenum EnumRequirementEnumFlags\n  A\n  B\n  C\nend\n\nstruct EnumRequirementTest < ASPEC::TestCase\n  def test_to_s_no_members : Nil\n    ART::Requirement::Enum(EnumRequirementEnum).new.to_s.should eq \"a|b|c\"\n    ART::Requirement::Enum(EnumRequirementEnumFlags).new.to_s.should eq \"a|b|c\"\n  end\n\n  def test_to_s_with_members : Nil\n    ART::Requirement::Enum(EnumRequirementEnum).new(:a, :c).to_s.should eq \"a|c\"\n    ART::Requirement::Enum(EnumRequirementEnumFlags).new(:b, :c).to_s.should eq \"b|c\"\n  end\n\n  @[Tags(\"compiled\")]\n  def test_constructor_non_enum_type : Nil\n    self.assert_compile_time_error \"'Int32' is not an Enum type.\", <<-CR\n      require \"../spec_helper\"\n      ART::Requirement::Enum(Int32).new\n    CR\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/requirement/requirement_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct RequirementsTest < ASPEC::TestCase\n  @[TestWith(\n    {\"FOO\"},\n    {\"foo\"},\n    {\"1987\"},\n    {\"42-42\"},\n    {\"for2o-bar\"},\n    {\"foo-bA198r-Ccc\"},\n    {\"for10O-bar-CCc-fooba187rccc\"},\n  )]\n  def test_ascii_slug_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::ASCII_SLUG}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"-\"},\n    {\"fôo\"},\n    {\"-FOO\"},\n    {\"foo-\"},\n    {\"-foo-\"},\n    {\"-foo-bar-\"},\n    {\"foo--bar\"},\n  )]\n  def test_ascii_slug_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::ASCII_SLUG}).compile.regex\n  end\n\n  @[TestWith(\n    {\"foo\"},\n    {\"foo/bar/ccc\"},\n    {\"///\"},\n  )]\n  def test_catch_all_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::CATCH_ALL}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n  )]\n  def test_catch_all_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::CATCH_ALL}).compile.regex\n  end\n\n  @[TestWith(\n    {\"0000-01-01\"},\n    {\"9999-12-31\"},\n    {\"2022-04-15\"},\n    {\"2024-02-29\"},\n    {\"1243-04-31\"},\n  )]\n  def test_date_ymd_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::DATE_YMD}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"0000-01-00\"},\n    {\"9999-00-31\"},\n    {\"2022-02-30\"},\n    {\"2022-02-31\"},\n  )]\n  def test_date_ymd_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::DATE_YMD}).compile.regex\n  end\n\n  @[TestWith(\n    {\"0\"},\n    {\"012\"},\n    {\"1\"},\n    {\"42\"},\n    {\"42198\"},\n    {\"999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999\"},\n  )]\n  def test_digits_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::DIGITS}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"-1\"},\n    {\"3.14\"},\n  )]\n  def test_digits_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::DIGITS}).compile.regex\n  end\n\n  @[TestWith(\n    {\"1\"},\n    {\"42\"},\n    {\"42198\"},\n    {\"999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999\"},\n  )]\n  def test_positive_int_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::POSITIVE_INT}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"0\"},\n    {\"045\"},\n    {\"foo\"},\n    {\"-1\"},\n    {\"3.14\"},\n  )]\n  def test_positive_int_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::POSITIVE_INT}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000000000000000000000\"},\n    {\"ZZZZZZZZZZZZZZZZZZZZZZZZZZ\"},\n    {\"01G0P4XH09KW3RCF7G4Q57ESN0\"},\n    {\"05CSACM1MS9RB9H5F61BYA146Q\"},\n  )]\n  def test_uid_base_32_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UID_BASE32}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"01G0P4XH09KW3RCF7G4Q57ESN\"},\n    {\"01G0P4XH09KW3RCF7G4Q57ESNU\"},\n  )]\n  def test_uid_base_32_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UID_BASE32}).compile.regex\n  end\n\n  @[TestWith(\n    {\"1111111111111111111111\"},\n    {\"zzzzzzzzzzzzzzzzzzzzzz\"},\n    {\"1BkPBX6T19U8TUAjBTtgwH\"},\n    {\"1fg491dt8eQpf2TU42o2bY\"},\n  )]\n  def test_uid_base_58_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UID_BASE58}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"1BkPBX6T19U8TUAjBTtgw\"},\n    {\"1BkPBX6T19U8TUAjBTtgwI\"},\n  )]\n  def test_uid_base_58_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UID_BASE58}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000-0000-0000-0000-000000000000\"},\n    {\"ffffffff-ffff-ffff-ffff-ffffffffffff\"},\n    {\"01802c4e-c409-9f07-863c-f025ca7766a0\"},\n    {\"056654ca-0699-4e16-9895-e60afca090d7\"},\n  )]\n  def test_uid_rfc4122_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UID_RFC4122}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"01802c4e-c409-9f07-863c-f025ca7766a\"},\n    {\"01802c4e-c409-9f07-863c-f025ca7766ag\"},\n    {\"01802c4ec4099f07863cf025ca7766a0\"},\n  )]\n  def test_uid_rfc4122_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UID_RFC4122}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000000000000000000000\"},\n    {\"7ZZZZZZZZZZZZZZZZZZZZZZZZZ\"},\n    {\"01G0P4ZPM69QTD4MM4ENAEA4EW\"},\n  )]\n  def test_ulid_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::ULID}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"8ZZZZZZZZZZZZZZZZZZZZZZZZZ\"},\n    {\"01G0P4ZPM69QTD4MM4ENAEA4E\"},\n  )]\n  def test_ulid_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::ULID}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000-0000-1000-8000-000000000000\"},\n    {\"ffffffff-ffff-8fff-bfff-ffffffffffff\"},\n    {\"8c670a1c-bc95-11ec-8422-0242ac120002\"},\n    {\"21902510-bc96-21ec-8422-0242ac120002\"},\n    {\"61c86569-e477-3ed9-9e3b-1562edb03277\"},\n    {\"e55a29be-ba25-46e0-a5e5-85b78a6f9a11\"},\n    {\"bad98960-f1a1-530e-9a82-07d0b6c4e62f\"},\n    {\"1ecbc9a8-432d-6b14-af93-715adc3b830c\"},\n    {\"216fff40-98d9-71e3-a5e2-0800200c9a66\"},\n    {\"216fff40-98d9-81e3-a5e2-0800200c9a66\"},\n  )]\n  def test_uuid_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"01802c74-d78c-b085-0cdf-7cbad87c70a3\"},\n    {\"e55a29be-bb25-46e0-a5e5-85b78a6f9a1\"},\n    {\"e55a29bh-bb25-46e0-a5e5-85b78a6f9a11\"},\n    {\"e55a29beba2546e0a5e585b78a6f9a11\"},\n  )]\n  def test_uuid_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000-0000-1000-8000-000000000000\"},\n    {\"ffffffff-ffff-1fff-bfff-ffffffffffff\"},\n    {\"21902510-bc96-11ec-8422-0242ac120002\"},\n    {\"a8ff8f60-088e-1099-a09d-53afc49918d1\"},\n    {\"b0ac612c-9117-17a1-901f-53afc49918d1\"},\n  )]\n  def test_uuid_v1_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V1}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"a3674b89-0170-3e30-8689-52939013e39c\"},\n    {\"e0040090-3cb0-4bf9-a868-407770c964f9\"},\n    {\"2e2b41d9-e08c-53d2-b435-818b9c323942\"},\n    {\"2a37b67a-5eaa-6424-b5d6-ffc9ba0f2a13\"},\n  )]\n  def test_uuid_v1_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V1}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000-0000-3000-8000-000000000000\"},\n    {\"ffffffff-ffff-3fff-bfff-ffffffffffff\"},\n    {\"2b3f1427-33b2-30a9-8759-07355007c204\"},\n    {\"c38e7b09-07f7-3901-843d-970b0186b873\"},\n  )]\n  def test_uuid_v3_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V3}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"e24d9c0e-bc98-11ec-9924-53afc49918d1\"},\n    {\"1c240248-7d0b-41a4-9d20-61ad2915a58c\"},\n    {\"4816b668-385b-5a65-808d-bca410f45090\"},\n    {\"1d2f3104-dff6-64c6-92ff-0f74b1d0e2af\"},\n  )]\n  def test_uuid_v3_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V3}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000-0000-4000-8000-000000000000\"},\n    {\"ffffffff-ffff-4fff-bfff-ffffffffffff\"},\n    {\"b8f15bf4-46e2-4757-bbce-11ae83f7a6ea\"},\n    {\"eaf51230-1ce2-40f1-ab18-649212b26198\"},\n  )]\n  def test_uuid_v4_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V4}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"15baaab2-f310-11d2-9ecf-53afc49918d1\"},\n    {\"acd44dc8-d2cc-326c-9e3a-80a3305a25e8\"},\n    {\"7fc2705f-a8a4-5b31-99a8-890686d64189\"},\n    {\"1ecbc991-3552-6920-998e-efad54178a98\"},\n  )]\n  def test_uuid_v4_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V4}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000-0000-5000-8000-000000000000\"},\n    {\"ffffffff-ffff-5fff-bfff-ffffffffffff\"},\n    {\"49f4d32c-28b3-5802-8717-a2896180efbd\"},\n    {\"58b3c62e-a7df-5a82-93a6-fbe5fda681c1\"},\n  )]\n  def test_uuid_v5_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V5}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"b99ad578-fdd3-1135-9d3b-53afc49918d1\"},\n    {\"b3ee3071-7a2b-3e17-afdf-6b6aec3acf85\"},\n    {\"2ab4f5a7-6412-46c1-b3ab-1fe1ed391e27\"},\n    {\"135fdd3d-e193-653e-865d-67e88cf12e44\"},\n  )]\n  def test_uuid_v5_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V5}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000-0000-6000-8000-000000000000\"},\n    {\"ffffffff-ffff-6fff-bfff-ffffffffffff\"},\n    {\"2c51caad-c72f-66b2-b6d7-8766d36c73df\"},\n    {\"17941ebb-48fa-6bfe-9bbd-43929f8784f5\"},\n    {\"1ecbc993-f6c2-67f2-8fbe-295ed594b344\"},\n  )]\n  def test_uuid_v6_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V6}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"821040f4-7b67-12a3-9770-53afc49918d1\"},\n    {\"802dc245-aaaa-3649-98c6-31c549b0df86\"},\n    {\"92d2e5ad-bc4e-4947-a8d9-77706172ca83\"},\n    {\"6e124559-d260-511e-afdc-e57c7025fed0\"},\n  )]\n  def test_uuid_v6_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V6}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000-0000-7000-8000-000000000000\"},\n    {\"ffffffff-ffff-7fff-bfff-ffffffffffff\"},\n    {\"216fff40-98d9-71e3-a5e2-0800200c9a66\"},\n  )]\n  def test_uuid_v7_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V7}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"821040f4-7b67-12a3-9770-53afc49918d1\"},\n    {\"802dc245-aaaa-3649-98c6-31c549b0df86\"},\n    {\"92d2e5ad-bc4e-4947-a8d9-77706172ca83\"},\n    {\"6e124559-d260-511e-afdc-e57c7025fed0\"},\n    {\"17941ebb-48fa-6bfe-9bbd-43929f8784f5\"},\n    {\"216fff40-98d9-81e3-a5e2-0800200c9a66\"},\n  )]\n  def test_uuid_v7_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V7}).compile.regex\n  end\n\n  @[TestWith(\n    {\"00000000-0000-8000-8000-000000000000\"},\n    {\"ffffffff-ffff-8fff-bfff-ffffffffffff\"},\n    {\"216fff40-98d9-81e3-a5e2-0800200c9a66\"},\n  )]\n  def test_uuid_v8_valid(path : String) : Nil\n    \"/#{path}\".should match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V8}).compile.regex\n  end\n\n  @[TestWith(\n    {\"\"},\n    {\"foo\"},\n    {\"821040f4-7b67-12a3-9770-53afc49918d1\"},\n    {\"802dc245-aaaa-3649-98c6-31c549b0df86\"},\n    {\"92d2e5ad-bc4e-4947-a8d9-77706172ca83\"},\n    {\"6e124559-d260-511e-afdc-e57c7025fed0\"},\n    {\"17941ebb-48fa-6bfe-9bbd-43929f8784f5\"},\n    {\"216fff40-98d9-71e3-a5e2-0800200c9a66\"},\n  )]\n  def test_uuid_v8_invalid(path : String) : Nil\n    \"/#{path}\".should_not match ART::Route.new(\"/{path}\", requirements: {\"path\" => ART::Requirement::UUID_V8}).compile.regex\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/route_collection_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct RouteCollectionTest < ASPEC::TestCase\n  def test_route_interactions : Nil\n    collection = ART::RouteCollection.new\n    route = ART::Route.new \"/foo\"\n    collection.add \"foo\", route\n    collection.routes.should eq({\"foo\" => route})\n    collection[\"foo\"].should be route\n    collection[\"foo\"]?.should be route\n\n    collection[\"bar\"]?.should be_nil\n\n    expect_raises ART::Exception::RouteNotFound, \"No route with the name 'bar' exists.\" do\n      collection[\"bar\"]\n    end\n  end\n\n  def test_overridden_route : Nil\n    collection = ART::RouteCollection.new\n    route1 = ART::Route.new \"/foo\"\n    route2 = ART::Route.new \"/bar\"\n\n    collection.add \"foo\", route1\n    collection.add \"foo\", route2\n\n    collection[\"foo\"].should be route2\n  end\n\n  def test_each_iterator : Nil\n    collection = ART::RouteCollection.new\n    route1 = ART::Route.new \"/foo\"\n    route2 = ART::Route.new \"/bar\"\n\n    collection.add \"foo\", route1\n    collection.add \"bar\", route2\n\n    assert_iterates_iterator [{\"foo\", route1}, {\"bar\", route2}], collection.each\n  end\n\n  def test_deep_overridden_route : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", ART::Route.new \"/foo\"\n\n    collection1 = ART::RouteCollection.new\n    collection1.add \"foo\", ART::Route.new \"/foo1\"\n\n    collection2 = ART::RouteCollection.new\n    collection2.add \"foo\", ART::Route.new \"/foo2\"\n\n    collection1.add collection2\n    collection.add collection1\n\n    collection1[\"foo\"].path.should eq \"/foo2\"\n    collection[\"foo\"].path.should eq \"/foo2\"\n  end\n\n  def test_size : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", ART::Route.new \"/foo\"\n\n    collection1 = ART::RouteCollection.new\n    collection1.add \"bar\", ART::Route.new \"/bar\"\n\n    collection.add collection1\n\n    collection.size.should eq 2\n  end\n\n  def test_add_collection : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", foo = ART::Route.new \"/foo\"\n\n    collection1 = ART::RouteCollection.new\n    collection1.add \"bar\", bar = ART::Route.new \"/bar\"\n\n    collection2 = ART::RouteCollection.new\n    collection2.add \"baz\", baz = ART::Route.new \"/baz\"\n\n    collection1.add collection2\n    collection.add collection1\n\n    collection.routes.should eq({\"foo\" => foo, \"bar\" => bar, \"baz\" => baz})\n  end\n\n  def test_add_defaults : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", ART::Route.new \"/{placeholder}\"\n\n    collection1 = ART::RouteCollection.new\n    collection1.add \"bar\", ART::Route.new \"/{placeholder}\", {\"placeholder\" => \"default\", \"foo\" => \"bar\"}, {\"placeholder\" => /.+/}\n    collection.add collection1\n\n    collection.add_defaults({\"placeholder\" => \"new-default\"})\n    collection[\"foo\"].defaults.should eq({\"placeholder\" => \"new-default\"})\n    collection[\"bar\"].defaults.should eq({\"placeholder\" => \"new-default\", \"foo\" => \"bar\"})\n  end\n\n  def test_add_requirements : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", ART::Route.new \"/{placeholder}\"\n\n    collection1 = ART::RouteCollection.new\n    collection1.add \"bar\", ART::Route.new \"/{placeholder}\", {\"placeholder\" => \"default\", \"foo\" => \"bar\"}, {\"placeholder\" => /.+/}\n    collection.add collection1\n\n    collection.add_requirements({\"placeholder\" => /\\d+/})\n    collection[\"foo\"].requirements.should eq({\"placeholder\" => /\\d+/})\n    collection[\"bar\"].requirements.should eq({\"placeholder\" => /\\d+/})\n  end\n\n  def test_add_prefix : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", ART::Route.new \"/foo\"\n\n    collection1 = ART::RouteCollection.new\n    collection1.add \"bar\", ART::Route.new \"/bar\"\n\n    collection.add collection1\n    collection.add_prefix \" / \"\n\n    collection[\"foo\"].path.should eq \"/foo\"\n\n    collection.add_prefix \"/{admin}\", {\"admin\" => \"admin\"}, {\"admin\" => /\\d+/}\n\n    collection[\"foo\"].path.should eq \"/{admin}/foo\"\n    collection[\"bar\"].path.should eq \"/{admin}/bar\"\n    collection[\"foo\"].defaults.should eq({\"admin\" => \"admin\"})\n    collection[\"bar\"].defaults.should eq({\"admin\" => \"admin\"})\n    collection[\"foo\"].requirements.should eq({\"admin\" => /\\d+/})\n    collection[\"bar\"].requirements.should eq({\"admin\" => /\\d+/})\n\n    collection.add_prefix \"0\"\n\n    collection[\"foo\"].path.should eq \"/0/{admin}/foo\"\n\n    collection.add_prefix \"/ /\"\n\n    collection[\"foo\"].path.should eq \"/ /0/{admin}/foo\"\n    collection[\"bar\"].path.should eq \"/ /0/{admin}/bar\"\n  end\n\n  def test_add_prefix_overrides_requirements : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", ART::Route.new \"/foo.{_format}\"\n    collection.add \"bar\", ART::Route.new \"/bar.{_format}\", requirements: {\"_format\" => \"json\"}\n    collection.add_prefix \"/admin\", requirements: {\"_format\" => \"html\"}\n\n    collection[\"foo\"].requirement(\"_format\").should eq /html/\n    collection[\"bar\"].requirement(\"_format\").should eq /html/\n  end\n\n  def test_unique_route_with_given_name : Nil\n    collection1 = ART::RouteCollection.new\n    collection1.add \"foo\", ART::Route.new \"/old\"\n    collection2 = ART::RouteCollection.new\n    collection3 = ART::RouteCollection.new\n    collection3.add \"foo\", route = ART::Route.new \"/new\"\n\n    collection2.add collection3\n    collection1.add collection2\n\n    collection1[\"foo\"].should be route\n    collection1.size.should eq 1\n  end\n\n  def test_remove : Nil\n    collection1 = ART::RouteCollection.new\n    collection1.add \"foo\", ART::Route.new \"/foo\"\n\n    collection2 = ART::RouteCollection.new\n    collection2.add \"bar\", bar = ART::Route.new \"/bar\"\n    collection1.add collection2\n    collection1.add \"last\", last = ART::Route.new \"/last\"\n\n    collection1.remove \"foo\"\n    collection1.routes.should eq({\"bar\" => bar, \"last\" => last})\n    collection1.remove \"bar\", \"last\"\n    collection1.routes.should be_empty\n  end\n\n  def test_set_host : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"a\", a = ART::Route.new \"/a\"\n    collection.add \"b\", b = ART::Route.new \"/b\", host: \"{locale}.example.net\"\n\n    collection.set_host \"{locale}.example.com\"\n\n    a.host.should eq \"{locale}.example.com\"\n    b.host.should eq \"{locale}.example.com\"\n  end\n\n  def test_clone : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"a\", ART::Route.new \"/a\"\n    collection.add \"b\", ART::Route.new \"/b\", {\"placeholder\" => \"default\"}, {\"placeholder\" => /.+/}\n\n    cloned_collection = collection.clone\n\n    cloned_collection.size.should eq 2\n    cloned_collection[\"a\"].should eq collection[\"a\"]\n    cloned_collection[\"a\"].should_not be collection[\"a\"]\n    cloned_collection[\"b\"].should eq collection[\"b\"]\n    cloned_collection[\"b\"].should_not be collection[\"b\"]\n  end\n\n  def test_set_scheme : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"a\", a = ART::Route.new \"/a\", schemes: \"http\"\n    collection.add \"b\", b = ART::Route.new \"/b\"\n\n    collection.schemes = {\"http\", \"https\"}\n\n    a.schemes.should eq Set{\"http\", \"https\"}\n    b.schemes.should eq Set{\"http\", \"https\"}\n  end\n\n  def test_set_methods : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"a\", a = ART::Route.new \"/a\", methods: {\"get\", \"POST\"}\n    collection.add \"b\", b = ART::Route.new \"/b\"\n\n    collection.methods = \"put\"\n\n    a.methods.should eq Set{\"PUT\"}\n    b.methods.should eq Set{\"PUT\"}\n  end\n\n  def test_add_name_prefix : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", foo = ART::Route.new \"/foo\"\n    collection.add \"bar\", bar = ART::Route.new \"/bar\"\n    collection.add \"api_foo\", api_foo = ART::Route.new \"/api/foo\"\n\n    collection.add_name_prefix \"api_\"\n\n    collection[\"api_foo\"].should be foo\n    collection[\"api_bar\"].should be bar\n    collection[\"api_api_foo\"].should be api_foo\n    collection[\"foo\"]?.should be_nil\n    collection[\"bar\"]?.should be_nil\n  end\n\n  def test_add_name_prefix_canonical_route_name : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", ART::Route.new \"/foo\", {\"_canonical_route\" => \"foo\"}\n    collection.add \"bar\", ART::Route.new \"/bar\", {\"_canonical_route\" => \"bar\"}\n    collection.add \"api_foo\", ART::Route.new \"/api/foo\", {\"_canonical_route\" => \"api_foo\"}\n\n    collection.add_name_prefix \"api_\"\n\n    collection[\"api_foo\"].default(\"_canonical_route\").should eq \"api_foo\"\n    collection[\"api_bar\"].default(\"_canonical_route\").should eq \"api_bar\"\n    collection[\"api_api_foo\"].default(\"_canonical_route\").should eq \"api_api_foo\"\n  end\n\n  def test_add_with_priority : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", foo = ART::Route.new(\"/foo\"), 0\n    collection.add \"bar\", bar = ART::Route.new(\"/bar\"), 1\n    collection.add \"baz\", baz = ART::Route.new \"/baz\"\n\n    collection.routes.should eq({\n      \"bar\" => bar,\n      \"foo\" => foo,\n      \"baz\" => baz,\n    })\n\n    collection2 = ART::RouteCollection.new\n    collection2.add \"foo2\", foo2 = ART::Route.new(\"/foo\"), 0\n    collection2.add \"bar2\", bar2 = ART::Route.new(\"/bar\"), 1\n    collection2.add \"baz2\", baz2 = ART::Route.new \"/baz\"\n    collection2.add collection\n\n    collection2.routes.should eq({\n      \"bar2\" => bar2,\n      \"bar\"  => bar,\n      \"foo2\" => foo2,\n      \"baz2\" => baz2,\n      \"baz\"  => baz,\n      \"foo\"  => foo,\n    })\n  end\n\n  def test_add_with_priority_and_prefix : Nil\n    collection = ART::RouteCollection.new\n    collection.add \"foo\", foo = ART::Route.new(\"/foo\"), 0\n    collection.add \"bar\", bar = ART::Route.new(\"/bar\"), 1\n    collection.add \"baz\", baz = ART::Route.new \"/baz\"\n\n    collection.add_name_prefix \"prefix_\"\n\n    collection.routes.should eq({\n      \"prefix_bar\" => bar,\n      \"prefix_foo\" => foo,\n      \"prefix_baz\" => baz,\n    })\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/route_compiler_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct RouteCompilerTest < ASPEC::TestCase\n  @[DataProvider(\"compiler_provider\")]\n  def test_compile(route : ART::Route, prefix : String, regex : Regex, variables : Set(String), tokens : Array(ART::CompiledRoute::Token)) : Nil\n    compiled_route = route.compile\n    compiled_route.static_prefix.should eq prefix\n    compiled_route.regex.should eq regex\n    compiled_route.variables.should eq variables\n    compiled_route.tokens.should eq tokens\n  end\n\n  def compiler_provider : Hash\n    {\n      \"static\" => {\n        ART::Route.new(\"/foo\"),\n        \"/foo\",\n        /^\\/foo$/,\n        Set(String).new,\n        [\n          ART::CompiledRoute::Token.new(:text, \"/foo\"),\n        ],\n      },\n      \"single variable\" => {\n        ART::Route.new(\"/foo/{bar}\"),\n        \"/foo\",\n        /^\\/foo\\/(?P<bar>[^\\/]++)$/,\n        Set{\"bar\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"bar\"),\n          ART::CompiledRoute::Token.new(:text, \"/foo\"),\n\n        ],\n      },\n      \"variable with default value\" => {\n        ART::Route.new(\"/foo/{bar}\", {\"bar\" => \"bar\"}),\n        \"/foo\",\n        /^\\/foo(?:\\/(?P<bar>[^\\/]++))?$/,\n        Set{\"bar\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"bar\"),\n          ART::CompiledRoute::Token.new(:text, \"/foo\"),\n\n        ],\n      },\n      \"several variable\" => {\n        ART::Route.new(\"/foo/{bar}/{foobar}\"),\n        \"/foo\",\n        /^\\/foo\\/(?P<bar>[^\\/]++)\\/(?P<foobar>[^\\/]++)$/,\n        Set{\"bar\", \"foobar\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"foobar\"),\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"bar\"),\n          ART::CompiledRoute::Token.new(:text, \"/foo\"),\n\n        ],\n      },\n      \"several variables with defaults\" => {\n        ART::Route.new(\"/foo/{bar}/{foobar}\", {\"bar\" => \"bar\", \"foobar\" => \"\"}),\n        \"/foo\",\n        /^\\/foo(?:\\/(?P<bar>[^\\/]++)(?:\\/(?P<foobar>[^\\/]++))?)?$/,\n        Set{\"bar\", \"foobar\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"foobar\"),\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"bar\"),\n          ART::CompiledRoute::Token.new(:text, \"/foo\"),\n\n        ],\n      },\n      \"several variables with some having defaults\" => {\n        ART::Route.new(\"/foo/{bar}/{foobar}\", {\"bar\" => \"bar\"}),\n        \"/foo\",\n        /^\\/foo\\/(?P<bar>[^\\/]++)\\/(?P<foobar>[^\\/]++)$/,\n        Set{\"bar\", \"foobar\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"foobar\"),\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"bar\"),\n          ART::CompiledRoute::Token.new(:text, \"/foo\"),\n\n        ],\n      },\n      \"optional variable as the first segment with default\" => {\n        ART::Route.new(\"/{bar}\", {\"bar\" => \"bar\"}),\n        \"\",\n        /^\\/(?P<bar>[^\\/]++)?$/,\n        Set{\"bar\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"bar\"),\n        ],\n      },\n      \"optional variable as the first segment with requirement\" => {\n        ART::Route.new(\"/{bar}\", {\"bar\" => \"bar\"}, {\"bar\" => /(foo|bar)/}),\n        \"\",\n        /^\\/(?P<bar>(?:foo|bar))?$/,\n        Set{\"bar\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"/\", /(?:foo|bar)/, \"bar\"),\n        ],\n      },\n      \"only optional variables with defaults\" => {\n        ART::Route.new(\"/{foo}/{bar}\", {\"foo\" => \"foo\", \"bar\" => \"bar\"}),\n        \"\",\n        /^\\/(?P<foo>[^\\/]++)?(?:\\/(?P<bar>[^\\/]++))?$/,\n        Set{\"foo\", \"bar\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"bar\"),\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"foo\"),\n        ],\n      },\n      \"variable in last position\" => {\n        ART::Route.new(\"/foo-{bar}\"),\n        \"/foo-\",\n        /^\\/foo\\-(?P<bar>[^\\/]++)$/,\n        Set{\"bar\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"-\", /[^\\/]++/, \"bar\"),\n          ART::CompiledRoute::Token.new(:text, \"/foo\"),\n        ],\n      },\n      \"nested placeholders\" => {\n        ART::Route.new(\"/{static{var}static}\"),\n        \"/{static\",\n        /^\\/\\{static(?P<var>[^\\/]+)static\\}$/,\n        Set{\"var\"},\n        [\n          ART::CompiledRoute::Token.new(:text, \"static}\"),\n          ART::CompiledRoute::Token.new(:variable, \"\", /[^\\/]+/, \"var\"),\n          ART::CompiledRoute::Token.new(:text, \"/{static\"),\n        ],\n      },\n      \"separator between variables\" => {\n        ART::Route.new(\"/{w}{x}{y}{z}.{_format}\", {\"z\" => \"default-z\", \"_format\" => \"html\"}, {\"y\" => /(y|Y)/}),\n        \"\",\n        /^\\/(?P<w>[^\\/\\.]+)(?P<x>[^\\/\\.]+)(?P<y>(?:y|Y))(?:(?P<z>[^\\/\\.]++)(?:\\.(?P<_format>[^\\/]++))?)?$/,\n        Set{\"w\", \"x\", \"y\", \"z\", \"_format\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \".\", /[^\\/]++/, \"_format\"),\n          ART::CompiledRoute::Token.new(:variable, \"\", /[^\\/\\.]++/, \"z\"),\n          ART::CompiledRoute::Token.new(:variable, \"\", /(?:y|Y)/, \"y\"),\n          ART::CompiledRoute::Token.new(:variable, \"\", /[^\\/\\.]+/, \"x\"),\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/\\.]+/, \"w\"),\n        ],\n      },\n      \"with format\" => {\n        ART::Route.new(\"/foo/{bar}.{_format}\"),\n        \"/foo\",\n        /^\\/foo\\/(?P<bar>[^\\/\\.]++)\\.(?P<_format>[^\\/]++)$/,\n        Set{\"bar\", \"_format\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \".\", /[^\\/]++/, \"_format\"),\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/\\.]++/, \"bar\"),\n          ART::CompiledRoute::Token.new(:text, \"/foo\"),\n        ],\n      },\n    }\n  end\n\n  def test_route_with_same_variable_twice : Nil\n    expect_raises ART::Exception::InvalidArgument, \"Route pattern '/{foo}/{foo}' cannot reference variable name 'foo' more than once.\" do\n      ART::Route.new(\"/{foo}/{foo}\").compile\n    end\n  end\n\n  def test_route_with_fragment_as_path_parameter : Nil\n    expect_raises ART::Exception::InvalidArgument, \"Route pattern '/{_fragment}' cannot contain '_fragment' as a path parameter.\" do\n      ART::Route.new(\"/{_fragment}\").compile\n    end\n  end\n\n  def test_route_with_too_long_parameter_name : Nil\n    expect_raises ART::Exception::InvalidArgument, \"Variable name 'abcdefghijklmnopqrstuvqxyz0123456789' cannot be longer than 32 characters in route pattern '/{abcdefghijklmnopqrstuvqxyz0123456789}'.\" do\n      ART::Route.new(\"/{abcdefghijklmnopqrstuvqxyz0123456789}\").compile\n    end\n  end\n\n  @[DataProvider(\"names_starting_with_digit_provider\")]\n  def test_route_with_variable_name_starting_with_digit(name : String) : Nil\n    expect_raises ART::Exception::InvalidArgument, \"Variable name '#{name}' cannot start with a digit in route pattern '/{#{name}}'.\" do\n      ART::Route.new(\"/{#{name}}\").compile\n    end\n  end\n\n  def names_starting_with_digit_provider : Tuple\n    {\n      {\"09\"},\n      {\"123\"},\n      {\"1e2\"},\n    }\n  end\n\n  @[DataProvider(\"capture_group_provider\")]\n  def test_remove_capture_groups(expected : Regex, actual : Regex) : Nil\n    ART::Route.new(\"/{foo}\", requirements: {\"foo\" => actual}).compile.regex.should eq expected\n  end\n\n  def capture_group_provider : Tuple\n    {\n      {\n        /^\\/(?P<foo>a(?:b|c)(?:d|e)f)$/,\n\n        /a(b|c)(d|e)f/,\n      },\n      {\n        /^\\/(?P<foo>a\\(b\\)c)$/,\n        /a\\(b\\)c/,\n      },\n      {\n        /^\\/(?P<foo>(?:b))$/,\n        /(?:b)/,\n      },\n      {\n        /^\\/(?P<foo>(*F))$/,\n        /(*F)/,\n      },\n      {\n        /^\\/(?P<foo>(?:(?:foo)))$/,\n        /((foo))/,\n      },\n    }\n  end\n\n  @[DataProvider(\"compiler_host_data_provider\")]\n  def test_compile_host_data(\n    route : ART::Route,\n    prefix : String,\n    regex : Regex,\n    variables : Set(String),\n    path_variables : Set(String),\n    tokens : Array(ART::CompiledRoute::Token),\n    host_regex : Regex,\n    host_variables : Set(String),\n    host_tokens : Array(ART::CompiledRoute::Token),\n  ) : Nil\n    compiled_route = route.compile\n    compiled_route.static_prefix.should eq prefix\n    compiled_route.regex.should eq regex\n    compiled_route.variables.should eq variables\n    compiled_route.path_variables.should eq path_variables\n    compiled_route.tokens.should eq tokens\n    compiled_route.host_regex.should eq host_regex\n    compiled_route.host_variables.should eq host_variables\n    compiled_route.host_tokens.should eq host_tokens\n  end\n\n  def compiler_host_data_provider : Hash\n    {\n      \"static value\" => {\n        ART::Route.new(\"/hello\", host: \"www.example.com\"),\n        \"/hello\",\n        /^\\/hello$/,\n        Set(String).new,\n        Set(String).new,\n        [\n          ART::CompiledRoute::Token.new(:text, \"/hello\"),\n        ],\n        /^www\\.example\\.com$/i,\n        Set(String).new,\n        [\n          ART::CompiledRoute::Token.new(:text, \"www.example.com\"),\n        ],\n      },\n      \"with variable\" => {\n        ART::Route.new(\"/hello/{name}\", host: \"www.example.{tld}\"),\n        \"/hello\",\n        /^\\/hello\\/(?P<name>[^\\/]++)$/,\n        Set{\"tld\", \"name\"},\n        Set{\"name\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \"/\", /[^\\/]++/, \"name\"),\n          ART::CompiledRoute::Token.new(:text, \"/hello\"),\n        ],\n        /^www\\.example\\.(?P<tld>[^\\.]++)$/i,\n        Set{\"tld\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \".\", /[^\\.]++/, \"tld\"),\n          ART::CompiledRoute::Token.new(:text, \"www.example\"),\n        ],\n      },\n      \"variable at beginning and end\" => {\n        ART::Route.new(\"/hello\", host: \"{locale}.example.{tld}\"),\n        \"/hello\",\n        /^\\/hello$/,\n        Set{\"locale\", \"tld\"},\n        Set(String).new,\n        [\n          ART::CompiledRoute::Token.new(:text, \"/hello\"),\n        ],\n        /^(?P<locale>[^\\.]++)\\.example\\.(?P<tld>[^\\.]++)$/i,\n        Set{\"locale\", \"tld\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \".\", /[^\\.]++/, \"tld\"),\n          ART::CompiledRoute::Token.new(:text, \".example\"),\n          ART::CompiledRoute::Token.new(:variable, \"\", /[^\\.]++/, \"locale\"),\n        ],\n      },\n      \"variable with a default value\" => {\n        ART::Route.new(\"/hello\", {\"locale\" => \"a\", \"tld\" => \"b\"}, host: \"{locale}.example.{tld}\"),\n        \"/hello\",\n        /^\\/hello$/,\n        Set{\"locale\", \"tld\"},\n        Set(String).new,\n        [\n          ART::CompiledRoute::Token.new(:text, \"/hello\"),\n        ],\n        /^(?P<locale>[^\\.]++)\\.example\\.(?P<tld>[^\\.]++)$/i,\n        Set{\"locale\", \"tld\"},\n        [\n          ART::CompiledRoute::Token.new(:variable, \".\", /[^\\.]++/, \"tld\"),\n          ART::CompiledRoute::Token.new(:text, \".example\"),\n          ART::CompiledRoute::Token.new(:variable, \"\", /[^\\.]++/, \"locale\"),\n        ],\n      },\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/route_provider_spec.cr",
    "content": "require \"./spec_helper\"\nrequire \"digest/md5\"\n\nstruct RouteProviderTest < ASPEC::TestCase\n  private COLLECTIONS = [\n    ART::RouteCollection.new,\n    self.default_collection,\n    self.redirection_collection,\n    self.root_prefix_collection,\n    self.head_match_case_collection,\n    self.group_optimized_collection,\n    self.trailing_slash_collection,\n    self.trailing_slash_collection,\n    self.host_tree_collection,\n    self.chunked_collection,\n    self.demo_collection,\n    self.suffix_collection,\n    self.host_collection,\n  ]\n\n  {% begin %}\n    {% for test_case in 0..12 %}\n      def test_compile_{{test_case}} : Nil\n        \\{% begin %}\n          ART::RouteProvider.compile COLLECTIONS[{{test_case}}]\n\n          \\{% data = read_file(\"#{__DIR__}/fixtures/route_provider/route_collection{{test_case}}.cr\").split(\"####\") %}\n\n          ART::RouteProvider.match_host?.should eq (\\{{data[0].id}})\n          ART::RouteProvider.static_routes.should eq (\\{{data[1].id}})\n          ART::RouteProvider.route_regexes.should eq (\\{{data[2].id}})\n          ART::RouteProvider.dynamic_routes.should eq (\\{{data[3].id}})\n          ART::RouteProvider.conditions.size.should eq (\\{{data[4].id}})\n        \\{% end %}\n      end\n    {% end %}\n  {% end %}\n\n  def test_inspect : Nil\n    routes = ART::RouteCollection.new\n    routes.add \"static\", ART::Route.new \"/static\"\n    routes.add \"dynamic\", ART::Route.new \"/user/{id}\"\n\n    ART::RouteProvider.compile routes\n\n    data = ART::RouteProvider.inspect\n\n    data.should contain \"Match Host:  false\"\n    data.should contain \"Static Routes:  {\\\"/static\\\" =>\"\n    data.should contain \"Regexes:  {0 =>\"\n    data.should contain \"Dynamic Routes:  {\\\"21\\\" =>\"\n    data.should contain \"Route Generation Data:  {\\\"static\\\" =>\"\n  end\n\n  def self.default_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    collection.add \"overridden\", ART::Route.new \"/overridden\"\n\n    # Defaults and requirements\n    collection.add \"foo\", ART::Route.new \"/foo/{bar}\", {\"def\" => \"test\"}, {\"bar\" => /baz|athenaa/}\n\n    # Method requirement\n    collection.add \"bar\", ART::Route.new \"/bar/{foo}\", methods: {\"GET\", \"head\"}\n\n    # GET also adds HEAD as valid\n    collection.add \"barhead\", ART::Route.new \"/barhead/{foo}\", methods: {\"GET\"}\n\n    # Simple\n    collection.add \"baz\", ART::Route.new \"/test/baz\"\n\n    # Simple with extension\n    collection.add \"baz2\", ART::Route.new \"/test/baz.html\"\n\n    # Trailing slash\n    collection.add \"baz3\", ART::Route.new \"/test/baz3/\"\n\n    # Trailing slash with variable\n    collection.add \"baz4\", ART::Route.new \"/test/{foo}/\"\n\n    # Trailing slash and method\n    collection.add \"baz5\", ART::Route.new \"/test/{foo}/\", methods: \"post\"\n\n    # Complex name\n    collection.add \"baz.baz6\", ART::Route.new \"/test/{foo}/\", methods: \"put\"\n\n    # Defaults without variable\n    collection.add \"foofoo\", ART::Route.new \"/foofoo\", {\"def\" => \"test\"}\n\n    # Pattern with quotes\n    collection.add \"quoter\", ART::Route.new \"/{quoter}\", requirements: {\"quoter\" => /[']+/}\n\n    # Space in pattern\n    collection.add \"space\", ART::Route.new \"/spa ce\"\n\n    # Prefixes\n    collection1 = ART::RouteCollection.new\n    collection1.add \"overridden\", ART::Route.new \"/overridden1\"\n    collection1.add \"foo1\", ART::Route.new(\"/{foo}\").methods=(\"PUT\")\n    collection1.add \"bar1\", ART::Route.new \"/{bar}\"\n    collection1.add_prefix \"/b\\\"b\"\n    collection2 = ART::RouteCollection.new\n    collection2.add collection1\n    collection2.add \"overridden\", ART::Route.new \"/{var}\", requirements: {\"var\" => /.*/}\n    collection1 = ART::RouteCollection.new\n    collection1.add \"foo2\", ART::Route.new \"/{foo1}\"\n    collection1.add \"bar2\", ART::Route.new \"/{bar1}\"\n    collection1.add_prefix \"/b\\\"b\"\n    collection2.add collection1\n    collection2.add_prefix \"/a\"\n    collection.add collection2\n\n    # Overridden through add (collection) and multiple sub-collections with no own prefix\n    collection1 = ART::RouteCollection.new\n    collection1.add \"overridden2\", ART::Route.new \"/old\"\n    collection1.add \"helloWorld\", ART::Route.new \"/hello/{who}\", {\"who\" => \"World!\"}\n    collection2 = ART::RouteCollection.new\n    collection3 = ART::RouteCollection.new\n    collection3.add \"overridden2\", ART::Route.new \"/new\"\n    collection3.add \"hey\", ART::Route.new \"/hey/\"\n    collection2.add collection3\n    collection1.add collection2\n    collection1.add_prefix \"/multi\"\n    collection.add collection1\n\n    # \"dynamic\" prefix\"\n    collection1 = ART::RouteCollection.new\n    collection1.add \"foo3\", ART::Route.new \"/{foo}\"\n    collection1.add \"bar3\", ART::Route.new \"/{bar}\"\n    collection1.add_prefix \"/b\"\n    collection1.add_prefix \"{_locale}\"\n    collection.add collection1\n\n    # Route between collections\n    collection.add \"ababa\", ART::Route.new \"/ababa\"\n\n    # Collection with static prefix but only one route\"\n    collection1 = ART::RouteCollection.new\n    collection1.add \"foo4\", ART::Route.new \"/{foo}\"\n    collection1.add_prefix \"/aba\"\n    collection.add collection1\n\n    # Prefix and host\n    collection1 = ART::RouteCollection.new\n    collection1.add \"route1\", ART::Route.new \"/route1\", host: \"a.example.com\"\n    collection1.add \"route2\", ART::Route.new \"/c2/route2\", host: \"a.example.com\"\n    collection1.add \"route3\", ART::Route.new \"/c2/route3\", host: \"b.example.com\"\n    collection1.add \"route4\", ART::Route.new \"/route4\", host: \"a.example.com\"\n    collection1.add \"route5\", ART::Route.new \"/route5\", host: \"c.example.com\"\n    collection1.add \"route6\", ART::Route.new \"/route6\", host: nil\n    collection.add collection1\n\n    # Host and variables\n    collection1 = ART::RouteCollection.new\n    collection1.add \"route11\", ART::Route.new \"/route11\", host: \"{var1}.example.com\"\n    collection1.add \"route12\", ART::Route.new \"/route12\", {\"var1\" => \"val\"}, host: \"{var1}.example.com\"\n    collection1.add \"route13\", ART::Route.new \"/route13/{name}\", host: \"{var1}.example.com\"\n    collection1.add \"route14\", ART::Route.new \"/route14/{name}\", {\"var1\" => \"val\"}, host: \"{var1}.example.com\"\n    collection1.add \"route15\", ART::Route.new \"/route15/{name}\", host: \"c.example.com\"\n    collection1.add \"route16\", ART::Route.new \"/route16/{name}\", {\"var1\" => \"val\"}, host: nil\n    collection1.add \"route17\", ART::Route.new \"/route17\", host: nil\n    collection.add collection1\n\n    # Multiple sub-collections with a single route and prefix each\n    collection1 = ART::RouteCollection.new\n    collection1.add \"a\", ART::Route.new \"/a...\"\n    collection2 = ART::RouteCollection.new\n    collection2.add \"b\", ART::Route.new \"/{var}\"\n    collection3 = ART::RouteCollection.new\n    collection3.add \"c\", ART::Route.new \"/{var}\"\n    collection3.add_prefix \"/c\"\n    collection2.add collection3\n    collection2.add_prefix \"/b\"\n    collection1.add collection2\n    collection1.add_prefix \"/a\"\n    collection.add collection1\n\n    collection\n  end\n\n  def self.redirection_collection : ART::RouteCollection\n    collection = self.default_collection.dup\n\n    collection.add \"secure\", ART::Route.new \"/secure\", schemes: \"https\"\n    collection.add \"nonsecure\", ART::Route.new \"/nonsecure\", schemes: \"http\"\n\n    collection\n  end\n\n  def self.root_prefix_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    collection.add \"static\", ART::Route.new \"/test\"\n    collection.add \"dynamic\", ART::Route.new \"/{var}\"\n    collection.add_prefix \"rootprefix\"\n\n    route = ART::Route.new \"/with-condition\"\n    route.condition do |request|\n      \"GET\" == request.method\n    end\n\n    collection.add \"with-condition\", route\n\n    collection\n  end\n\n  def self.head_match_case_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    collection.add \"just_head\", ART::Route.new \"/just_head\", methods: \"HEAD\"\n    collection.add \"head_and_get\", ART::Route.new \"/head_and_get\", methods: {\"HEAD\", \"GET\"}\n    collection.add \"get_and_head\", ART::Route.new \"/get_and_head\", methods: {\"GET\", \"HEAD\"}\n    collection.add \"post_and_head\", ART::Route.new \"/post_and_head\", methods: {\"POST\", \"HEAD\"}\n    collection.add \"put_and_post\", ART::Route.new \"/put_and_post\", methods: {\"PUT\", \"POST\"}\n    collection.add \"put_and_get_and_head\", ART::Route.new \"/put_and_post\", methods: {\"PUT\", \"GET\", \"HEAD\"}\n\n    collection\n  end\n\n  def self.group_optimized_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    collection.add \"a_first\", ART::Route.new \"/a/11\"\n    collection.add \"a_second\", ART::Route.new \"/a/22\"\n    collection.add \"a_third\", ART::Route.new \"/a/33\"\n    collection.add \"a_wildcard\", ART::Route.new \"/{param}\"\n    collection.add \"a_fourth\", ART::Route.new \"/a/44/\"\n    collection.add \"a_fifth\", ART::Route.new \"/a/55/\"\n    collection.add \"a_sixth\", ART::Route.new \"/a/66/\"\n    collection.add \"nested_wildcard\", ART::Route.new \"/nested/{param}\"\n\n    collection.add \"nested_a\", ART::Route.new \"/nested/group/a/\"\n    collection.add \"nested_b\", ART::Route.new \"/nested/group/b/\"\n    collection.add \"nested_c\", ART::Route.new \"/nested/group/c/\"\n\n    collection.add \"slashed_a\", ART::Route.new \"/slashed/group/\"\n    collection.add \"slashed_b\", ART::Route.new \"/slashed/group/b/\"\n    collection.add \"slashed_c\", ART::Route.new \"/slashed/group/c/\"\n\n    collection\n  end\n\n  def self.trailing_slash_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    collection.add \"simple_trailing_slash_no_methods\", ART::Route.new \"/trailing/simple/no-methods/\"\n    collection.add \"simple_trailing_slash_GET_method\", ART::Route.new \"/trailing/simple/get-method/\", methods: \"GET\"\n    collection.add \"simple_trailing_slash_HEAD_method\", ART::Route.new \"/trailing/simple/head-method/\", methods: \"HEAD\"\n    collection.add \"simple_trailing_slash_POST_method\", ART::Route.new \"/trailing/simple/post-method/\", methods: \"POST\"\n    collection.add \"regex_trailing_slash_no_methods\", ART::Route.new \"/trailing/regex/no-methods/{param}/\"\n    collection.add \"regex_trailing_slash_GET_method\", ART::Route.new \"/trailing/regex/get-method/{param}/\", methods: \"GET\"\n    collection.add \"regex_trailing_slash_HEAD_method\", ART::Route.new \"/trailing/regex/head-method/{param}/\", methods: \"HEAD\"\n    collection.add \"regex_trailing_slash_POST_method\", ART::Route.new \"/trailing/regex/post-method/{param}/\", methods: \"POST\"\n\n    collection.add \"simple_not_trailing_slash_no_methods\", ART::Route.new \"/not-trailing/simple/no-methods\"\n    collection.add \"simple_not_trailing_slash_GET_method\", ART::Route.new \"/not-trailing/simple/get-method\", methods: \"GET\"\n    collection.add \"simple_not_trailing_slash_HEAD_method\", ART::Route.new \"/not-trailing/simple/head-method\", methods: \"HEAD\"\n    collection.add \"simple_not_trailing_slash_POST_method\", ART::Route.new \"/not-trailing/simple/post-method\", methods: \"POST\"\n    collection.add \"regex_not_trailing_slash_no_methods\", ART::Route.new \"/not-trailing/regex/no-methods/{param}\"\n    collection.add \"regex_not_trailing_slash_GET_method\", ART::Route.new \"/not-trailing/regex/get-method/{param}\", methods: \"GET\"\n    collection.add \"regex_not_trailing_slash_HEAD_method\", ART::Route.new \"/not-trailing/regex/head-method/{param}\", methods: \"HEAD\"\n    collection.add \"regex_not_trailing_slash_POST_method\", ART::Route.new \"/not-trailing/regex/post-method/{param}\", methods: \"POST\"\n\n    collection\n  end\n\n  def self.host_tree_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    collection.add \"a\", ART::Route.new \"/\", host: \"{d}.e.c.b.a\"\n    collection.add \"b\", ART::Route.new \"/\", host: \"d.c.b.a\"\n    collection.add \"c\", ART::Route.new \"/\", host: \"{e}.e.c.b.a\"\n\n    collection\n  end\n\n  def self.chunked_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    1000.times do |idx|\n      hash = Digest::MD5.hexdigest(idx.to_s)[0...6]\n      collection.add \"_#{idx}\", ART::Route.new \"/#{hash}/{a}/{b}/{c}/#{hash}\"\n    end\n\n    collection\n  end\n\n  def self.demo_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    collection.add \"a\", ART::Route.new \"/admin/post/\"\n    collection.add \"b\", ART::Route.new \"/admin/post/new\"\n    collection.add \"c\", ART::Route.new \"/admin/post/{id}\", requirements: {\"id\" => /\\d+/}\n    collection.add \"d\", ART::Route.new \"/admin/post/{id}/edit\", requirements: {\"id\" => /\\d+/}\n    collection.add \"e\", ART::Route.new \"/admin/post/{id}/delete\", requirements: {\"id\" => /\\d+/}\n\n    collection.add \"f\", ART::Route.new \"/blog/\"\n    collection.add \"g\", ART::Route.new \"/blog/rss.xml\"\n    collection.add \"h\", ART::Route.new \"/blog/page/{page}\", requirements: {\"id\" => /\\d+/}\n    collection.add \"i\", ART::Route.new \"/blog/posts/{page}\", requirements: {\"id\" => /\\d+/}\n    collection.add \"j\", ART::Route.new \"/blog/comments/{id}/new\", requirements: {\"id\" => /\\d+/}\n\n    collection.add \"k\", ART::Route.new \"/blog/search\"\n    collection.add \"l\", ART::Route.new \"/login\"\n    collection.add \"m\", ART::Route.new \"/logout\"\n    collection.add_prefix \"/{_locale}\"\n    collection.add \"n\", ART::Route.new \"/{_locale}\"\n    collection.add_requirements({\"_locale\" => /en|fr/})\n    collection.add_defaults({\"_locale\" => \"en\"})\n\n    collection\n  end\n\n  def self.suffix_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    collection.add \"r1\", ART::Route.new \"abc{foo}/1\"\n    collection.add \"r2\", ART::Route.new \"abc{foo}/2\"\n\n    collection.add \"r10\", ART::Route.new \"abc{foo}/10\"\n    collection.add \"r20\", ART::Route.new \"abc{foo}/20\"\n    collection.add \"r100\", ART::Route.new \"abc{foo}/100\"\n    collection.add \"r200\", ART::Route.new \"abc{foo}/200\"\n\n    collection\n  end\n\n  def self.host_collection : ART::RouteCollection\n    collection = ART::RouteCollection.new\n\n    collection.add \"r1\", ART::Route.new \"abc{foo}\", host: \"{foo}.example.com\"\n    collection.add \"r2\", ART::Route.new \"abc{foo}\", host: \"{foo}.example.com\"\n\n    collection\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/route_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct RouteTest < ASPEC::TestCase\n  def test_constructor : Nil\n    route = ART::Route.new \"/{foo}\", {\"foo\" => \"bar\"}, {\"foo\" => /\\d+/}, host: \"{locale}.example.com\"\n    route.path.should eq \"/{foo}\"\n    route.defaults.should eq({\"foo\" => \"bar\"})\n    route.requirements.should eq({\"foo\" => /\\d+/})\n    route.host.should eq \"{locale}.example.com\"\n\n    route = ART::Route.new \"/\", schemes: {\"Https\"}, methods: {\"POST\", \"put\"}\n    route.schemes.should eq Set{\"https\"}\n    route.methods.should eq Set{\"POST\", \"PUT\"}\n    route.has_scheme?(\"https\").should be_true\n    route.has_scheme?(\"HTTPS\").should be_true\n    route.has_scheme?(\"HTTP\").should be_false\n\n    route = ART::Route.new \"/\", schemes: \"Https\", methods: \"Post\"\n    route.schemes.should eq Set{\"https\"}\n    route.methods.should eq Set{\"POST\"}\n\n    route = ART::Route.new \"/foo\", host: /foo.com/\n    route.host.should eq \"foo.com\"\n    route.host = /bar.net/\n    route.host.should eq \"bar.net\"\n  end\n\n  @[DataProvider(\"path_provider\")]\n  def test_path(path : String, expected : String) : Nil\n    route = ART::Route.new(\"/{foo}\").path = path\n    route.path.should eq expected\n  end\n\n  def path_provider : Hash\n    {\n      \"simple\"                     => {\"/{bar}\", \"/{bar}\"},\n      \"adds missing /\"             => {\"bar\", \"/bar\"},\n      \"defaults to /\"              => {\"\", \"/\"},\n      \"strips leading /\"           => {\"//path\", \"/path\"},\n      \"keeps !\"                    => {\"/path/{!foo}\", \"/path/{!foo}\"},\n      \"strips inline requirements\" => {\"/path/{bar<w++>}\", \"/path/{bar}\"},\n      \"strips inline defaults\"     => {\"/path/{foo?value}\", \"/path/{foo}\"},\n      \"strips all inline settings\" => {\"/path/{!bar<\\\\d+>?value}\", \"/path/{!bar}\"},\n    }\n  end\n\n  def test_defaults : Nil\n    route = ART::Route.new \"/{foo}\"\n    route.defaults = {\"foo\" => \"bar\"}\n    route.defaults.should eq({\"foo\" => \"bar\"})\n\n    route.defaults = Hash(String, String).new\n    route.defaults.should be_empty\n\n    route.set_default \"foo\", \"bar\"\n    route.default(\"foo\").should eq \"bar\"\n\n    route.set_default \"foo2\", \"bar2\"\n    route.default(\"foo2\").should eq \"bar2\"\n    route.default(\"missing\").should be_nil\n\n    route.defaults = {\"foo\" => \"foo\"}\n    route.add_defaults({\"bar\" => \"bar\"})\n    route.defaults.should eq({\"foo\" => \"foo\", \"bar\" => \"bar\"})\n\n    route.has_default?(\"foo\").should be_true\n    route.has_default?(\"missing\").should be_false\n\n    route.defaults = ART::Parameters.new\n    route.defaults.should be_empty\n\n    # Test add_defaults with ART::Parameters\n    params = ART::Parameters.new({\"key1\" => \"value1\", \"key2\" => \"value2\"})\n    route.add_defaults(params)\n    route.defaults.should eq({\"key1\" => \"value1\", \"key2\" => \"value2\"})\n\n    # Test typed default getter\n    route.set_default \"count\", 42\n    route.default(\"count\", Int32).should eq 42\n    route.default(\"missing\", Int32).should be_nil\n    route.default(\"key1\", String).should eq \"value1\"\n  end\n\n  def test_requirements : Nil\n    route = ART::Route.new \"/{foo}\"\n    route.requirements = {\"foo\" => /\\d+/, \"bar\" => \"foo\"}\n    route.requirements.should eq({\"foo\" => /\\d+/, \"bar\" => /foo/})\n\n    route.requirement(\"foo\").should eq /\\d+/\n    route.requirement(\"missing\").should be_nil\n\n    # Removes ^|\\A and $|\\z from the pattern\n    route.requirements = {\"foo\" => /^\\d+$/, \"bar\" => /\\A\\d+\\z/}\n    route.requirements.should eq({\"foo\" => /\\d+/, \"bar\" => /\\d+/})\n\n    route.has_requirement?(\"foo\").should be_true\n    route.has_requirement?(\"missing\").should be_false\n\n    route.requirements = Hash(String, Regex | String).new\n    route.set_requirement \"foo\", \"foo\"\n    route.set_requirement \"bar\", /bar/\n    route.requirements.should eq({\"foo\" => /foo/, \"bar\" => /bar/})\n  end\n\n  def test_compile : Nil\n    route = ART::Route.new \"/{foo}\"\n    compiled_route = route.compile\n    route.compile.should eq compiled_route\n    route.set_requirement \"foo\", /\\d+/\n    route.compile.should_not eq compiled_route\n  end\n\n  @[DataProvider(\"inline_settings_provider\")]\n  def test_inline_defaults_and_requirements(expected : ART::Route, actual : ART::Route) : Nil\n    expected.should eq actual\n  end\n\n  def inline_settings_provider : Tuple\n    {\n      {\n        ART::Route.new(\"/foo/{bar}\").set_default(\"bar\", nil),\n        ART::Route.new(\"/foo/{bar?}\"),\n      },\n      {\n        ART::Route.new(\"/foo/{bar}\").set_default(\"bar\", \"baz\"),\n        ART::Route.new(\"/foo/{bar?baz}\"),\n      },\n      {\n        ART::Route.new(\"/foo/{bar}\").set_default(\"bar\", \"baz<buz>\"),\n        ART::Route.new(\"/foo/{bar?baz<buz>}\"),\n      },\n      {\n        ART::Route.new(\"/foo/{!bar}\").set_default(\"bar\", \"baz<buz>\"),\n        ART::Route.new(\"/foo/{!bar?baz<buz>}\"),\n      },\n      {\n        ART::Route.new(\"/foo/{bar}\").set_default(\"bar\", \"baz\"),\n        ART::Route.new(\"/foo/{bar?}\", {\"bar\" => \"baz\"}),\n      },\n\n      {\n        ART::Route.new(\"/foo/{bar}\").set_requirement(\"bar\", \".*\"),\n        ART::Route.new(\"/foo/{bar<.*>}\"),\n      },\n      {\n        ART::Route.new(\"/foo/{bar}\").set_requirement(\"bar\", \">\"),\n        ART::Route.new(\"/foo/{bar<>>}\"),\n      },\n      {\n        ART::Route.new(\"/foo/{bar}\").set_requirement(\"bar\", /\\d+/),\n        ART::Route.new(\"/foo/{bar<.*>}\", requirements: {\"bar\" => /\\d+/}),\n      },\n      {\n        ART::Route.new(\"/foo/{bar}\").set_requirement(\"bar\", \"[a-z]{2}\"),\n        ART::Route.new(\"/foo/{bar<[a-z]{2}>}\"),\n      },\n      {\n        ART::Route.new(\"/foo/{!bar}\").set_requirement(\"bar\", \"\\\\d+\"),\n        ART::Route.new(\"/foo/{!bar<\\\\d+>}\"),\n      },\n\n      {\n        ART::Route.new(\"/foo/{bar}\").set_default(\"bar\", nil).set_requirement(\"bar\", \".*\"),\n        ART::Route.new(\"/foo/{bar<.*>?}\"),\n      },\n      {\n        ART::Route.new(\"/foo/{bar}\").set_default(\"bar\", \"<>\").set_requirement(\"bar\", \">\"),\n        ART::Route.new(\"/foo/{bar<>>?<>}\"),\n      },\n\n      {\n        ART::Route.new(\"/{foo}/{!bar}\").set_default(\"bar\", \"<>\").set_default(\"foo\", \"\\\\\").set_requirement(\"bar\", /\\\\/).set_requirement(\"foo\", \".\"),\n        ART::Route.new(\"/{foo<.>?\\\\}/{!bar<\\\\>?<>}\"),\n      },\n\n      {\n        ART::Route.new(\"/\").set_default(\"bar\", nil).host=(\"{bar}\"),\n        ART::Route.new(\"/\").host=(\"{bar?}\"),\n      },\n      {\n        ART::Route.new(\"/\").set_default(\"bar\", \"baz\").host=(\"{bar}\"),\n        ART::Route.new(\"/\").host=(\"{bar?baz}\"),\n      },\n      {\n        ART::Route.new(\"/\").set_default(\"bar\", \"baz<buz>\").host=(\"{bar}\"),\n        ART::Route.new(\"/\").host=(\"{bar?baz<buz>}\"),\n      },\n      {\n        ART::Route.new(\"/\").set_default(\"bar\", nil).host=(\"{bar}\"),\n        ART::Route.new(\"/\", {\"bar\" => \"baz\"}).host=(\"{bar?}\"),\n      },\n\n      {\n        ART::Route.new(\"/\").set_requirement(\"bar\", \".*\").host=(\"{bar}\"),\n        ART::Route.new(\"/\").host=(\"{bar<.*>}\"),\n      },\n      {\n        ART::Route.new(\"/\").set_requirement(\"bar\", \">\").host=(\"{bar}\"),\n        ART::Route.new(\"/\").host=(\"{bar<>>}\"),\n      },\n      {\n        ART::Route.new(\"/\").set_requirement(\"bar\", \".*\").host=(\"{bar}\"),\n        ART::Route.new(\"/\", requirements: {\"bar\" => /\\d+/}).host=(\"{bar<.*>}\"),\n      },\n      {\n        ART::Route.new(\"/\").set_requirement(\"bar\", \"[a-z]{2}\").host=(\"{bar}\"),\n        ART::Route.new(\"/\").host=(\"{bar<[a-z]{2}>}\"),\n      },\n\n      {\n        ART::Route.new(\"/\").set_default(\"bar\", nil).set_requirement(\"bar\", \".*\").host=(\"{bar}\"),\n        ART::Route.new(\"/\").host=(\"{bar<.*>?}\"),\n      },\n      {\n        ART::Route.new(\"/\").set_default(\"bar\", \"<>\").set_requirement(\"bar\", \">\").host=(\"{bar}\"),\n        ART::Route.new(\"/\").host=(\"{bar<>>?<>}\"),\n      },\n    }\n  end\n\n  @[DataProvider(\"non_localized_routes_provider\")]\n  def test_locale_default_with_non_localized_routes(route : ART::Route) : Nil\n    route.default(\"_locale\").should_not eq \"fr\"\n    route.set_default \"_locale\", \"fr\"\n    route.default(\"_locale\").should eq \"fr\"\n  end\n\n  @[DataProvider(\"localized_routes_provider\")]\n  def test_locale_default_with_localized_routes(route : ART::Route) : Nil\n    expected = route.default(\"_locale\").should_not be_nil\n    expected.should_not eq \"fr\"\n    route.set_default \"_locale\", \"fr\"\n    route.default(\"_locale\").should eq expected\n  end\n\n  @[DataProvider(\"non_localized_routes_provider\")]\n  def test_locale_requirement_with_non_localized_routes(route : ART::Route) : Nil\n    route.requirement(\"_locale\").should_not eq \"fr\"\n    route.set_requirement \"_locale\", \"fr\"\n    route.requirement(\"_locale\").should eq /fr/\n  end\n\n  @[DataProvider(\"localized_routes_provider\")]\n  def test_locale_requirement_with_localized_routes(route : ART::Route) : Nil\n    expected = route.requirement(\"_locale\").should_not be_nil\n    expected.should_not eq \"fr\"\n    route.set_requirement \"_locale\", \"fr\"\n    route.requirement(\"_locale\").should eq expected\n  end\n\n  def non_localized_routes_provider : Tuple\n    {\n      {ART::Route.new(\"/foo\")},\n      {ART::Route.new(\"/foo\").set_default(\"_locale\", \"en\")},\n      {ART::Route.new(\"/foo\").set_default(\"_locale\", \"en\").set_default(\"_canonical_route\", \"foo\")},\n      {ART::Route.new(\"/foo\").set_default(\"_locale\", \"en\").set_default(\"_canonical_route\", \"foo\").set_requirement(\"_locale\", \"foobar\")},\n    }\n  end\n\n  def localized_routes_provider : Tuple\n    {\n      {ART::Route.new(\"/foo\").set_default(\"_locale\", \"en\").set_default(\"_canonical_route\", \"foo\").set_requirement(\"_locale\", \"en\")},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/router_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct RouterTest < ASPEC::TestCase\n  def test_generate : Nil\n    self.router.generate(\"foo\").should eq \"/foo\"\n    self.router.generate(\"foo\", id: \"1\").should eq \"/foo?id=1\"\n  end\n\n  def test_match : Nil\n    self.router.match(\"/foo\").should eq({\"_route\" => \"foo\"})\n    self.router.match(ART::Request.new \"GET\", \"/bar\").should eq({\"_route\" => \"bar\"})\n  end\n\n  def test_match? : Nil\n    self.router.match?(\"/foo\").should eq({\"_route\" => \"foo\"})\n    self.router.match?(ART::Request.new \"GET\", \"/bar\").should eq({\"_route\" => \"bar\"})\n\n    self.router.match?(\"/baz\").should be_nil\n    self.router.match?(ART::Request.new \"GET\", \"/baz\").should be_nil\n  end\n\n  private def router : ART::Router\n    collection = ART::RouteCollection.new\n    route1 = ART::Route.new \"/foo\"\n    route2 = ART::Route.new \"/bar\"\n\n    collection.add \"foo\", route1\n    collection.add \"bar\", route2\n\n    ART.compile collection\n\n    router = ART::Router.new collection\n\n    router.context = ART::RequestContext.new\n\n    router\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/routing_handler_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate class MockURLMatcher\n  include Athena::Routing::Matcher::RequestMatcherInterface\n  include Athena::Routing::Matcher::URLMatcherInterface\n\n  property context : ART::RequestContext\n\n  def initialize(@route : String, @exception : ::Exception? = nil)\n    @context = ART::RequestContext.new\n  end\n\n  # :inherit:\n  def match(path : String) : ART::Parameters\n    if ex = @exception\n      raise ex\n    end\n\n    params = ART::Parameters.new\n    params[\"_route\"] = @route\n    params\n  end\n\n  def match?(path : String) : ART::Parameters?\n  end\n\n  # :inherit:\n  def match?(@request : ART::Request) : ART::Parameters?\n    self.match? @request.not_nil!.path\n  ensure\n    @request = nil\n  end\n\n  # :inherit:\n  def match(@request : ART::Request) : ART::Parameters\n    self.match @request.not_nil!.path\n  ensure\n    @request = nil\n  end\nend\n\ndescribe ART::RoutingHandler do\n  describe \"#add\" do\n    it \"raises when trying to add another collection\" do\n      expect_raises ArgumentError, \"Cannot add an existing collection to a routing handler.\" do\n        ART::RoutingHandler.new.add ART::RouteCollection.new\n      end\n    end\n\n    it \"captures the provided route\" do\n      handler = ART::RoutingHandler.new\n      handler.add \"a_route\", ART::Route.new \"/foo\"\n      handler.size.should eq 1\n    end\n  end\n\n  describe \"#call\" do\n    describe \"when not bubbling exceptions\" do\n      it \"happy path\" do\n        value = 0\n\n        handler = ART::RoutingHandler.new MockURLMatcher.new \"foo\"\n\n        handler.add \"foo\", ART::Route.new \"/foo\" do |ctx, params|\n          ctx.request.method.should eq \"GET\"\n          ctx.request.path.should eq \"/foo\"\n          value += 10\n          params.to_h.should eq({\"_route\" => \"foo\"})\n        end\n\n        handler.call ::HTTP::Server::Context.new ::HTTP::Request.new(\"GET\", \"/foo\"), ::HTTP::Server::Response.new(IO::Memory.new)\n\n        value.should eq 10\n      end\n\n      it \"missing route\" do\n        handler = ART::RoutingHandler.new MockURLMatcher.new \"foo\", ART::Exception::ResourceNotFound.new \"Missing\"\n        handler.add(\"foo\", ART::Route.new(\"/foo\")) { }\n\n        Log.capture do |logs|\n          handler.call ::HTTP::Server::Context.new ::HTTP::Request.new(\"GET\", \"/foo\"), resp = ::HTTP::Server::Response.new(IO::Memory.new)\n          resp.status.should eq ::HTTP::Status::NOT_FOUND\n\n          logs.empty\n        end\n      end\n\n      it \"unsupported method\" do\n        handler = ART::RoutingHandler.new MockURLMatcher.new \"foo\", ART::Exception::MethodNotAllowed.new [\"PUT\", \"SEARCH\"], \"Not Allowed\"\n        handler.add(\"foo\", ART::Route.new(\"/foo\")) { }\n\n        Log.capture do |logs|\n          handler.call ::HTTP::Server::Context.new ::HTTP::Request.new(\"GET\", \"/foo\"), resp = ::HTTP::Server::Response.new(IO::Memory.new)\n          resp.status.should eq ::HTTP::Status::METHOD_NOT_ALLOWED\n\n          logs.empty\n        end\n      end\n\n      it \"domain exception\" do\n        handler = ART::RoutingHandler.new MockURLMatcher.new \"foo\"\n\n        handler.add \"foo\", ART::Route.new \"/foo\" do |ctx|\n          ctx.request.method.should eq \"GET\"\n          ctx.request.path.should eq \"/foo\"\n          raise \"Oh no!\"\n        end\n\n        Log.capture do |logs|\n          handler.call ::HTTP::Server::Context.new ::HTTP::Request.new(\"GET\", \"/foo\"), resp = ::HTTP::Server::Response.new(IO::Memory.new)\n          resp.status.should eq ::HTTP::Status::INTERNAL_SERVER_ERROR\n\n          logs.check :error, \"Unhandled exception\"\n        end\n      end\n    end\n\n    describe \"when bubbling exceptions\" do\n      it \"happy path\" do\n        value = 0\n\n        handler = ART::RoutingHandler.new MockURLMatcher.new(\"foo\"), bubble_exceptions: true\n\n        handler.add \"foo\", ART::Route.new \"/foo\" do |ctx, params|\n          ctx.request.method.should eq \"GET\"\n          ctx.request.path.should eq \"/foo\"\n          value += 10\n          params.to_h.should eq({\"_route\" => \"foo\"})\n        end\n\n        handler.call ::HTTP::Server::Context.new ::HTTP::Request.new(\"GET\", \"/foo\"), ::HTTP::Server::Response.new(IO::Memory.new)\n\n        value.should eq 10\n      end\n\n      it \"missing route\" do\n        handler = ART::RoutingHandler.new MockURLMatcher.new(\"foo\", ART::Exception::ResourceNotFound.new(\"Missing\")), bubble_exceptions: true\n        handler.add(\"foo\", ART::Route.new(\"/foo\")) { }\n\n        expect_raises ART::Exception::ResourceNotFound do\n          handler.call ::HTTP::Server::Context.new ::HTTP::Request.new(\"GET\", \"/foo\"), ::HTTP::Server::Response.new(IO::Memory.new)\n        end\n      end\n\n      it \"unsupported method\" do\n        handler = ART::RoutingHandler.new MockURLMatcher.new(\"foo\", ART::Exception::MethodNotAllowed.new([\"PUT\", \"SEARCH\"], \"Not Allowed\")), bubble_exceptions: true\n        handler.add(\"foo\", ART::Route.new(\"/foo\")) { }\n\n        ex = expect_raises ART::Exception::MethodNotAllowed do\n          handler.call ::HTTP::Server::Context.new ::HTTP::Request.new(\"GET\", \"/foo\"), ::HTTP::Server::Response.new(IO::Memory.new)\n        end\n\n        ex.allowed_methods.should eq [\"PUT\", \"SEARCH\"]\n      end\n\n      it \"domain exception\" do\n        handler = ART::RoutingHandler.new MockURLMatcher.new(\"foo\"), bubble_exceptions: true\n\n        handler.add \"foo\", ART::Route.new \"/foo\" do |ctx|\n          ctx.request.method.should eq \"GET\"\n          ctx.request.path.should eq \"/foo\"\n          raise \"Oh no!\"\n        end\n        Log.capture do |logs|\n          expect_raises ::Exception, \"Oh no!\" do\n            handler.call ::HTTP::Server::Context.new ::HTTP::Request.new(\"GET\", \"/foo\"), ::HTTP::Server::Response.new(IO::Memory.new)\n          end\n\n          logs.empty\n        end\n      end\n    end\n  end\n\n  describe \"#compile\" do\n    it \"compiles the wrapped collection\" do\n      handler = ART::RoutingHandler.new\n      handler.add \"a_route\", ART::Route.new \"/foo\"\n      handler.compile\n      ART::RoutingHandler::RouteProvider.compiled?.should be_true\n      ART::RoutingHandler::RouteProvider.static_routes.size.should eq 1\n      ART::RouteProvider.compiled?.should be_false\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/routing/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"spec/helpers/iterate\"\nrequire \"athena-spec\"\nrequire \"../src/athena-routing\"\n\nrequire \"log/spec\"\n\nASPEC.run_all\n\nSpec.before_each do\n  ART::RouteProvider.reset\nend\n\nLog.setup :none\n\n# FIXME: Refactor these specs to not depend on calling a protected method.\ninclude Athena::Routing\n"
  },
  {
    "path": "src/components/routing/spec/static_prefix_collection_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct StaticPrefixCollectionTest < ASPEC::TestCase\n  @[DataProvider(\"route_provider\")]\n  def test_grouping(routes : Array(Tuple(String, String)), expected : String) : Nil\n    collection = ART::RouteProvider::StaticPrefixCollection.new \"/\"\n\n    routes.each do |(path, name)|\n      static_prefix = (route = ART::Route.new(path)).compile.static_prefix\n      collection.add_route static_prefix, ART::RouteProvider::StaticPrefixCollection::StaticTreeNamedRoute.new name, route\n    end\n\n    self.dump(collection).should eq expected\n  end\n\n  def route_provider : Hash\n    {\n      \"simple - not nested\" => {\n        [\n          {\"/\", \"root\"},\n          {\"/prefix/segment/\", \"prefix_segment\"},\n          {\"/leading/segment/\", \"leading_segment\"},\n        ],\n        \"root\\nprefix_segment\\nleading_segment\",\n      },\n      \"simple - one level nesting\" => {\n        [\n          {\"/\", \"root\"},\n          {\"/group/segment/\", \"nested_segment\"},\n          {\"/group/thing/\", \"some_segment\"},\n          {\"/group/other/\", \"other_segment\"},\n        ],\n        \"root\\n/group/\\n-> nested_segment\\n-> some_segment\\n-> other_segment\",\n      },\n      \"nested - small group\" => {\n        [\n          {\"/\", \"root\"},\n          {\"/prefix/segment/\", \"prefix_segment\"},\n          {\"/prefix/segment/bb\", \"leading_segment\"},\n        ],\n        \"root\\n/prefix/segment/\\n-> prefix_segment\\n-> leading_segment\",\n      },\n      \"nested - contains item at intersection\" => {\n        [\n          {\"/\", \"root\"},\n          {\"/prefix/segment/\", \"prefix_segment\"},\n          {\"/prefix/segment/bb\", \"leading_segment\"},\n        ],\n        \"root\\n/prefix/segment/\\n-> prefix_segment\\n-> leading_segment\",\n      },\n      \"Retains matching order within groups\" => {\n        [\n          {\"/group/aa/\", \"aa\"},\n          {\"/group/bb/\", \"bb\"},\n          {\"/group/cc/\", \"cc\"},\n          {\"/(.*)\", \"root\"},\n          {\"/group/dd/\", \"dd\"},\n          {\"/group/ee/\", \"ee\"},\n          {\"/group/ff/\", \"ff\"},\n        ],\n        \"/group/\\n-> aa\\n-> bb\\n-> cc\\nroot\\n/group/\\n-> dd\\n-> ee\\n-> ff\",\n      },\n      \"Retains complex matching order with groups at base\" => {\n        [\n          {\"/aaa/111/\", \"first_aaa\"},\n          {\"/prefixed/group/aa/\", \"aa\"},\n          {\"/prefixed/group/bb/\", \"bb\"},\n          {\"/prefixed/group/cc/\", \"cc\"},\n          {\"/prefixed/(.*)\", \"root\"},\n          {\"/prefixed/group/dd/\", \"dd\"},\n          {\"/prefixed/group/ee/\", \"ee\"},\n          {\"/prefixed/\", \"parent\"},\n          {\"/prefixed/group/ff/\", \"ff\"},\n          {\"/aaa/222/\", \"second_aaa\"},\n          {\"/aaa/333/\", \"third_aaa\"},\n        ],\n        \"/aaa/\\n-> first_aaa\\n-> second_aaa\\n-> third_aaa\\n/prefixed/\\n-> /prefixed/group/\\n-> -> aa\\n-> -> bb\\n-> -> cc\\n-> root\\n-> /prefixed/group/\\n-> -> dd\\n-> -> ee\\n-> -> ff\\n-> parent\",\n      },\n      \"Group regardless of segments\" => {\n        [\n          {\"/aaa-111/\", \"a1\"},\n          {\"/aaa-222/\", \"a2\"},\n          {\"/aaa-333/\", \"a3\"},\n          {\"/group-aa/\", \"g1\"},\n          {\"/group-bb/\", \"g2\"},\n          {\"/group-cc/\", \"g3\"},\n        ],\n        \"/aaa-\\n-> a1\\n-> a2\\n-> a3\\n/group-\\n-> g1\\n-> g2\\n-> g3\",\n      },\n    }\n  end\n\n  private def dump(collection : ART::RouteProvider::StaticPrefixCollection, prefix : String = \"\") : String\n    lines = [] of String\n\n    collection.items.each do |item|\n      if item.is_a? ART::RouteProvider::StaticPrefixCollection\n        lines << \"#{prefix}#{item.prefix}\"\n        lines << self.dump(item, \"#{prefix}-> \")\n      else\n        lines << \"#{prefix}#{item.name}\"\n      end\n    end\n\n    lines.join \"\\n\"\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/annotations.cr",
    "content": "# Contains all the `Athena::Routing` based annotations.\n# See `ARTA::Route` for more information.\n#\n# NOTE: These are primarily to define a common type/documentation to use in custom implementations.\n# As of now, they are not leveraged internally, but a future iteration could provide a built in way to resolve them into an `ART::RouteCollection`.\nmodule Athena::Routing::Annotations\n  # Same as `ARTA::Route`, but only matches the `DELETE` method.\n  annotation Delete; end\n\n  # Same as `ARTA::Route`, but only matches the `GET` method.\n  annotation Get; end\n\n  # Same as `ARTA::Route`, but only matches the `HEAD` method.\n  annotation Head; end\n\n  # Same as `ARTA::Route`, but only matches the `LINK` method.\n  annotation Link; end\n\n  # Same as `ARTA::Route`, but only matches the `PATCH` method.\n  annotation Patch; end\n\n  # Same as `ARTA::Route`, but only matches the `POST` method.\n  annotation Post; end\n\n  # Same as `ARTA::Route`, but only matches the `PUT` method.\n  annotation Put; end\n\n  # Annotation representation of an `ART::Route`.\n  # Most commonly this will be applied to a method to define it as the controller for the related route,\n  # but could also be applied to a controller class to apply defaults to all other `ARTA::Route` within it.\n  # Custom implementations may support alternate APIs.\n  # See `ART::Route` for more information.\n  #\n  # ## Configuration\n  #\n  # Various fields can be used within this annotation to control how the route is created.\n  # All fields are optional unless otherwise noted.\n  #\n  # WARNING: Not all fields may be supported by the underlying implementation.\n  #\n  # #### path\n  #\n  # **Type:** `String | Hash(String, String)` - **required**\n  #\n  # The path of the route.\n  #\n  # #### name\n  #\n  # **Type:** `String`\n  #\n  # The unique name of the route. If not provided, a unique name should be created automatically.\n  #\n  # #### requirements\n  #\n  # **Type:** `Hash(String, String | Regex)`\n  #\n  # A `Hash` of patterns that each parameter must match in order for the route to match.\n  #\n  # #### defaults\n  #\n  # **Type:** `Hash(String, _)`\n  #\n  # The values that should be applied to the route parameters if they were not supplied within the request.\n  #\n  # #### host\n  #\n  # **Type:** `String | Regex`\n  #\n  # Require the [host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header to match this value in order for the route to match.\n  #\n  # #### methods\n  #\n  # **Type:** `String | Enumerable(String)`\n  #\n  # A whitelist of the HTTP methods this route supports.\n  #\n  # #### schemes\n  #\n  # **Type:** `String | Enumerable(String)`\n  #\n  # A whitelist of the HTTP schemes this route supports.\n  #\n  # #### condition\n  #\n  # **Type:** `ART::Route::Condition`\n  #\n  # A callback used to dynamically determine if the request matches the route.\n  #\n  # #### priority\n  #\n  # **Type:** `Int32`\n  #\n  # A value used to control the order the routes are registered in.\n  # A higher value means that route will be registered earlier.\n  #\n  # #### locale\n  #\n  # **Type:** `String`\n  #\n  # Allows setting the locale this route supports.\n  # Sets the special `_locale` route parameter.\n  #\n  # #### format\n  #\n  # **Type:** `String`\n  #\n  # Allows setting the format this route supports.\n  # Sets the special `_format` route parameter.\n  #\n  # #### stateless\n  #\n  # **Type:** `Bool`\n  #\n  # If the route should be cached or not.\n  annotation Route; end\n\n  # Same as `ARTA::Route`, but only matches the `UNLINK` method.\n  annotation Unlink; end\nend\n"
  },
  {
    "path": "src/components/routing/src/athena-routing.cr",
    "content": "require \"./ext/regex\"\n\nrequire \"http/request\"\n\nrequire \"./annotations\"\nrequire \"./compiled_route\"\nrequire \"./parameters\"\nrequire \"./request_context\"\nrequire \"./request_context_aware_interface\"\nrequire \"./route\"\nrequire \"./route_collection\"\nrequire \"./route_compiler\"\nrequire \"./route_provider\"\nrequire \"./routing_handler\"\nrequire \"./router\"\n\nrequire \"./exception/*\"\nrequire \"./generator/*\"\nrequire \"./matcher/*\"\nrequire \"./requirement/*\"\n\n# Convenience alias to make referencing `Athena::Routing` types easier.\nalias ART = Athena::Routing\n\n# Convenience alias to make referencing `ART::Annotations` types easier.\nalias ARTA = ART::Annotations\n\n# Provides a performant and robust HTTP based routing library/framework.\nmodule Athena::Routing\n  VERSION = \"0.2.0\"\n\n  {% if @top_level.has_constant?(\"Athena\") && Athena.has_constant?(\"HTTP\") && Athena::HTTP.has_constant?(\"Request\") %}\n    # Represents the type of the *request* parameter within an `ART::Route::Condition`.\n    #\n    # Will be an [AHTTP::Request](/HTTP/Request) instance if used within the Athena Framework, otherwise [HTTP::Request](https://crystal-lang.org/api/HTTP/Request.html).\n    alias Request = Athena::HTTP::Request\n  {% else %}\n    # Represents the type of the *request* parameter within an `ART::Route::Condition`.\n    #\n    # Will be an [AHTTP::Request](/HTTP/Request) instance if used within the Athena Framework, otherwise [HTTP::Request](https://crystal-lang.org/api/HTTP/Request.html).\n    alias Request = ::HTTP::Request\n  {% end %}\n\n  # Includes types related to generating URLs.\n  module Generator; end\n\n  # Includes types related to matching a path/request to a route.\n  module Matcher; end\n\n  # Both acts as a namespace for exceptions related to the `Athena::Routing` component, as well as a way to check for exceptions from the component.\n  module Exception; end\n\n  # Before `ART::Route`s can be matched or generated, they must first be compiled.\n  # This process compiles each route into its `ART::CompiledRoute` representation,\n  # then merges them all together into a more efficient cacheable format.\n  #\n  # A custom *route_provider* type may be provided to compile the routes into a different provider.\n  # By default, the default global `ART::RouteProvider` is used.\n  def self.compile(routes : ART::RouteCollection, *, route_provider : ART::RouteProvider.class = ART::RouteProvider) : Nil\n    route_provider.compile routes\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/compiled_route.cr",
    "content": "# Represents an immutable snapshot of an `ART::Route` that exposes the `Regex` patterns and variables used to match/generate the route.\nstruct Athena::Routing::CompiledRoute\n  # An immutable representation of a segment of a route used to reconstruct a valid URL from an `ART::CompiledRoute`.\n  struct Token\n    # Represents if a `ART::CompiledRoute::Token` is static text, or has a variable portion.\n    enum Type\n      # Static text.\n      TEXT\n\n      # Variable data.\n      VARIABLE\n    end\n\n    # Returns the type this token represents.\n    getter type : Type\n\n    # Returns that static prefix related to this token.\n    getter prefix : String\n\n    # Returns the pattern this `ART::CompiledRoute::Token::Type::VARIABLE` token requires.\n    getter regex : Regex?\n\n    # Returns the name of parameter this `ART::CompiledRoute::Token::Type::VARIABLE` token represents.\n    getter var_name : String?\n\n    # Returns `true` if this token should always be included within the generated URL, otherwise `false`.\n    getter? important : Bool\n\n    def initialize(\n      @type : Type,\n      @prefix : String,\n      @regex : Regex? = nil,\n      @var_name : String? = nil,\n      @important : Bool = false,\n    )\n    end\n\n    # :nodoc:\n    def_clone\n  end\n\n  # Returns the static text prefix of this route.\n  getter static_prefix : String\n\n  # Returns the regex pattern used to match this route.\n  getter regex : Regex\n\n  # Returns the tokens that make up the path of this route.\n  getter tokens : Array(ART::CompiledRoute::Token)\n\n  # Returns the names of the route parameters within this route.\n  getter path_variables : Set(String)\n\n  # Returns the regex pattern used to match the hostname of this route.\n  getter host_regex : Regex?\n\n  # Returns the tokens that make up the hostname of this route.\n  getter host_tokens : Array(ART::CompiledRoute::Token)\n\n  # Returns the names of the route parameters within the hostname pattern this route.\n  getter host_variables : Set(String)\n\n  # Returns the compiled parameter names from the path and hostname patterns.\n  getter variables : Set(String)\n\n  def initialize(\n    @static_prefix : String,\n    @regex : Regex,\n    @tokens : Array(ART::CompiledRoute::Token),\n    @path_variables : Set(String),\n    @host_regex : Regex? = nil,\n    @host_tokens : Array(ART::CompiledRoute::Token) = Array(ART::CompiledRoute::Token).new,\n    @host_variables : Set(String) = Set(String).new,\n    @variables : Set(String) = Set(String).new,\n  )\n  end\n\n  # :nodoc:\n  def_clone\nend\n"
  },
  {
    "path": "src/components/routing/src/exception/invalid_argument.cr",
    "content": "class Athena::Routing::Exception::InvalidArgument < ArgumentError\n  include Athena::Routing::Exception\nend\n"
  },
  {
    "path": "src/components/routing/src/exception/invalid_parameter.cr",
    "content": "class Athena::Routing::Exception::InvalidParameter < ArgumentError\n  include Athena::Routing::Exception\nend\n"
  },
  {
    "path": "src/components/routing/src/exception/method_not_allowed.cr",
    "content": "class Athena::Routing::Exception::MethodNotAllowed < RuntimeError\n  include Athena::Routing::Exception\n\n  getter allowed_methods : Array(String)\n\n  def initialize(allowed_methods : Enumerable(String), message : String? = nil, cause : ::Exception? = nil)\n    @allowed_methods = allowed_methods.map &.upcase\n    super message, cause\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/exception/missing_required_parameters.cr",
    "content": "class Athena::Routing::Exception::MissingRequiredParameters < ArgumentError\n  include Athena::Routing::Exception\nend\n"
  },
  {
    "path": "src/components/routing/src/exception/no_configuration.cr",
    "content": "require \"./resource_not_found\"\n\nclass Athena::Routing::Exception::NoConfiguration < Athena::Routing::Exception::ResourceNotFound\n  include Athena::Routing::Exception\nend\n"
  },
  {
    "path": "src/components/routing/src/exception/resource_not_found.cr",
    "content": "class Athena::Routing::Exception::ResourceNotFound < RuntimeError\n  include Athena::Routing::Exception\nend\n"
  },
  {
    "path": "src/components/routing/src/exception/route_not_found.cr",
    "content": "class Athena::Routing::Exception::RouteNotFound < ArgumentError\n  include Athena::Routing::Exception\nend\n"
  },
  {
    "path": "src/components/routing/src/ext/regex.cr",
    "content": "lib LibPCRE2\n  fun jit_match = pcre2_jit_match_8(code : Code*, subject : UInt8*, length : LibC::SizeT, startoffset : LibC::SizeT, options : UInt32, match_data : MatchData*, mcontext : MatchContext*) : Int\n  fun get_mark = pcre2_get_mark_8(match_data : MatchData*) : UInt8*\nend\n\n# Customizations to stdlib Regex logic to support fast path API and MARK verb\n\nclass Regex\n  def self.fast_path(source : String, options : Options = Options::None)\n    new(_source: source, _options: options, _force_jit: true)\n  end\nend\n\nmodule Regex::PCRE2\n  module MatchData\n    getter mark : String?\n\n    def initialize(\n      @regex : Regex,\n      @code : LibPCRE2::Code*,\n      @string : String,\n      @pos : Int32,\n      @ovector : LibC::SizeT*,\n      @group_size : Int32,\n      @mark : String?,\n    )\n    end\n  end\n\n  @force_jit : Bool = false\n\n  def initialize(*, _source @source : String, _options @options, _force_jit @force_jit : Bool = false)\n    options = pcre2_compile_options(options) | LibPCRE2::UTF | LibPCRE2::DUPNAMES | LibPCRE2::UCP\n    @re = PCRE2.compile(source, options) do |error_message|\n      raise ArgumentError.new(error_message)\n    end\n\n    @jit = jit_compile\n  end\n\n  private def match_data(str, byte_index, options)\n    # TODO: Remove and make 1.19 min supported version\n    match_data = {% if compare_versions(Crystal::VERSION, \"1.19.0-dev\") >= 0 %}\n                   Regex::PCRE2.current_match_data.value\n                 {% else %}\n                   self.match_data\n                 {% end %}\n\n    # CUSTOMIZE - Leverage JIT Fast Path mode if available\n    match_count = if @jit && @force_jit\n                    LibPCRE2.jit_match(@re, str, str.bytesize, byte_index, pcre2_match_options(options), match_data, PCRE2.match_context)\n                  else\n                    LibPCRE2.match(@re, str, str.bytesize, byte_index, pcre2_match_options(options), match_data, PCRE2.match_context)\n                  end\n\n    if match_count < 0\n      case error = LibPCRE2::Error.new(match_count)\n      when .nomatch?\n        return\n      when .badutfoffset?, .utf8_validity?\n        error_message = PCRE2.get_error_message(error)\n        raise ArgumentError.new(\"Regex match error: #{error_message}\")\n      else\n        error_message = PCRE2.get_error_message(error)\n        raise Regex::Error.new(\"Regex match error: #{error_message}\")\n      end\n    end\n\n    match_data\n  end\n\n  private def match_impl(str, byte_index, options)\n    match_data = match_data(str, byte_index, options) || return\n\n    # TODO: Remove and make 1.19 min supported version\n    ovector_count = {% if compare_versions(Crystal::VERSION, \"1.19.0-dev\") >= 0 %}\n                      # We reuse the same `match_data` allocation, so we must reimplement the\n                      # behavior of pcre2_match_data_create_from_pattern (get_ovector_count always\n                      # returns 65535, aka the maximum).\n                      capture_count_impl &+ 1\n                    {% else %}\n                      LibPCRE2.get_ovector_count(match_data)\n                    {% end %}\n    ovector = Slice.new(LibPCRE2.get_ovector_pointer(match_data), ovector_count &* 2)\n\n    # We need to dup the ovector because `match_data` is re-used for subsequent\n    # matches. We only dup the match data (not everything).\n    ovector = ovector.dup\n\n    ::Regex::MatchData.new(\n      self,\n      @re,\n      str,\n      byte_index,\n      ovector.to_unsafe,\n      ovector_count.to_i32 &- 1,\n\n      # CUSTOMIZE - Get MARK verb\n      ((mark = LibPCRE2.get_mark(match_data)) ? String.new(mark) : nil)\n    )\n  end\nend\n\nmodule Athena::Routing\n  protected def self.create_regex(source : String) : ::Regex\n    ::Regex.fast_path source, ::Regex::CompileOptions[:dotall, :dollar_endonly, :no_utf8_check]\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/generator/configurable_requirements_interface.cr",
    "content": "# Represents a URL generator that can be configured whether an exception should be generated when the parameters do not match the requirements.\nmodule Athena::Routing::Generator::ConfigurableRequirementsInterface\n  # Sets how invalid parameters should be treated:\n  #\n  # * `true` - Raise an exception for mismatched requirements.\n  # * `false` - Do not raise an exception, but return an empty string.\n  # * `nil` - Disables checks, returning a URL with possibly invalid parameters.\n  abstract def strict_requirements=(enabled : Bool?)\n\n  # Returns the current strict requirements mode.\n  abstract def strict_requirements? : Bool?\nend\n"
  },
  {
    "path": "src/components/routing/src/generator/interface.cr",
    "content": "# Allows generating a URL for a given `ART::Route`.\n#\n# ```\n# routes = ART::RouteCollection.new\n# routes.add \"blog_show\", ART::Route.new \"/blog/{slug}\"\n#\n# generator = ART::Generator::URLGenerator.new context\n# generator.generate \"blog_show\", slug: \"bar-baz\" # => \"/blog/bar-baz\"\n# ```\n#\n# ## Query Parameters\n#\n# If a parameter passed in via *params* does not map to a known route parameter (path, hostname, etc) it'll be added as a query parameter.\n# For example, using the route defined above:\n#\n# ```\n# generator.generate \"blog_show\", slug: \"bar-baz\", source: \"Crystal\" # => \"/blog/bar-baz?source=Crystal\"\n# ```\n#\n# The special `_query` parameter may be used to explicitly add query parameters.\n# This can be useful when a query parameter may conflict with a route parameter of the same name.\n# For example, given a route like `https://{siteCode}.{domain}/admin/stats`:\n#\n# ```\n# generator\n#   .generate(\n#     \"admin_stats\",\n#     {\n#       \"siteCode\" => \"fr\",\n#       \"domain\"   => \"example.com\",\n#       \"_query\"   => {\n#         \"siteCode\" => \"us\",\n#       },\n#     },\n#   ) # => \"https://fr.example.com/admin/stats?siteCode=us\"\n# ```\n#\n# ## Parameter Default Values\n#\n# By default parameters with a default value the same as the provided parameter will be excluded from the generated URL.\n# For example:\n#\n# ```\n# routes = ART::RouteCollection.new\n# routes.add \"articles\", ART::Route.new \"/articles/{page}\", {\"page\" => \"1\"}\n#\n# ART.compile routes\n#\n# generator = ART::Generator::URLGenerator.new ART::RequestContext.new\n# generator.generate \"articles\"          # => \"/articles\"\n# generator.generate \"articles\", page: 1 # => \"/articles\"\n# generator.generate \"articles\", page: 2 # => \"/articles/2\"\n# ```\n#\n# If you want to always include a parameter, add a `!` before the `ART::Route#path`, for example:\n#\n# ```\n# routes.add \"users\", ART::Route.new \"/users/{!page}\", {\"page\" => \"1\"}\n#\n# generator.generate \"users\"          # => \"/users/1\"\n# generator.generate \"users\", page: 1 # => \"/users/1\"\n# generator.generate \"users\", page: 2 # => \"/users/2\"\n# ```\n#\n# ## URL Types\n#\n# `Athena::Routing` supports various ways to generate the URL, via the *reference_type* parameter.\n# See `ART::Generator::ReferenceType` for description/examples of the possible types.\nmodule Athena::Routing::Generator::Interface\n  include Athena::Routing::RequestContextAwareInterface\n\n  # Generates a URL for the provided *route*, optionally with the provided *params* and *reference_type*.\n  abstract def generate(route : String, params : Hash = Hash(String, String?).new, reference_type : ART::Generator::ReferenceType = :absolute_path) : String\n\n  # :ditto:\n  abstract def generate(route : String, reference_type : ART::Generator::ReferenceType = :absolute_path, **params) : String\nend\n"
  },
  {
    "path": "src/components/routing/src/generator/reference_type.cr",
    "content": "# Represents the type of URLs that are able to be generated via an `ART::Generator::Interface`.\nenum Athena::Routing::Generator::ReferenceType\n  # Includes an absolute URL including protocol, hostname, and path: `https://api.example.com/add/10/5`.\n  ABSOLUTE_URL\n\n  # The default type, includes an absolute path from the root to the generated route: `/add/10/5`.\n  ABSOLUTE_PATH\n\n  # Returns a path relative to the path of the request.\n  # For example:\n  #\n  # ```\n  # routes = ART::RouteCollection.new\n  # routes.add \"one\", ART::Route.new \"/a/b/c/d\"\n  # routes.add \"two\", ART::Route.new \"/a/b/c/\"\n  # routes.add \"three\", ART::Route.new \"/a/b/\"\n  # routes.add \"four\", ART::Route.new \"/a/b/c/other\"\n  # routes.add \"five\", ART::Route.new \"/a/x/y\"\n  #\n  # ART.compile routes\n  #\n  # context = ART::RequestContext.new path: \"/a/b/c/d\"\n  #\n  # generator = ART::Generator::URLGenerator.new context\n  #\n  # generator.generate \"one\", reference_type: :relative_path   # => \"\"\n  # generator.generate \"two\", reference_type: :relative_path   # => \"./\"\n  # generator.generate \"three\", reference_type: :relative_path # => \"../\"\n  # generator.generate \"four\", reference_type: :relative_path  # => \"other\"\n  # generator.generate \"five\", reference_type: :relative_path  # => \"../../x/y\"\n  # ```\n  RELATIVE_PATH\n\n  # Similar to `ABSOLUTE_URL`, but reuses the current protocol: `//api.example.com/add/10/5`.\n  NETWORK_PATH\nend\n"
  },
  {
    "path": "src/components/routing/src/generator/url_generator.cr",
    "content": "# Default implementation of `ART::Generator::Interface`.\nclass Athena::Routing::Generator::URLGenerator\n  include Athena::Routing::Generator::Interface\n  include Athena::Routing::Generator::ConfigurableRequirementsInterface\n\n  # Maps some chars that should be displayed in their raw form and _NOT_ percent encoded, for reasons below.\n  private DECODED_CHARS = {\n    # the slash can be used to designate a hierarchical structure and we want allow using it with this meaning\n    # some webservers don't allow the slash in encoded form in the path for security reasons anyway\n    # see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss\n    \"%2F\"   => \"/\",\n    \"%252F\" => \"%2F\",\n\n    # the following chars are general delimiters in the URI specification but have only special meaning in the authority component\n    # so they can safely be used in the path in unencoded form\n    \"%40\" => \"@\",\n    \"%3A\" => \":\",\n\n    # these chars are only sub-delimiters that have no predefined meaning and can therefore be used literally\n    # so URI producing applications can use these chars to delimit subcomponents in a path segment without being encoded for better readability\n    \"%3B\" => \";\",\n    \"%2C\" => \",\",\n    \"%3D\" => \"=\",\n    \"%2B\" => \"+\",\n    \"%21\" => \"!\",\n    \"%2A\" => \"*\",\n    \"%7C\" => \"|\",\n  }\n\n  private DECODED_QUERY_FRAGMENT_CHARS = {\n    # RFC 3986 explicitly allows those in the query/fragment to reference other URIs unencoded\n    \"%2F\"   => \"/\",\n    \"%252F\" => \"%2F\",\n    \"%3F\"   => \"?\",\n\n    # reserved chars that have no special meaning for HTTP URIs in a query or fragment\n    # this excludes esp. \"&\", \"=\" and also \"+\" because PHP would treat it as a space (form-encoded)\n    \"%40\" => \"@\",\n    \"%3A\" => \":\",\n    \"%21\" => \"!\",\n    \"%3B\" => \";\",\n    \"%2C\" => \",\",\n    \"%2A\" => \"*\",\n  }\n\n  # :inherit:\n  property context : ART::RequestContext\n\n  # :inherit:\n  getter? strict_requirements : Bool? = true\n\n  def initialize(\n    @context : ART::RequestContext,\n    @default_locale : String? = nil,\n    @route_provider : ART::RouteProvider.class = ART::RouteProvider,\n  )\n  end\n\n  def strict_requirements=(enabled : Bool?)\n    @strict_requirements = enabled\n  end\n\n  # :inherit:\n  def generate(route : String, params : Hash = Hash(String, String?).new, reference_type : ART::Generator::ReferenceType = :absolute_path) : String\n    if locale = params[\"_locale\"]? || @context.parameters[\"_locale\"]? || @default_locale\n      if (locale_route = @route_provider.route_generation_data[\"#{route}.#{locale}\"]?) && (route == locale_route[1][\"_canonical_route\"]?)\n        route = \"#{route}.#{locale}\"\n      end\n    end\n\n    unless generation_data = @route_provider.route_generation_data[route]?\n      raise ART::Exception::RouteNotFound.new \"No route with the name '#{route}' exists.\"\n    end\n\n    variables, defaults, requirements, tokens, host_tokens, schemes = generation_data\n\n    if defaults.has_key?(\"_canonical_route\") && defaults.has_key?(\"_locale\")\n      if !variables.includes? \"_locale\"\n        params.delete \"_locale\"\n      elsif !params.has_key?(\"_locale\")\n        params = params.merge({\"_locale\" => defaults[\"_locale\"]?.try(&.to_s)})\n      end\n    end\n\n    self.do_generate variables, defaults, requirements, tokens, params, route, reference_type, host_tokens, schemes\n  end\n\n  # :inherit:\n  def generate(route : String, reference_type : ART::Generator::ReferenceType = :absolute_path, **params) : String\n    self.generate route, params.to_h.transform_keys(&.to_s), reference_type\n  end\n\n  # OPTIMIZE: We could probably make use of `URI` for a lot of this stuff.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def do_generate(\n    variables : Set(String),\n    defaults : ART::Parameters,\n    requirements : Hash(String, Regex),\n    tokens : Array(ART::CompiledRoute::Token),\n    params : Hash,\n    name : String,\n    reference_type : ART::Generator::ReferenceType,\n    host_tokens : Array(ART::CompiledRoute::Token),\n    required_schemes : Set(String)?,\n  ) : String\n    query_parameters = Hash(String, String).new\n\n    if (qp = params[\"_query\"]?).is_a?(Hash)\n      query_parameters = qp.transform_values(&.to_s)\n      params.delete \"_query\"\n    end\n\n    # Normalize params types after handling `_query`\n    params = params.transform_values(&.to_s)\n\n    merged_params = Hash(String, String?).new\n    merged_params.merge! defaults.to_h\n    merged_params.merge! @context.parameters\n    merged_params.merge! params\n\n    unless (missing_params = variables - merged_params.keys).empty?\n      raise ART::Exception::MissingRequiredParameters.new %(Cannot generate URL for route '#{name}'. Missing required parameters: #{missing_params.join(\", \") { |p| \"'#{p}'\" }}.)\n    end\n\n    url = \"\"\n    optional = true\n    message = \"Parameter '%s' for route '%s' must match '%s' (got '%s') to generate the corresponding URL.\"\n    tokens.each do |token|\n      case token.type\n      in .variable?\n        var_name = token.var_name.not_nil!\n        important = token.important?\n\n        if !optional || important || !defaults.has_key?(var_name) || ((mv = merged_params[var_name]?.presence) && mv.to_s != defaults[var_name].to_s)\n          if !@strict_requirements.nil? && (r = token.regex) && !(merged_params[token.var_name]? || \"\").to_s.matches?(/^#{r.source.gsub /\\(\\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\\((?1)\\))*)\\)/, \"\"}$/i)\n            if @strict_requirements\n              raise ART::Exception::InvalidParameter.new message % {var_name, name, r, merged_params[var_name]}\n            end\n\n            # TODO: Add logger integration\n\n            return \"\"\n          end\n\n          url = \"#{token.prefix}#{merged_params[var_name]}#{url}\"\n          optional = false\n        end\n      in .text?\n        url = \"#{token.prefix}#{url}\"\n        optional = false\n      end\n    end\n\n    url = \"/\" if url.empty?\n\n    url = URI.encode_path(url).gsub Regex.union(DECODED_CHARS.keys), DECODED_CHARS\n\n    url = url.gsub Regex.union((hash = {\"/../\" => \"/%2E%2E/\", \"/./\" => \"/%2E/\"}).keys), hash\n\n    if url.ends_with? \"/..\"\n      url = url.sub (-2..-1), \"%2E%2E\"\n    elsif url.ends_with? \"/.\"\n      url = url.sub -1, \"%2E\"\n    end\n\n    scheme_authority = \"\"\n    host = @context.host\n    scheme = @context.scheme\n\n    if required_schemes\n      unless required_schemes.includes? scheme\n        reference_type = ART::Generator::ReferenceType::ABSOLUTE_URL\n        scheme = required_schemes.to_a.first\n      end\n    end\n\n    unless host_tokens.empty?\n      route_host = \"\"\n\n      host_tokens.each do |token|\n        case token.type\n        in .variable?\n          if !@strict_requirements.nil? && (r = token.regex) && !(merged_params[token.var_name]? || \"\").to_s.matches?(/^#{r.source.gsub /\\(\\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\\((?1)\\))*)\\)/, \"\"}$/i)\n            if @strict_requirements\n              raise ART::Exception::InvalidParameter.new message % {token.var_name, name, r, merged_params[token.var_name]}\n            end\n\n            # TODO: Add logger integration\n\n            return \"\"\n          end\n\n          route_host = \"#{token.prefix}#{merged_params[token.var_name]}#{route_host}\"\n        in .text?\n          route_host = \"#{token.prefix}#{route_host}\"\n        end\n      end\n\n      if route_host != host\n        host = route_host\n        reference_type = ART::Generator::ReferenceType::NETWORK_PATH unless reference_type.absolute_url?\n      end\n    end\n\n    if reference_type.absolute_url? || reference_type.network_path?\n      if !host.empty? || (!scheme.in? \"\", \"https\", \"http\")\n        port = \"\"\n\n        if \"http\" == scheme && 80 != @context.http_port\n          port = \":#{@context.http_port}\"\n        elsif \"https\" == scheme && 443 != @context.https_port\n          port = \":#{@context.https_port}\"\n        end\n\n        scheme_authority = reference_type.network_path? || scheme.empty? ? \"//\" : \"#{scheme}://\"\n        scheme_authority = \"#{scheme_authority}#{host}#{port}\"\n      end\n    end\n\n    if reference_type.relative_path?\n      url = if @context.path == url\n              \"\"\n            else\n              URI.new(path: @context.path).relativize(URI.new path: url).to_s\n            end\n    else\n      url = \"#{scheme_authority}#{@context.base_url}#{url}\"\n    end\n\n    extra_params = params.reject { |key, value| variables.includes?(key) || defaults.raw?(key).try(&.to_s) == value }\n    extra_params.merge! query_parameters\n\n    fragment = defaults[\"_fragment\"]? || \"\"\n\n    if frag = extra_params.delete(\"_fragment\")\n      fragment = frag.to_s.presence || \"\"\n    end\n\n    unless extra_params.empty?\n      query = URI::Params.encode(extra_params.transform_values(&.to_s.as(String)).select! { |_, value| value.presence }).gsub Regex.union(DECODED_QUERY_FRAGMENT_CHARS.keys), DECODED_QUERY_FRAGMENT_CHARS\n    end\n\n    if query.presence\n      url = \"#{url}?#{query}\"\n    end\n\n    unless fragment.empty?\n      url = \"#{url}##{URI.encode_path_segment(fragment).gsub Regex.union(DECODED_QUERY_FRAGMENT_CHARS.keys), DECODED_QUERY_FRAGMENT_CHARS}\"\n    end\n\n    url\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/matcher/redirectable_url_matcher_interface.cr",
    "content": "# :nodoc:\nmodule Athena::Routing::Matcher::RedirectableURLMatcherInterface\n  abstract def redirect(path : String, route : String, scheme : String? = nil) : ART::Parameters?\nend\n"
  },
  {
    "path": "src/components/routing/src/matcher/request_matcher_interface.cr",
    "content": "# Similar to `ART::Matcher::URLMatcherInterface`, but tries to match against an `ART::Request`.\nmodule Athena::Routing::Matcher::RequestMatcherInterface\n  # Tries to match the provided *request* to its related route.\n  # Returns an `ART::Parameters` containing the route's defaults and parameters resolved from the *request*.\n  #\n  # Raises an `ART::Exception::ResourceNotFound` if no route could be matched.\n  #\n  # Raises an `ART::Exception::MethodNotAllowed` if a route exists but not for the *request*'s method.\n  abstract def match(request : ART::Request) : ART::Parameters\n\n  # Tries to match the provided *request* to its related route.\n  # Returns an `ART::Parameters` containing the route's defaults and parameters resolved from the *request*.\n  #\n  # Returns `nil` if no route could be matched or a route exists but not for the *request*'s method.\n  abstract def match?(request : ART::Request) : ART::Parameters?\nend\n"
  },
  {
    "path": "src/components/routing/src/matcher/traceable_url_matcher.cr",
    "content": "require \"./url_matcher\"\n\n# Extension of `ART::Matcher::URLMatcher` to assist with debugging by tracing the match.\n#\n# See `#traces`.\nclass Athena::Routing::Matcher::TraceableURLMatcher < Athena::Routing::Matcher::URLMatcher\n  # Represents the match level of a `ART::Matcher::TraceableURLMatcher::Trace`.\n  enum Match\n    # The route did not match at all.\n    NONE\n\n    # The route matched, but not fully.\n    PARTIAL\n\n    # The route is a match.\n    FULL\n  end\n\n  record Trace, message : String, level : ART::Matcher::TraceableURLMatcher::Match, name : String, route : ART::Route\n\n  @traces = Array(Trace).new\n\n  def initialize(\n    @routes : ART::RouteCollection,\n    context : ART::RequestContext,\n    route_provider : ART::RouteProvider.class = ART::RouteProvider,\n  )\n    super context, route_provider\n  end\n\n  # Returns an array of `ART::Matcher::TraceableURLMatcher::Trace` representing the history of the matching logic when trying to match the provided *request*.\n  def traces(@request : ART::Request) : Array(ART::Matcher::TraceableURLMatcher::Trace)\n    self.traces @request.not_nil!.path\n  ensure\n    @request = nil\n  end\n\n  # Returns an array of `ART::Matcher::TraceableURLMatcher::Trace` representing the history of the matching logic when trying to match the provided *path*.\n  def traces(path : String) : Array(ART::Matcher::TraceableURLMatcher::Trace)\n    @traces.clear\n\n    begin\n      self.match path\n    rescue ex : ::Exception\n      raise ex unless ex.is_a? ART::Exception\n    end\n\n    @traces\n  end\n\n  # :inherit:\n  def match(path : String) : ART::Parameters\n    allow = Array(String).new\n    allow_schemes = Array(String).new\n\n    if match = self.match_collection (URI.decode(path).presence || \"/\"), allow, allow_schemes, @routes\n      return match\n    end\n\n    if \"/\" == path && allow.empty? && allow_schemes.empty?\n      raise ART::Exception::NoConfiguration.new\n    end\n\n    unless allow.empty?\n      raise ART::Exception::MethodNotAllowed.new allow\n    end\n\n    raise ART::Exception::ResourceNotFound.new \"No routes found for '#{path}'.\"\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def match_collection(path : String, allow : Array(String), allow_schemes : Array(String), routes : ART::RouteCollection) : ART::Parameters?\n    method = @context.method\n    method = \"GET\" if \"HEAD\" == method\n\n    supports_trailing_slash = false # TODO: Support this\n\n    trimmed_path = path.rstrip('/').presence || \"/\"\n\n    routes.each do |name, route|\n      compiled_route = route.compile\n      static_prefix = compiled_route.static_prefix.rstrip '/'\n      required_methods = route.methods\n\n      if !static_prefix.empty? && !trimmed_path.starts_with? static_prefix\n        @traces << Trace.new \"Path '#{route.path}' does not match\", :none, name, route\n\n        next\n      end\n\n      regex_source = compiled_route.regex.source\n\n      # Use rindex since we want the right most ending `$`\n      pos = regex_source.rindex!('$')\n      has_trailing_slash = '/' == regex_source[pos - 1]\n\n      # Enable multiline mode to catch paths with new lines\n      regex = ART.create_regex regex_source\n\n      unless match = regex.match path\n        # Does it match w/o any requirements?\n        r = ART::Route.new path: route.path, defaults: route.defaults.to_h\n        cr = r.compile\n\n        unless cr.regex.matches? path\n          @traces << Trace.new \"Path '#{route.path}' does not match\", :none, name, route\n\n          next\n        end\n\n        route.requirements.each do |k, pattern|\n          r = ART::Route.new path: route.path, defaults: route.defaults.to_h, requirements: {k => pattern}\n          cr = r.compile\n\n          if cr.variables.includes?(k) && !path.matches?(cr.regex)\n            @traces << Trace.new \"Requirement for '#{k}' does not match (#{pattern.source})\", :partial, name, route\n\n            break\n          end\n        end\n\n        next\n      end\n\n      has_trailing_var = trimmed_path != path && route.path.matches?(/\\{[\\w\\x80-\\xFF]+\\}\\/?$/)\n\n      if has_trailing_var &&\n         (has_trailing_slash || ((n = match[compiled_route.path_variables.size]?).nil?) || ('/' != (n.try &.[-1]? || '/'))) &&\n         (sub_match = regex.match(trimmed_path))\n        if has_trailing_slash\n          match = sub_match\n        else\n          has_trailing_var = false\n        end\n      end\n\n      if (host_pattern = compiled_route.host_regex) && !(host_match = host_pattern.match @context.host)\n        @traces << Trace.new \"Host '#{@context.host}' does not match the requirement ('#{route.host}')\", :partial, name, route\n\n        next\n      end\n\n      attributes = self.get_attributes route, name, host_match ? match.to_h.merge(host_match.to_h) : match.to_h\n      condition_match = self.handle_route_requirements path, name, route, attributes\n\n      unless condition_match\n        @traces << Trace.new \"Route condition for '#{name}' does not evaluate to 'true'\", :partial, name, route\n\n        next\n      end\n\n      if \"/\" != path && !has_trailing_var && has_trailing_slash == (trimmed_path == path)\n        if supports_trailing_slash && (required_methods && (required_methods.empty? || required_methods.includes? \"GET\"))\n          @traces << Trace.new \"Route matches!\", :full, name, route\n\n          return\n        end\n\n        @traces << Trace.new \"Path '#{route.path}' does not match\", :none, name, route\n        next\n      end\n\n      if (schemes = route.schemes) && !route.has_scheme?(@context.scheme)\n        allow_schemes.concat schemes\n        @traces << Trace.new \"Scheme '#{@context.scheme}' does not match any of the required schemes (#{schemes.join \", \"})\", :partial, name, route\n        next\n      end\n\n      if required_methods && !required_methods.includes? method\n        allow.concat required_methods\n        @traces << Trace.new \"Method '#{@context.method}' does not match any of the required methods (#{required_methods.join \", \"})\", :partial, name, route\n        next\n      end\n\n      @traces << Trace.new \"Route matches!\", :full, name, route\n\n      return attributes\n    end\n  end\n\n  private def get_attributes(route : ART::Route, name : String, attributes : Hash(String | Int32, String?)) : ART::Parameters\n    defaults = route.defaults.dup\n\n    if canonical_route = defaults[\"_canonical_route\"]?\n      name = canonical_route\n      defaults.delete \"_canonical_route\"\n    end\n\n    defaults[\"_route\"] = name\n\n    self.merge_defaults attributes, defaults\n  end\n\n  private def merge_defaults(params : Hash(String | Int32, String?), defaults : ART::Parameters) : ART::Parameters\n    params.each do |k, v|\n      if !k.is_a?(Int) && !v.nil?\n        defaults[k] = v\n      end\n    end\n\n    defaults\n  end\n\n  private def handle_route_requirements(path : String, name : String, route : ART::Route, attributes : ART::Parameters) : Bool\n    if (condition = route.condition) && !condition.call(@context, @request || self.build_request(path))\n      return false\n    end\n\n    true\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/matcher/url_matcher.cr",
    "content": "require \"./url_matcher_interface\"\n\n# Default implementation of `ART::Matcher::RequestMatcherInterface` and `ART::Matcher::URLMatcherInterface`.\nclass Athena::Routing::Matcher::URLMatcher\n  include Athena::Routing::Matcher::RequestMatcherInterface\n  include Athena::Routing::Matcher::URLMatcherInterface\n\n  property context : ART::RequestContext\n\n  @request : ART::Request? = nil\n\n  def initialize(\n    @context : ART::RequestContext,\n    @route_provider : ART::RouteProvider.class = ART::RouteProvider,\n  ); end\n\n  # :inherit:\n  def match(@request : ART::Request) : ART::Parameters\n    self.match @request.not_nil!.path\n  ensure\n    @request = nil\n  end\n\n  # :inherit:\n  def match?(@request : ART::Request) : ART::Parameters?\n    self.match? @request.not_nil!.path\n  ensure\n    @request = nil\n  end\n\n  # :inherit:\n  def match(path : String) : ART::Parameters\n    allow = Array(String).new\n    allow_schemes = Array(String).new\n\n    if match = self.do_match path, allow, allow_schemes\n      return match\n    end\n\n    unless allow.empty?\n      raise ART::Exception::MethodNotAllowed.new allow\n    end\n\n    unless self.is_a? ART::Matcher::RedirectableURLMatcherInterface\n      raise ART::Exception::ResourceNotFound.new \"No routes found for '#{path}'.\"\n    end\n\n    if !@context.method.in? \"GET\", \"HEAD\"\n      # no-op\n    elsif !allow_schemes.empty?\n      redirect_schema\n    elsif \"/\" != (trimmed_path = (path.rstrip('/').presence || \"/\"))\n      path = trimmed_path == path ? \"#{path}/\" : trimmed_path\n\n      if match = self.do_match path, allow, allow_schemes\n        return match.merge! self.redirect(path, match[\"_route\"])\n      end\n\n      unless allow_schemes.empty?\n        redirect_schema\n      end\n    end\n\n    raise ART::Exception::ResourceNotFound.new \"No routes found for '#{path}'.\"\n  end\n\n  # :inherit:\n  def match?(path : String) : ART::Parameters?\n    self.do_match path, Array(String).new, Array(String).new\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def do_match(path : String, allow : Array(String) = [] of String, allow_schemes : Array(String) = [] of String) : ART::Parameters?\n    allow.clear\n    allow_schemes.clear\n\n    path = URI.decode(path).presence || \"/\"\n    path = path.presence || \"/\"\n    trimmed_path = path.rstrip('/').presence || \"/\"\n    request_method = canonical_method = @context.method\n\n    host = @context.host.downcase if @route_provider.match_host?\n\n    canonical_method = \"GET\" if \"HEAD\" == request_method\n\n    supports_redirect = \"GET\" == canonical_method && self.is_a? ART::Matcher::RedirectableURLMatcherInterface\n\n    @route_provider.static_routes[trimmed_path]?.try &.each do |data, required_host, required_methods, required_schemes, has_trailing_slash, _, condition|\n      if condition && !(@route_provider.conditions[condition].call(@context, @request || self.build_request(path)))\n        next\n      end\n\n      # Dup the data hash so we don't mutate the original.\n      data = data.dup\n\n      if h = required_host\n        case h\n        in String then next if h != host\n        in Regex\n          if match = host.try &.match h\n            host_matches = match.named_captures\n            host_matches[\"_route\"] = data[\"_route\"]\n\n            host_matches.each do |key, value|\n              data[key] = value unless value.nil?\n            end\n          else\n            next\n          end\n        end\n      end\n\n      if \"/\" != path && has_trailing_slash == (trimmed_path == path)\n        if supports_redirect && (!required_methods || (required_methods.empty? || required_methods.includes? \"GET\"))\n          allow.clear\n          allow_schemes.clear\n\n          return\n        end\n\n        next\n      end\n\n      # TODO: Check schemas\n      has_required_scheme = required_schemes.nil? || required_schemes.includes? @context.scheme\n      if has_required_scheme && required_methods && !required_methods.includes?(canonical_method) && !required_methods.includes?(request_method)\n        allow.concat required_methods\n        next\n      end\n\n      if !has_required_scheme\n        required_schemes.try do |schemes|\n          allow_schemes.concat schemes\n        end\n        next\n      end\n\n      return data\n    end\n\n    matched_path = @route_provider.match_host? ? \"#{host}.#{path}\" : path\n\n    @route_provider.route_regexes.each do |offset, regex|\n      while match = regex.match matched_path\n        @route_provider.dynamic_routes[matched_mark = match.mark.not_nil!]?.try &.each do |data, vars, required_methods, required_schemes, has_trailing_slash, has_trailing_var, condition|\n          # Dup the data hash so we don't mutate the original.\n          data = data.dup\n\n          if condition && !(@route_provider.conditions[condition].call(@context, @request || self.build_request(path)))\n            next\n          end\n\n          has_trailing_var = trimmed_path != path && has_trailing_var\n\n          if has_trailing_var &&\n             (has_trailing_slash || (!vars || (n = match[vars.size]?).nil?) || ('/' != (n.try &.[-1]? || '/'))) &&\n             (sub_match = regex.match(@route_provider.match_host? ? \"#{host}.#{trimmed_path}\" : trimmed_path)) && (matched_mark == sub_match.mark.not_nil!)\n            if has_trailing_slash\n              match = sub_match\n            else\n              has_trailing_var = false\n            end\n          end\n\n          if \"/\" != path && !has_trailing_var && has_trailing_slash == (trimmed_path == path)\n            if supports_redirect && (!required_methods || (required_methods.empty? || required_methods.includes? \"GET\"))\n              allow.clear\n              allow_schemes.clear\n\n              return\n            end\n\n            next\n          end\n\n          vars.try &.each_with_index do |var, idx|\n            if m = match[idx + 1]?\n              data[var] = m\n            end\n          end\n\n          if required_schemes && required_schemes.includes? @context.scheme\n            allow_schemes.concat required_schemes\n            next\n          end\n\n          if required_methods && !required_methods.includes?(canonical_method) && !required_methods.includes?(request_method)\n            allow.concat required_methods\n            next\n          end\n\n          return data\n        end\n\n        regex = ART.create_regex regex.source.sub \"(*:#{matched_mark})\", \"(*F)\"\n        offset += matched_mark.size\n      end\n    end\n\n    if \"/\" == path && allow.empty? && allow_schemes.empty?\n      raise ART::Exception::NoConfiguration.new\n    end\n\n    nil\n  end\n\n  private def build_request(path : String) : ART::Request\n    request = ::HTTP::Request.new(\n      @context.method,\n      \"#{@context.base_url}#{path}\",\n      headers: ::HTTP::Headers{\n        \"host\" => %(#{@context.host}:#{\"http\" == @context.scheme ? @context.http_port : @context.https_port}),\n      }\n    )\n\n    {% if @top_level.has_constant?(\"Athena\") && Athena.has_constant?(\"HTTP\") && Athena::HTTP.has_constant?(\"Request\") %}\n      request = Athena::HTTP::Request.new request\n    {% end %}\n\n    request\n  end\n\n  private macro redirect_schema\n    scheme = @context.scheme\n    @context.scheme = allow_schemes.last? || \"\"\n\n    begin\n      if match = self.do_match path\n        return match.merge! self.redirect(path, match[\"_route\"], @context.scheme)\n      end\n    ensure\n      @context.scheme = scheme\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/matcher/url_matcher_interface.cr",
    "content": "# Allows matching a request path, or `ART::Request` in the case of `ART::Matcher::RequestMatcherInterface`, to its related route.\n#\n# ```\n# # Create a new route collection and add a route with a single parameter to it.\n# routes = ART::RouteCollection.new\n# routes.add \"blog_show\", ART::Route.new \"/blog/{slug}\"\n#\n# # Compile the routes.\n# ART.compile routes\n#\n# # Represents the request in an agnostic data format.\n# # In practice this would be created from the current `ART::Request`.\n# context = ART::RequestContext.new\n#\n# # Match a request by path.\n# matcher = ART::Matcher::URLMatcher.new context\n# matcher.match \"/blog/foo-bar\" # => ART::Parameters{\"_route\" => \"blog_show\", \"slug\" => \"foo-bar\"}\n# ```\nmodule Athena::Routing::Matcher::URLMatcherInterface\n  include Athena::Routing::RequestContextAwareInterface\n\n  # Tries to match the provided *path* to its related route.\n  # Returns an `ART::Parameters` containing the route's defaults and parameters resolved from the *path*.\n  #\n  # Raises an `ART::Exception::ResourceNotFound` if no route could be matched.\n  #\n  # Raises an `ART::Exception::MethodNotAllowed` if a route exists but not for the current HTTP method.\n  abstract def match(path : String) : ART::Parameters\n\n  # Tries to match the provided *path* to its related route.\n  # Returns an `ART::Parameters` containing the route's defaults and parameters resolved from the *path*.\n  #\n  # Returns `nil` if no route could be matched or a route exists but not for the current HTTP method.\n  abstract def match?(path : String) : ART::Parameters?\nend\n"
  },
  {
    "path": "src/components/routing/src/parameters.cr",
    "content": "# A container representing parameters defined via `ART::Route#defaults`, or returned when matching a route.\n# Allows the value to be of any type.\nclass Athena::Routing::Parameters\n  private abstract struct Param\n    abstract def value\n    abstract def type_name : String\n\n    def inspect(io : IO) : Nil\n      if self.value.is_a?(String | Bool | Number::Primitive)\n        return self.value.inspect io\n      end\n\n      io << \"#<Param(\" << self.type_name << \")>\"\n    end\n  end\n\n  private record Parameter(T) < Param, value : T do\n    def type_name : String\n      {{ T.stringify }}\n    end\n  end\n\n  @parameters : Hash(String, Param) = Hash(String, Param).new\n\n  def initialize\n  end\n\n  def self.new(parameters : self) : self\n    parameters\n  end\n\n  def self.new(hash : Hash(String, _))\n    params = new\n    hash.each do |key, value|\n      params[key] = value\n    end\n    params\n  end\n\n  # Returns `true` if a parameter with the provided *name* exists, otherwise `false`.\n  def has_key?(name : String) : Bool\n    @parameters.has_key? name\n  end\n\n  # Returns the value of the parameter with the provided *name* as a `String`.\n  #\n  # Raises a `KeyError` if no parameter with that name exists.\n  def [](name : String) : String\n    @parameters.fetch(name) { raise KeyError.new \"No parameter exists with the name '#{name}'.\" }.value.as(String)\n  end\n\n  # Returns the value of the parameter with the provided *name* as a `String` if it exists, otherwise `nil`.\n  def []?(name : String) : String?\n    @parameters[name]?.try &.value.as?(String)\n  end\n\n  # Returns the value of the parameter with the provided *name* casted to the provided *type* if it exists, otherwise `nil`.\n  def get?(name : String, type : T.class) : T? forall T\n    @parameters[name]?.try &.value.as?(T)\n  end\n\n  # Returns the value of the parameter with the provided *name*, casted to the provided *type*.\n  #\n  # Raises a `KeyError` if no parameter with that name exists.\n  def get(name : String, type : T.class) : T forall T\n    @parameters.fetch(name) { raise KeyError.new \"No parameter exists with the name '#{name}'.\" }.value.as(T)\n  end\n\n  # Sets a parameter with the provided *name* to *value*.\n  def []=(name : String, value : T) : Nil forall T\n    @parameters[name] = Parameter(T).new value\n  end\n\n  # Removes the parameter with the provided *name*.\n  def delete(name : String) : Nil\n    @parameters.delete name\n  end\n\n  # :nodoc:\n  def raw?(name : String)\n    @parameters[name]?.try &.value\n  end\n\n  # :nodoc:\n  def keys : Array(String)\n    @parameters.keys\n  end\n\n  # Returns `true` if empty.\n  def empty? : Bool\n    @parameters.empty?\n  end\n\n  # :nodoc:\n  def size : Int32\n    @parameters.size\n  end\n\n  # :nodoc:\n  def each(&) : Nil\n    @parameters.each do |key, param|\n      yield key, param.value\n    end\n  end\n\n  # :nodoc:\n  def dup : self\n    copy = self.class.new\n    @parameters.each do |key, param|\n      copy.@parameters[key] = param\n    end\n    copy\n  end\n\n  # :nodoc:\n  def clone : self\n    self.dup\n  end\n\n  def merge!(other : ART::Parameters) : self\n    other.@parameters.each do |key, param|\n      @parameters[key] = param\n    end\n    self\n  end\n\n  # :nodoc:\n  def merge!(other : ART::Parameters?) : self\n    if other\n      other.@parameters.each do |key, param|\n        @parameters[key] = param\n      end\n    end\n    self\n  end\n\n  # Returns a `Hash(String, String?)` representation of these parameters.\n  # Values that are not `String?` are converted via `#to_s`.\n  def to_h : Hash(String, String?)\n    @parameters.to_h do |(key, param)|\n      value = param.value\n      {key, value.nil? ? nil : value.to_s}\n    end\n  end\n\n  # :nodoc:\n  def ==(other : self) : Bool\n    return false unless @parameters.size == other.@parameters.size\n    @parameters.each do |key, param|\n      return false unless other.@parameters[key]?.try { |p| p.value == param.value }\n    end\n    true\n  end\n\n  # :nodoc:\n  def ==(other : Hash(String, String?)) : Bool\n    self.to_h == other\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/request_context.cr",
    "content": "# Represents data from a request in an agnostic manner, primarily used to augment URL matching and generation with additional context.\nclass Athena::Routing::RequestContext\n  # Represents the path of the URL _before_ `#path`.\n  # E.g. a path that should be prefixed to all other `#path`s.\n  getter base_url : String\n\n  getter method : String\n  getter path : String\n  getter host : String\n  getter scheme : String\n  getter http_port : Int32\n  getter https_port : Int32\n\n  # Returns the query string of the current request.\n  getter query_string : String\n\n  # Returns the global parameters that should be used as part of the URL generation logic.\n  getter parameters : Hash(String, String?) = Hash(String, String?).new\n\n  # Creates a new instance of self from the provided *uri*.\n  # The *host*, *scheme*, *http_port*, and *https_port* optionally act as fallbacks if they are not contained within the *uri*.\n  #\n  # ameba:disable Metrics/CyclomaticComplexity:\n  def self.from_uri(uri : String, host : String = \"localhost\", scheme : String = \"http\", http_port : Int32 = 80, https_port : Int32 = 443) : self\n    if (idx = uri.index('\\\\')) && (i = uri.index(/\\?|\\#/) || uri.bytesize) && idx < i\n      uri = \"\"\n    end\n\n    if (u = uri.presence) && (u[0].ord <= 32 || u[-1].ord <= 32 || ((idx = u.index(/\\r|\\n|\\t/) || u.bytesize) && (u.bytesize != idx)))\n      uri = \"\"\n    end\n\n    self.from_uri URI.parse(uri), host, scheme, http_port, https_port\n  end\n\n  # :ditto:\n  def self.from_uri(uri : URI, host : String = \"localhost\", scheme : String = \"http\", http_port : Int32 = 80, https_port : Int32 = 443) : self\n    scheme = uri.scheme || scheme\n\n    if port = uri.port\n      if \"http\" == scheme\n        http_port = port\n      elsif \"https\" == scheme\n        https_port = port\n      end\n    end\n\n    new(\n      uri.path,\n      \"GET\",\n      uri.hostname || host,\n      scheme,\n      http_port,\n      https_port\n    )\n  end\n\n  def initialize(\n    @base_url : String = \"\",\n    @method : String = \"GET\",\n    @host : String = \"localhost\",\n    @scheme : String = \"http\",\n    @http_port : Int32 = 80,\n    @https_port : Int32 = 443,\n    @path : String = \"/\",\n    @query_string : String = \"\",\n  )\n    self.method = @method\n    self.host = @host\n    self.scheme = @scheme\n  end\n\n  # Updates the properties within `self` based on the provided *request*.\n  def apply(request : ART::Request) : self\n    self.method = request.method\n\n    self.host = if (h = request.hostname) && (h != \"localhost\")\n                  h\n                elsif h = @host\n                  h\n                else\n                  \"localhost\"\n                end\n\n    self.path = request.path\n    self.query_string = request.query || \"\"\n\n    # TODO: Support this once it's exposed.\n    # self.scheme = request.scheme\n\n    self\n  end\n\n  def base_url=(@base_url : String) : self\n    self\n  end\n\n  def path=(@path : String) : self\n    self\n  end\n\n  def method=(method : String) : self\n    @method = method.upcase\n\n    self\n  end\n\n  def host=(host : String) : self\n    @host = host.downcase\n\n    self\n  end\n\n  def scheme=(scheme : String) : self\n    @scheme = scheme.downcase\n\n    self\n  end\n\n  def query_string=(query_string : String?) : self\n    @query_string = query_string.to_s\n\n    self\n  end\n\n  def http_port=(@http_port : Int32) : self\n    self\n  end\n\n  def https_port=(@https_port : Int32) : self\n    self\n  end\n\n  def parameter(name : String)\n    @parameters[name]\n  end\n\n  def set_parameter(name : String, value : String?) : self\n    @parameters[name] = value\n\n    self\n  end\n\n  def parameters=(@parameters : Hash(String, String?)) : self\n    self\n  end\n\n  def has_parameter?(name : String) : Bool\n    @parameters.has_key? name\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/request_context_aware_interface.cr",
    "content": "# Represents a type that has access to the current `ART::RequestContext`.\nmodule Athena::Routing::RequestContextAwareInterface\n  # Returns the request context.\n  abstract def context : ART::RequestContext\n\n  # Sets the request context.\n  abstract def context=(context : ART::RequestContext)\nend\n"
  },
  {
    "path": "src/components/routing/src/requirement/enum.cr",
    "content": "# Provides an easier way to define a [route requirement][Athena::Routing::Route--parameter-validation] for all, or a subset of, Enum members.\n#\n# For example:\n# ```\n# require \"athena\"\n#\n# enum Color\n#   Red\n#   Blue\n#   Green\n#   Black\n# end\n#\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\n#     \"/color/{color}\",\n#     requirements: {\"color\" => ART::Requirement::Enum(Color).new},\n#   )]\n#   def get_color(color : Color) : Color\n#     color\n#   end\n#\n#   @[ARTA::Get(\n#     \"/rgb-color/{color}\",\n#     requirements: {\"color\" => ART::Requirement::Enum(Color).new(:red, :green, :blue)},\n#   )]\n#   def get_rgb_color(color : Color) : Color\n#     color\n#   end\n# end\n#\n# ATH.run\n#\n# # GET /color/red  # => \"red\"\n# # GET /color/pink # => 404\n# #\n# # GET /rgb-color/red   # => \"red\"\n# # GET /rgb-color/green # => \"green\"\n# # GET /rgb-color/blue  # => \"blue\"\n# # GET /rgb-color/black # => 404\n# ```\n#\n# NOTE: This type _ONLY_ supports the string representation of enum members.\nstruct Athena::Routing::Requirement::Enum(EnumType)\n  # Returns the set of allowed enum members, or `nil` if all members are allowed.\n  getter members : Set(EnumType)? = nil\n\n  def self.new(*cases : EnumType)\n    new cases.to_set\n  end\n\n  def initialize(@members : Set(EnumType)? = nil)\n    {%\n      unless EnumType <= ::Enum\n        raise \"'#{EnumType}' is not an Enum type.\"\n      end\n    %}\n  end\n\n  # :nodoc:\n  def to_s(io : IO) : Nil\n    (@members || EnumType.names).join io, '|' do |member, join_io|\n      join_io << Regex.escape member.to_s.underscore\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/requirement/requirement.cr",
    "content": "# Includes types related to [route requirements][Athena::Routing::Route--parameter-validation].\n#\n# The namespace also exposes various regex constants representing common universal requirements to make using them in routes easier.\n#\n# ```\n# class ExampleController < ATH::Controller\n#   @[ARTA::Get(\n#     \"/user/{id}\",\n#     requirements: {\"id\" => ART::Requirement::DIGITS},\n#   )]\n#   def get_user(id : Int64) : Int64\n#     id\n#   end\n#\n#   @[ARTA::Get(\n#     \"/article/{slug}\",\n#     requirements: {\"slug\" => ART::Requirement::ASCII_SLUG},\n#   )]\n#   def get_article(slug : String) : String\n#     slug\n#   end\n# end\n# ```\nmodule Athena::Routing::Requirement\n  # Sourced from https://github.com/symfony/symfony/blob/c70be0957a11fd8b7aa687d6173e76724068daa4/src/Symfony/Component/Routing/Requirement/Requirement.php\n\n  ASCII_SLUG = /[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*/\n  CATCH_ALL  = /.+/\n\n  # Matches a date string in the format of `YYYY-MM-DD`.\n  DATE_YMD     = /[0-9]{4}-(?:0[1-9]|1[012])-(?:0[1-9]|[12][0-9]|(?<!02-)3[01])/\n  DIGITS       = /[0-9]+/\n  POSITIVE_INT = /[1-9][0-9]*/\n  UID_BASE32   = /[0-9A-HJKMNP-TV-Z]{26}/\n  UID_BASE58   = /[1-9A-HJ-NP-Za-km-z]{22}/\n  UID_RFC4122  = /[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}/\n  ULID         = /[0-7][0-9A-HJKMNP-TV-Z]{25}/\n  UUID         = /[0-9a-f]{8}-[0-9a-f]{4}-[12-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/\n  UUID_V1      = /[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/\n  UUID_V3      = /[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/\n  UUID_V4      = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/\n  UUID_V5      = /[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/\n  UUID_V6      = /[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/\n  UUID_V7      = /[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/\n  UUID_V8      = /[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/\nend\n"
  },
  {
    "path": "src/components/routing/src/route.cr",
    "content": "# Provides an object-oriented way to represent an HTTP route,\n# including the path, methods, schemes, host, and/or conditions required for it to match.\n#\n# Ultimately, `ART::Route`s are compiled into `ART::CompiledRoute` that represents an immutable\n# snapshot of a route, along with `ART::CompiledRoute::Token`s representing each route parameter.\n#\n# By default, a route is very liberal in regards to what allows when matching.\n# E.g. Matching anything that matches the `path`, but with any HTTP method and any scheme.\n# The `methods` and `schemes` properties can be used to restrict which methods/schemes the route allows.\n#\n# ```\n# # This route will only handle `https` `POST` requests to `/path`.\n# route1 = ART::Route.new \"/path\", schemes: \"https\", methods: \"POST\"\n#\n# # This route will handle `http` or `ftp` `GET`/`PATCH` requests to `/path`.\n# route2 = ART::Route.new \"/path\", schemes: {\"https\", \"ftp\"}, methods: {\"GET\", \"PATCH\"}\n# ```\n#\n# ## Expressions\n#\n# In some cases you may want to match a route using arbitrary dynamic runtime logic.\n# An example use case for this could be checking a request header, or anything else on the underlying `ART::RequestContext` and/or `ART::Request` instance.\n# The `condition` property can be used for just this purpose:\n#\n# ```\n# route = ART::Route.new \"/contact\"\n# route.condition do |context, request|\n#   request.headers[\"user-agent\"].includes? \"Firefox\"\n# end\n# ```\n#\n# This route would only match requests whose `user-agent` header includes `Firefox`.\n# Be sure to also handle cases where headers may not be set.\n#\n# WARNING: Route conditions are _NOT_ taken into consideration when generating routes via an `ART::Generator::Interface`.\n#\n# ## Parameters\n#\n# Route parameters represent variable portions within a route's `path`.\n# Parameters are uniquely named placeholders wrapped within curly braces.\n# For example, `/blog/{slug}` includes a `slug` parameter.\n# Routes can have more than one parameter, but each one may only map to a single value.\n# Parameter placeholders may also be included with static portions for a string, such as `/blog/posts-about-{category}`.\n# This can be useful for supporting format based URLs, such as `/users.json` or `/users.csv` via a `/users.{_format}` path.\n#\n# ### Parameter Validation\n#\n# By default, a placeholder is happy to accept any value.\n# However in most cases you will want to restrict which values it allows, such as ensuring only numeric digits are allowed for a `page` parameter.\n# Parameter validation also allows multiple routes to have variable portions within the same location.\n# I.e. allowing `/blog/{slug}` and `/blog/{page}` to co-exist, which is a limitation for some other Crystal routers.\n#\n# The `requirements` property accepts a `Hash(String, String | Regex)` where the keys are the name of the parameter and the value is a pattern\n# in which the value must match for the route to match. The value can either be a string for exact matches, or a `Regex` for more complex patterns.\n#\n# Route parameters may also be inlined within the `path` by putting the pattern within `<>`, instead of providing it as a dedicated argument.\n# For example, `/blog/{page<\\\\d+>}` (note we need to escape the `\\` within a string literal).\n#\n# ```\n# routes = ART::RouteCollection.new\n# routes.add \"blog_list\", ART::Route.new \"/blog/{page}\", requirements: {\"page\" => /\\d+/}\n# routes.add \"blog_show\", ART::Route.new \"/blog/{slug}\"\n#\n# matcher.match \"/blog/foo\" # => ART::Parameters{\"_route\" => \"blog_show\", \"slug\" => \"foo\"}\n# matcher.match \"/blog/10\"  # => ART::Parameters{\"_route\" => \"blog_list\", \"page\" => \"10\"}\n# ```\n#\n# TIP: Checkout `ART::Requirement` for a set of common, helpful requirement regexes.\n#\n# ### Optional Parameters\n#\n# By default, all parameters are required, meaning given the path `/blog/{page}`, `/blog/10` would match but `/blog` would _NOT_ match.\n# Parameters can be made optional by providing a default value for the parameter, for example:\n#\n# ```\n# ART::Route.new \"/blog/{page}\", {\"page\" => 1}, {\"page\" => /\\d+/}\n#\n# # ...\n#\n# matcher.match \"/blog\" # => ART::Parameters{\"_route\" => \"blog_list\", \"page\" => \"1\"}\n# ```\n#\n# CAUTION: More than one parameter may have a default value, but everything after an optional parameter must also be optional.\n# For example within `/{page}/blog`, `page` will always be required and `/blog` will _NOT_ match.\n#\n# `defaults` may also be inlined within the `path` by putting the value after a `?`.\n# This is also compatible with `requirements`, allowing both to be defined within a path.\n# For example `/blog/{page<\\\\d+>?1}`.\n#\n# TIP: The default value for a parameter may also be `nil`, with the inline syntax being adding a `?` with no following value, e.g. `{page?}`.\n# Be sure to update any type restrictions to be nilable as well.\n#\n# ### Priority Parameter\n#\n# When determining which route should match, the first matching route will win.\n# For example, if two routes were added with variable parameters in the same location, the first one that was added would match regardless of what their requirements are.\n# In most cases this will not be a problem, but in some cases you may need to ensure a particular route is checked first.\n#\n# ### Special Parameters\n#\n# The routing component comes with a few standardized parameters that have special meanings.\n# These parameters could be leveraged within the underlying implementation, but are not directly used within the routing component other than for matching.\n#\n# * `_format` - Could be used to set the underlying format of the request, as well as determining the [content-type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) of the response.\n# * `_fragment` - Represents the fragment identifier when generating a URL. E.g. `/article/10#summary` with the fragment being `summary`.\n# * `_locale` - Could be used to set the underlying locale of the `ART::Request` based on which route is matched.\n# * `_query` - Used to explicitly add [query parameters](/Routing/Generator/Interface/#Athena::Routing::Generator::Interface--query-parameters) to the generated URL.\n#\n# ```\n# ART::Route.new(\n#   \"/articles/{_locale}/search.{_format}\",\n#   {\n#     \"_locale\" => \"en\",\n#     \"_format\" => \"html\",\n#   },\n#   {\n#     \"_locale\" => /en|fr/,\n#     \"_format\" => /html|xml/,\n#   }\n# )\n# ```\n#\n# This route supports `en` and `fr` locales in either `html` or `xml` formats with a default of `en` and `html`.\n#\n# TIP: The trailing `.` is optional if the parameter to the right has a default.\n# E.g. `/articles/en/search` would match with a format of `html` but `/articles/en/search.xml` would be required for matching non-default formats.\n#\n# ### Extra Parameters\n#\n# The defaults defined within a route do not all need to be present as route parameters.\n# This could be useful to provide extra context to the controller that should handle the request.\n#\n# ```\n# ART::Route.new \"/blog/{page}\", {\"page\" => 1, \"title\" => \"Hello world!\"}\n# ```\n#\n# ### Slash Characters in Route Parameters\n#\n# By default, route parameters may include any value except a `/`, since that's the character used to separate the different portions of the URL.\n# Route parameter matching logic may be made more permissive by using a more liberal regex, such as `.+`, for example:\n#\n# ```\n# ART::Route.new \"/share/{token}\", requirements: {\"token\" => /.+/}\n# ```\n#\n# Special parameters should _NOT_ be made more permissive.\n# For example, if the pattern is `/share/{token}.{_format}` and `{token}` allows any character, the `/share/foo/bar.json` URL will consider `foo/bar.json` as the token and the format will be empty.\n# This can be solved by replacing the `.+` requirement with `[^.]+` to allow any character except dots.\n#\n# Related to this, allowing multiple parameters to accept `/` may also lead to unexpected results.\n#\n# ## Sub-Domain Routing\n#\n# The `host` property can be used to require the HTTP [host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header to match this value in order for the route to match.\n#\n# ```\n# mobile_homepage = ART::Route.new \"/\", host: \"m.example.com\"\n# homepage = ART::Route.new \"/\"\n# ```\n#\n# In this example, both routes match the same path, but one requires a specific hostname.\n# The `host` parameter can also be used as route parameters, including `defaults` and `requirements` support:\n#\n# ```\n# mobile_homepage = ART::Route.new(\n#   \"/\",\n#   {\"subdomain\" => \"m\"},\n#   {\"subdomain\" => /m|mobile/},\n#   \"{subdomain}.example.com\"\n# )\n# homepage = ART::Route.new \"/\"\n# ```\n#\n# TIP: Inline defaults and requirements also works for `host` values, `\"{subdomain<m|mobile>?m}.example.com\"`.\nclass Athena::Routing::Route\n  # Represents the callback proc used to dynamically determine if a route should be matched.\n  # See [Routing Expressions][Athena::Routing::Route--expressions] for more information.\n  alias Condition = Proc(ART::RequestContext, ART::Request, Bool)\n\n  # Returns the URL that this route will handle.\n  # See [Routing Parameters][Athena::Routing::Route--parameters] for more information.\n  getter path : String\n\n  # Returns the default values of a route's parameters if they were not provided in the request.\n  # See [Optional Parameters][Athena::Routing::Route--optional-parameters] for more information.\n  getter defaults : ART::Parameters = ART::Parameters.new\n\n  # Returns a hash representing the requirements the route's parameters must match in order for this route to match.\n  # See [Parameter Validation][Athena::Routing::Route--parameter-validation] for more information.\n  getter requirements : Hash(String, Regex) = Hash(String, Regex).new\n\n  # Returns the hostname that the HTTP [host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header must match in order for this route to match.\n  # See [Sub-Domain Routing][Athena::Routing::Route--sub-domain-routing] for more information.\n  getter host : String?\n\n  # Returns the set of valid HTTP methods that this route supports.\n  # See `ART::Route` for more information.\n  getter methods : Set(String)?\n\n  # Returns the optional `ART::Route::Condition` callback used to determine if this route should match.\n  # See [Routing Expressions][Athena::Routing::Route--expressions] for more information.\n  property condition : Condition? = nil\n\n  # TODO: Don't think we actually know what this is:\n\n  # Returns the set of valid URI schemes that this route supports.\n  # See `ART::Route` for more information.\n  getter schemes : Set(String)? = nil\n\n  @compiled_route : ART::CompiledRoute? = nil\n\n  def initialize(\n    @path : String,\n    defaults : Hash(String, _) | ART::Parameters = Hash(String, String?).new,\n    requirements : Hash(String, Regex | String) = Hash(String, Regex | String).new,\n    host : String | Regex | Nil = nil,\n    methods : String | Enumerable(String) | Nil = nil,\n    schemes : String | Enumerable(String) | Nil = nil,\n    @condition : ART::Route::Condition? = nil,\n  )\n    self.path = @path\n    self.add_defaults defaults\n    self.add_requirements requirements\n    self.host = host unless host.nil?\n    self.methods = methods unless methods.nil?\n    self.schemes = schemes unless schemes.nil?\n  end\n\n  # :nodoc:\n  def_equals @path, @defaults, @requirements, @host, @methods, @schemes\n\n  # :nodoc:\n  def_clone\n\n  # Sets the optional `ART::Route::Condition` callback used to determine if this route should match.\n  #\n  # ```\n  # route = ART::Route.new \"/foo\"\n  # route.condition do |context, request|\n  #   request.headers[\"user-agent\"].includes? \"Firefox\"\n  # end\n  # ```\n  #\n  # See [Routing Expressions][Athena::Routing::Route--expressions] for more information.\n  def condition(&@condition : ART::RequestContext, ART::Request -> Bool) : self\n    self\n  end\n\n  # Sets the hostname that the HTTP [host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header must match in order for this route to match to the provided *pattern*.\n  # See [Sub-Domain Routing][Athena::Routing::Route--sub-domain-routing] for more information.\n  def host=(pattern : String | Regex) : self\n    @host = self.extract_inline_defaults_and_requirements pattern\n    @compiled_route = nil\n\n    self\n  end\n\n  # Sets the path required for this route to match to the provided *pattern*.\n  def path=(pattern : String) : self\n    pattern = self.extract_inline_defaults_and_requirements pattern\n    @path = \"/#{pattern.strip.lstrip '/'}\"\n    @compiled_route = nil\n\n    self\n  end\n\n  # Sets the set of valid URI *scheme(s)* that this route supports.\n  # See `ART::Route` for more information.\n  def schemes=(schemes : String | Enumerable(String)) : self\n    schemes = schemes.is_a?(String) ? {schemes} : schemes\n\n    schemes_set = (@schemes ||= Set(String).new)\n    schemes_set.clear\n    schemes.each { |s| schemes_set << s.downcase }\n\n    @compiled_route = nil\n\n    self\n  end\n\n  # Returns `true` if this route allows the provided *scheme*, otherwise `false`.\n  def has_scheme?(scheme : String) : Bool\n    !!@schemes.try &.includes? scheme.downcase\n  end\n\n  # Sets the set of valid HTTP *method(s)* that this route supports.\n  # See `ART::Route` for more information.\n  def methods=(methods : String | Enumerable(String)) : self\n    methods = methods.is_a?(String) ? {methods} : methods\n\n    methods_set = (@methods ||= Set(String).new)\n    methods_set.clear\n    methods.each { |m| methods_set << m.upcase }\n\n    @compiled_route = nil\n\n    self\n  end\n\n  # Compiles and returns an `ART::CompiledRoute` representing this route.\n  # The route is only compiled once and future calls to this method will return the same compiled route,\n  # assuming no changes were made to this route in between.\n  def compile : CompiledRoute\n    @compiled_route ||= ART::RouteCompiler.compile self\n  end\n\n  # Returns `true` if this route has a default with the provided *key*, otherwise `false`.\n  def has_default?(key : String) : Bool\n    !!@defaults.try &.has_key?(key)\n  end\n\n  # Returns the default with the provided *key* as a `String`, if any.\n  def default(key : String) : String?\n    @defaults[key]?\n  end\n\n  # Returns the default with the provided *key* casted to the provided *type*, if any.\n  def default(key : String, type : T.class) : T? forall T\n    @defaults.get?(key, T)\n  end\n\n  # Sets the default values of a route's parameters if they were not provided in the request to the provided *defaults*.\n  # See [Optional Parameters][Athena::Routing::Route--optional-parameters] for more information.\n  def defaults=(defaults : Hash(String, _)) : self\n    @defaults = ART::Parameters.new\n\n    self.add_defaults defaults\n  end\n\n  # :ditto:\n  def defaults=(defaults : ART::Parameters) : self\n    @defaults = ART::Parameters.new\n\n    self.add_defaults defaults\n  end\n\n  # Adds the provided *defaults*, overriding previously set values.\n  def add_defaults(defaults : Hash(String, _)) : self\n    if defaults.has_key?(\"_locale\") && self.localized?\n      defaults.delete \"_locale\"\n    end\n\n    defaults.each do |key, value|\n      @defaults[key] = value\n    end\n\n    @compiled_route = nil\n\n    self\n  end\n\n  # :ditto:\n  def add_defaults(defaults : ART::Parameters) : self\n    if defaults.has_key?(\"_locale\") && self.localized?\n      defaults.delete \"_locale\"\n    end\n\n    defaults.each do |key, value|\n      @defaults[key] = value\n    end\n\n    @compiled_route = nil\n\n    self\n  end\n\n  # Sets the default with the provided *key* to the provided *value*.\n  def set_default(key : String, value) : self\n    if \"_locale\" == key && self.localized?\n      return self\n    end\n\n    @defaults[key] = value\n    @compiled_route = nil\n\n    self\n  end\n\n  # Returns `true` if this route has a requirement with the provided *key*, otherwise `false`.\n  def has_requirement?(key : String) : Bool\n    !!@requirements.try &.has_key?(key)\n  end\n\n  # Returns the requirement with the provided *key*, if any.\n  def requirement(key : String) : Regex?\n    @requirements[key]?\n  end\n\n  # Sets the hash representing the requirements the route's parameters must match in order for this route to match to the provided *requirements*.\n  # See [Parameter Validation][Athena::Routing::Route--parameter-validation] for more information.\n  def requirements=(requirements : Hash(String, Regex | String)) : self\n    @requirements.clear\n\n    self.add_requirements requirements\n  end\n\n  # Adds the provided *requirements*, overriding previously set values.\n  def add_requirements(requirements : Hash(String, Regex | String)) : self\n    if requirements.has_key?(\"_locale\") && self.localized?\n      requirements.delete \"_locale\"\n    end\n\n    requirements.each do |key, regex|\n      @requirements[key] = self.sanitize_requirement key, regex\n    end\n\n    @compiled_route = nil\n\n    self\n  end\n\n  # Sets the requirement with the provided *key* to the provided *value*.\n  def set_requirement(key : String, requirement : Regex | String) : self\n    if \"_locale\" == key && self.localized?\n      return self\n    end\n\n    @requirements[key] = self.sanitize_requirement key, requirement\n\n    @compiled_route = nil\n\n    self\n  end\n\n  private def extract_inline_defaults_and_requirements(pattern : Regex) : String\n    self.extract_inline_defaults_and_requirements pattern.source\n  end\n\n  private def extract_inline_defaults_and_requirements(pattern : String) : String\n    return pattern if !pattern.includes?('?') && !pattern.includes?('<')\n\n    pattern.gsub /\\{(!?)(\\w++)(<.*?>)?(\\?[^\\}]*+)?\\}/ do |_, match|\n      if requirement = match[3]?.presence\n        self.set_requirement match[2], requirement[1...-1]\n      end\n\n      if match[4]?.presence\n        self.set_default match[2], \"?\" != match[4] ? match[4][1..] : nil\n      end\n\n      \"{#{match[1]}#{match[2]}}\"\n    end\n  end\n\n  private def sanitize_requirement(key : String, pattern : Regex) : Regex\n    self.sanitize_requirement key, pattern.source\n  end\n\n  private def sanitize_requirement(key : String, pattern : String) : Regex\n    unless pattern.empty?\n      if p = pattern.lchop? '^'\n        pattern = p\n      elsif p = pattern.lchop? \"\\\\A\"\n        pattern = p\n      end\n    end\n\n    if p = pattern.rchop? '$'\n      pattern = p\n    elsif p = pattern.rchop? \"\\\\z\"\n      pattern = p\n    end\n\n    pattern = \"\\\\\\\\\" if pattern == \"\\\\\"\n\n    raise ArgumentError.new \"Routing requirement for '#{key}' cannot be empty.\" if pattern.empty?\n\n    Regex.new pattern\n  end\n\n  private def localized? : Bool\n    return false unless locale = @defaults[\"_locale\"]?\n    @defaults.has_key?(\"_canonical_route\") && self.requirement(\"_locale\").try &.source == Regex.escape(locale)\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/route_collection.cr",
    "content": "# Represents a collection of `ART::Route`s.\n# Provides a way to traverse, edit, remove, and access the stored routes.\n#\n# Each route has an associated name that should be unique.\n# Adding another route with the same name will override the previous one.\n#\n# ## Route Priority\n#\n# When determining which route should match, the first matching route will win.\n# For example, if two routes were added with variable parameters in the same location, the first one that was added would match regardless of what their requirements are.\n# In most cases this will not be a problem, but in some cases you may need to ensure a particular route is checked first.\n#\n# The `priority` argument within `#add` can be used to control this order.\nclass Athena::Routing::RouteCollection\n  include Enumerable({String, Athena::Routing::Route})\n  include Iterable({String, Athena::Routing::Route})\n\n  @routes = Hash(String, ART::Route).new\n  protected getter priorities = Hash(String, Int32).new\n\n  @sorted : Bool = false\n\n  # :nodoc:\n  def_clone\n\n  # TODO: Support route aliases?\n\n  # Returns the `ART::Action` with the provided *name*.\n  #\n  # Raises a `ART::Exception::RouteNotFound` if a route with the provided *name* does not exist.\n  def [](name : String) : ART::Route\n    self.routes.fetch(name) { raise ART::Exception::RouteNotFound.new \"No route with the name '#{name}' exists.\" }\n  end\n\n  # Returns the `ART::Action` with the provided *name*, or `nil` if it does not exist.\n  def []?(name : String) : ART::Route?\n    self.routes[name]?\n  end\n\n  # Adds all the routes from the provided *collection* to this collection.\n  def add(collection : self) : Nil\n    @sorted = false\n\n    # Remove the routes first so they are added to the end of the routes hash.\n    collection.each do |name, route|\n      self.delete name\n\n      @routes[name] = route\n\n      if priority = collection.priorities[name]?\n        @priorities[name] = priority\n      end\n    end\n  end\n\n  # Adds the provided *route* with the provided *name* to this collection, optionally with the provided *priority*.\n  def add(name : String, route : ART::Route, priority : Int32 = 0) : Nil\n    self.delete name\n\n    @routes[name] = route\n\n    @priorities[name] = priority unless priority.zero?\n  end\n\n  def add_defaults(defaults : Hash(String, _)) : Nil\n    return if defaults.empty?\n\n    @routes.each_value do |route|\n      route.add_defaults defaults\n    end\n  end\n\n  # Adds a path *prefix* to all routes stored in this collection.\n  # Optionally allows merging in additional *defaults* or *requirements*.\n  def add_prefix(prefix : String, defaults : Hash(String, _) = Hash(String, String?).new, requirements : Hash(String, String | Regex) = Hash(String, String | Regex).new) : Nil\n    prefix = prefix.strip.rstrip '/'\n    return if prefix.empty?\n\n    @routes.each_value do |route|\n      route.path = \"/#{prefix}#{route.path}\"\n      route.add_defaults defaults\n      route.add_requirements requirements\n    end\n  end\n\n  # Adds the provided *prefix* to the name of all routes stored within this collection.\n  def add_name_prefix(prefix : String) : Nil\n    prefixed_routes = Hash(String, ART::Route).new\n    prefixed_priorities = Hash(String, Int32).new\n\n    @routes.each do |name, route|\n      prefixed_routes[\"#{prefix}#{name}\"] = route\n\n      if canonical_route = route.default \"_canonical_route\"\n        route.set_default \"_canonical_route\", \"#{prefix}#{canonical_route}\"\n      end\n\n      if priority = @priorities[name]?\n        prefixed_priorities[\"#{prefix}#{name}\"] = priority\n      end\n    end\n\n    # TODO: Support aliases?\n\n    @routes = prefixed_routes\n    @priorities = prefixed_priorities\n  end\n\n  # Merges the provided *requirements* into all routes stored within this collection.\n  def add_requirements(requirements : Hash(String, Regex | String)) : Nil\n    return if requirements.empty?\n\n    @routes.each_value do |route|\n      route.add_requirements requirements\n    end\n  end\n\n  # Sets the host property of all routes stored in this collection.\n  # Optionally allows merging in additional *defaults* or *requirements*.\n  def set_host(host : String, defaults : Hash(String, _) = Hash(String, String?).new, requirements : Hash(String, String | Regex) = Hash(String, String | Regex).new) : Nil\n    @routes.each_value do |route|\n      route.host = host\n      route.add_defaults defaults\n      route.add_requirements requirements\n    end\n  end\n\n  # Sets the scheme(s) of all routes stored within this collection.\n  def schemes=(schemes : String | Enumerable(String)) : Nil\n    @routes.each_value do |route|\n      route.schemes = schemes\n    end\n  end\n\n  # Sets the method(s) of all routes stored within this collection.\n  def methods=(methods : String | Enumerable(String)) : Nil\n    @routes.each_value do |route|\n      route.methods = methods\n    end\n  end\n\n  # Removes the routes with the provide *names*.\n  def remove(*names : String) : Nil\n    names.each { |n| self.remove n }\n  end\n\n  # Removes the route with the provide *name*.\n  def remove(name : String) : Nil\n    self.delete name\n  end\n\n  # Yields the name and `ART::Route` object for each registered route.\n  def each(&) : Nil\n    self.routes.each do |k, v|\n      yield({k, v})\n    end\n  end\n\n  # Returns an `Iterator` for each registered route.\n  def each\n    self.routes.each\n  end\n\n  # Returns the routes stored within this collection.\n  def routes : Hash(String, ART::Route)\n    if !@priorities.empty? && !@sorted\n      insert_order = @routes.keys\n\n      @routes\n        .to_a\n        .sort! do |(n1, r1), (n2, r2)|\n          priority = (@priorities[n2]? || 0) <=> (@priorities[n1]? || 0)\n\n          next priority unless priority.zero?\n\n          insert_order.index!(n1) <=> insert_order.index!(n2)\n        end\n        .tap { @routes.clear }\n        .each { |name, route| @routes[name] = route }\n\n      @sorted = true\n    end\n\n    @routes\n  end\n\n  # Returns the number of routes stored within this collection.\n  def size : Int\n    self.routes.size\n  end\n\n  private def delete(name : String) : Nil\n    @routes.delete name\n    @priorities.delete name\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/route_compiler.cr",
    "content": "# :nodoc:\nmodule Athena::Routing::RouteCompiler\n  private PATH_REGEX = /{(!)?(\\w+)}/\n  private SEPARATORS = \"/,;.:-_~+*=@|\"\n  private MAX_LENGTH = 32\n\n  private record CompiledPattern,\n    static_prefix : String,\n    regex : Regex,\n    tokens : Array(ART::CompiledRoute::Token),\n    variables : Set(String)\n\n  def self.compile(route : Route) : CompiledRoute\n    host_variables = Set(String).new\n    variables = Set(String).new\n    host_regex = nil\n    host_tokens = Array(ART::CompiledRoute::Token).new\n\n    if host = route.host.presence\n      pattern = self.compile_pattern route, host, true\n\n      host_variables = pattern.variables\n      variables = host_variables.dup\n\n      host_tokens = pattern.tokens\n      host_regex = pattern.regex\n    end\n\n    if (locale = route.default(\"_locale\", String)) && !route.default(\"_canonical_route\").nil? && route.requirement(\"_locale\").try &.source == Regex.escape(locale)\n      requirements = route.requirements\n      requirements.delete \"_locale\"\n\n      # TODO: Pretty sure this deletes via reference\n      route.requirements = requirements\n      route.path = route.path.sub \"{_locale}\", locale\n    end\n\n    path = route.path\n\n    pattern = self.compile_pattern route, path, false\n\n    path_variables = pattern.variables\n\n    raise ART::Exception::InvalidArgument.new \"Route pattern '#{route.path}' cannot contain '_fragment' as a path parameter.\" if path_variables.includes? \"_fragment\"\n\n    variables.concat path_variables\n\n    CompiledRoute.new(\n      pattern.static_prefix,\n      pattern.regex,\n      pattern.tokens,\n      path_variables,\n      host_regex,\n      host_tokens,\n      host_variables,\n      variables\n    )\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def self.compile_pattern(route : Route, pattern : String, is_host : Bool)\n    pos = 0\n    variables = Set(String).new\n    tokens = Array(ART::CompiledRoute::Token).new\n    default_separator = is_host ? \".\" : \"/\"\n\n    # Matches and iterates over all variables within `{}`.\n    # match[0] => var name with {}\n    # match[1] => (optional) `!` symbol\n    # match[2] => var name without {}\n    pattern.scan(PATH_REGEX) do |match|\n      is_important = !match[1]?.nil?\n      var_name = match[2]\n\n      # Static text before the match\n      preceding_text = pattern[pos, match.begin - pos]\n      pos = match.begin + match[0].size\n\n      preceding_char = preceding_text.empty? ? \"\" : preceding_text[-1].to_s\n      is_separator = !preceding_char.empty? && SEPARATORS.includes?(preceding_char)\n\n      raise ART::Exception::InvalidArgument.new \"Variable name '#{var_name}' cannot start with a digit in route pattern '#{pattern}'.\" if var_name.starts_with? /\\d/\n      raise ART::Exception::InvalidArgument.new \"Route pattern '#{pattern}' cannot reference variable name '#{var_name}' more than once.\" unless variables.add? var_name\n      raise ART::Exception::InvalidArgument.new \"Variable name '#{var_name}' cannot be longer than #{MAX_LENGTH} characters in route pattern '#{pattern}'.\" if var_name.size > MAX_LENGTH\n\n      if is_separator && preceding_text != preceding_char\n        tokens << ART::CompiledRoute::Token.new :text, preceding_text[0...-preceding_char.size]\n      elsif !is_separator && !preceding_text.empty?\n        tokens << ART::CompiledRoute::Token.new :text, preceding_text\n      end\n\n      if regex = route.requirement var_name\n        regex = self.transform_capturing_groups_to_non_capturings regex.source\n      else\n        following_pattern = pattern[pos..]\n        next_separator = self.find_next_separator following_pattern\n\n        regex = /[^#{Regex.escape default_separator}#{default_separator != next_separator && \"\" != next_separator ? Regex.escape(next_separator) : \"\"}]+/\n\n        if (!next_separator.empty? && !following_pattern.matches?(/^\\{\\w+\\}/)) || following_pattern.empty?\n          regex = /#{regex.source}+/\n        end\n      end\n\n      tokens << if is_important\n        ART::CompiledRoute::Token.new :variable, is_separator ? preceding_char : \"\", regex, var_name, true\n      else\n        ART::CompiledRoute::Token.new :variable, is_separator ? preceding_char : \"\", regex, var_name\n      end\n    end\n\n    if pos < pattern.size\n      tokens << ART::CompiledRoute::Token.new :text, pattern[pos..]\n    end\n\n    first_optional_index = Int32::MAX\n\n    unless is_host\n      idx = tokens.size - 1\n\n      while idx >= 0\n        token = tokens[idx]\n\n        break if !token.type.variable? || token.important? || !route.has_default?(token.var_name.not_nil!)\n\n        first_optional_index = idx\n        idx -= 1\n      end\n    end\n\n    route_pattern = \"\"\n    tokens.each_with_index do |_, i|\n      route_pattern += self.compute_regex tokens, i, first_optional_index\n    end\n\n    route_regex = Regex.new \"^#{route_pattern}$\", is_host ? Regex::CompileOptions::IGNORE_CASE : Regex::CompileOptions::None\n\n    # Crystal has UTF-8 regex mode enabled by default, so no need to add it.\n\n    CompiledPattern.new(\n      self.determine_static_prefix(route, tokens),\n      route_regex,\n      tokens.reverse!,\n      variables\n    )\n  end\n\n  private def self.determine_static_prefix(route : Route, tokens : Array(ART::CompiledRoute::Token)) : String\n    first_token = tokens.first\n\n    unless first_token.type.text?\n      return (route.has_default?(first_token.var_name.not_nil!) || \"/\" == first_token.prefix) ? \"\" : first_token.prefix\n    end\n\n    prefix = first_token.prefix\n\n    if (second_token = tokens[1]?) && (\"/\" != second_token.prefix) && !route.has_default?(second_token.var_name.not_nil!)\n      prefix += second_token.prefix\n    end\n\n    prefix\n  end\n\n  private def self.find_next_separator(pattern : String) : String\n    return \"\" if pattern.empty?\n    return \"\" if (pattern = pattern.gsub(/\\{\\w+\\}/, \"\")).empty?\n\n    pattern = pattern[0].to_s\n\n    SEPARATORS.includes?(pattern) ? pattern : \"\"\n  end\n\n  private def self.compute_regex(tokens : Array(ART::CompiledRoute::Token), idx : Int, first_optional_index : Int) : String\n    token = tokens[idx]\n\n    case token.type\n    in .text? then Regex.escape token.prefix\n    in .variable?\n      if idx.zero? && 0 == first_optional_index\n        \"#{Regex.escape token.prefix}(?P<#{token.var_name}>#{token.regex.not_nil!.source})?\"\n      else\n        regex = \"#{Regex.escape token.prefix}(?P<#{token.var_name}>#{token.regex.not_nil!.source})\"\n\n        if idx >= first_optional_index\n          regex = \"(?:#{regex}\"\n          num_tokens = tokens.size\n\n          if idx == num_tokens - 1\n            regex += \")?\" * (num_tokens - first_optional_index - (first_optional_index.zero? ? 1 : 0))\n          end\n        end\n\n        regex\n      end\n    end\n  end\n\n  private def self.transform_capturing_groups_to_non_capturings(source : String) : Regex\n    idx = 0\n    while idx < source.size\n      if '\\\\' == source[idx]\n        idx += 2\n        next\n      end\n\n      if '(' != source[idx] || source[idx + 2]?.nil?\n        idx += 1\n        next\n      end\n\n      if '*' == source[(idx += 1)] || '?' == source[idx]\n        idx += 2\n        next\n      end\n\n      source = source.insert idx, \"?:\"\n      idx += 1\n    end\n\n    Regex.new source\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/route_provider.cr",
    "content": "require \"./static_prefix_collection\"\n\n# Stores the compiled route data on the class level for performance reasons.\n#\n# This type is default location, but can be extended to support multiple routers using different route collections without affecting one another.\n#\n# ```\n# class MyCustomProvider < ART::RouteProvider\n# end\n#\n# # ...\n#\n# # Compile the provided routes into MyCustomProvider, instead of the default provider.\n# ART.compile routes, route_provider: MyCustomProvider\n# ```\nclass Athena::Routing::RouteProvider\n  private alias Condition = Athena::Routing::Route::Condition\n\n  # We store this as a tuple in order to get splatting/unpacking features.\n  # defaults, variables, methods, schemas, trailing slash?, trailing var?, conditions\n  #\n  # :nodoc:\n  alias DynamicRouteData = Tuple(ART::Parameters, Set(String)?, Set(String)?, Set(String)?, Bool, Bool, Int32?)\n\n  # We store this as a tuple in order to get splatting/unpacking features.\n  # defaults, host, methods, schemas, trailing slash?, trailing var?, conditions\n  #\n  # :nodoc:\n  alias StaticRouteData = Tuple(ART::Parameters, String | Regex | Nil, Set(String)?, Set(String)?, Bool, Bool, Int32?)\n\n  # We store this as a tuple in order to get splatting/unpacking features.\n  # variables, defaults, requirements, tokens, host tokens, schemes\n  #\n  # :nodoc:\n  alias RouteGenerationData = Tuple(Set(String), ART::Parameters, Hash(String, Regex), Array(ART::CompiledRoute::Token), Array(ART::CompiledRoute::Token), Set(String)?)\n\n  private record PreCompiledStaticRoute, route : ART::Route, has_trailing_slash : Bool\n  private record PreCompiledDynamicRegex, host_regex : Regex?, regex : Regex, static_prefix : String\n  private record PreCompiledDynamicRoute, pattern : String, routes : ART::RouteCollection\n\n  private class State\n    property vars : Set(String) = Set(String).new\n    property host_vars : Set(String) = Set(String).new\n    property mark : Int32 = 0\n    property mark_tail : Int32 = 0\n    getter routes : Hash(String, Array(DynamicRouteData))\n    property regex : String = \"\"\n\n    def initialize(@routes : Hash(String, Array(DynamicRouteData))); end\n\n    def vars(subject : String, regex : Regex) : String\n      subject.gsub(regex) do |_, match|\n        next \"?:\" if \"_route\" == match[1]\n\n        @vars << match[1].to_s\n\n        \"\"\n      end\n    end\n  end\n\n  protected class_getter? match_host : Bool = false\n  protected class_getter static_routes : Hash(String, Array(StaticRouteData)) = Hash(String, Array(StaticRouteData)).new\n  protected class_getter route_regexes : Hash(Int32, Regex) = Hash(Int32, Regex).new\n  protected class_getter dynamic_routes : Hash(String, Array(DynamicRouteData)) = Hash(String, Array(DynamicRouteData)).new\n  protected class_getter conditions : Hash(Int32, Condition) = Hash(Int32, Condition).new\n  protected class_getter route_generation_data : Hash(String, RouteGenerationData) = Hash(String, RouteGenerationData).new\n\n  protected class_getter? compiled : Bool = false\n\n  @@routes : ART::RouteCollection? = nil\n\n  def self.compile(routes : ART::RouteCollection) : Nil\n    return if @@compiled\n\n    @@routes = routes\n\n    self.compile\n  end\n\n  # :nodoc:\n  def self.inspect(io : IO) : Nil\n    io << \"Match Host:  \"\n    @@match_host.inspect io\n    io << \"\\n\\nStatic Routes:  \"\n    @@static_routes.inspect io\n    io << \"\\n\\nRegexes:  \"\n    @@route_regexes.inspect io\n    io << \"\\n\\nDynamic Routes:  \"\n    @@dynamic_routes.inspect io\n    io << \"\\n\\nConditions:  \"\n    @@conditions.inspect io\n    io << \"\\n\\nRoute Generation Data:  \"\n    @@route_generation_data.inspect io\n    io << \"\\n\\n\"\n  end\n\n  protected def self.reset : Nil\n    @@match_host = false\n    @@static_routes.clear\n    @@dynamic_routes.clear\n    @@route_regexes.clear\n    @@conditions.clear\n    @@route_generation_data.clear\n    @@compiled = false\n    @@routes = nil\n  end\n\n  private def self.compile : Nil\n    match_host = false\n    routes = ART::RouteProvider::StaticPrefixCollection.new\n\n    self.routes.each do |name, route|\n      if host = route.host\n        match_host = true\n\n        host = %(/#{host.reverse.tr \"}.{\", \"(/)\"})\n      end\n\n      routes.add_route (host || \"/(.*)\"), ART::RouteProvider::StaticPrefixCollection::StaticTreeNamedRoute.new name, route\n    end\n\n    if match_host\n      @@match_host = true\n      routes = routes.populate_collection ART::RouteCollection.new\n    else\n      @@match_host = false\n      routes = self.routes\n    end\n\n    static_routes, dynamic_routes = self.group_static_routes routes\n\n    conditions = Array(Condition).new\n\n    self.compile_static_routes static_routes, conditions\n\n    chunk_limit = dynamic_routes.size\n\n    loop do\n      self.compile_dynamic_routes dynamic_routes, match_host, chunk_limit, conditions\n      break\n    rescue e : ArgumentError\n      if 1 < chunk_limit && e.message.try(&.starts_with?(\"regular expression is too large\"))\n        chunk_limit = 1 + (chunk_limit >> 1)\n        next\n      end\n\n      raise e\n    end\n\n    self.routes.each do |name, route|\n      compiled_route = route.compile\n\n      @@route_generation_data[name] = {\n        compiled_route.variables,\n        route.defaults,\n        route.requirements,\n        compiled_route.tokens,\n        compiled_route.host_tokens,\n        route.schemes,\n      }\n    end\n\n    @@compiled = true\n    @@routes = nil\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def self.compile_dynamic_routes(collection : ART::RouteCollection, match_host : Bool, chunk_limit : Int, conditions : Array(Condition)) : Nil\n    dr = Hash(String, Array(DynamicRouteData)).new\n\n    if collection.empty?\n      return @@dynamic_routes = dr\n    end\n\n    state = State.new dr\n\n    chunk_size = 0\n    routes = nil\n    collections = Array(ART::RouteCollection).new\n\n    collection.each do |name, route|\n      if chunk_limit < (chunk_size += 1) || routes.nil?\n        chunk_size = 1\n        routes = ART::RouteCollection.new\n        collections << routes\n      end\n\n      routes.not_nil!.add name, route\n    end\n\n    collections.each do |sub_collection|\n      previous_regex = false\n      per_host_routes = Array(Tuple(Regex?, ART::RouteCollection)).new\n      host_routes = nil\n\n      sub_collection.each do |name, route|\n        regex = route.compile.host_regex\n        if previous_regex != regex\n          host_routes = ART::RouteCollection.new\n          per_host_routes << {regex, host_routes}\n          previous_regex = regex\n        end\n\n        host_routes.not_nil!.add name, route\n      end\n\n      previous_regex = false\n      final_regex = \"^(?\"\n      starting_mark = state.mark\n      state.mark += final_regex.size + 1 # Add 1 to account for the eventual `/`.\n      state.regex = final_regex\n\n      per_host_routes.each do |host_regex, sub_routes|\n        if match_host\n          if host_regex\n            host_regex.source.match(/^\\^(.*)\\$$/).try do |match|\n              state.vars = Set(String).new\n              host_regex = state.vars match[1], /\\?P<([^>]++)>/\n              host_regex = Regex.new \"(?i:#{host_regex})\\\\.\"\n              state.host_vars = state.vars\n            end\n          else\n            host_regex = /(?:(?:[^.\\/]*+\\.)++)/\n            state.host_vars = Set(String).new\n          end\n\n          pattern = %<#{previous_regex ? \")\" : \"\"}|#{host_regex.source}(?>\n          state.mark += pattern.size\n          state.regex += pattern\n          previous_regex = true\n        end\n\n        tree = ART::RouteProvider::StaticPrefixCollection.new\n\n        sub_routes.each do |name, route|\n          matched_regex = route.compile.regex.source.match!(/\\^(.*)\\$$/)\n\n          state.vars = Set(String).new\n          pattern = state.vars matched_regex[1], /\\?P<([^>]++)>/\n\n          if has_trailing_slash = \"/\" != pattern && pattern.ends_with? '/'\n            pattern = pattern.rchop '/'\n          end\n\n          has_trailing_var = route.path.matches? /\\{\\w+\\}\\/?$/\n\n          tree.add_route pattern, ART::RouteProvider::StaticPrefixCollection::StaticPrefixTreeRoute.new name, pattern, state.vars, route, has_trailing_slash, has_trailing_var\n        end\n\n        self.compile_static_prefix_collection tree, state, 0, conditions\n      end\n\n      if match_host\n        state.regex += \")\"\n      end\n\n      state.regex += \")/?$\"\n      state.mark_tail = 0\n\n      @@route_regexes[starting_mark] = ART.create_regex state.regex\n    end\n\n    @@dynamic_routes = state.routes\n  end\n\n  private def self.compile_static_prefix_collection(tree : ART::RouteProvider::StaticPrefixCollection, state : State, prefix_length : Int32, conditions) : Nil\n    previous_regex = nil\n\n    tree.items.each do |item|\n      case item\n      in ART::RouteProvider::StaticPrefixCollection\n        previous_regex = nil\n        prefix = item.prefix[prefix_length..]\n        pattern = \"|#{prefix}(?\"\n        state.mark += pattern.size\n        state.regex += pattern\n\n        self.compile_static_prefix_collection item, state, prefix_length + prefix.size, conditions\n\n        state.regex += \")\"\n        state.mark_tail += 1\n\n        next\n      in ART::RouteProvider::StaticPrefixCollection::StaticPrefixTreeRoute\n        compiled_route = item.route.compile\n        vars = state.host_vars + item.variables\n\n        if compiled_route.regex == previous_regex\n          state.routes[state.mark.to_s] << self.compile_dynamic_route item.route, item.name, vars, item.has_trailing_slash, item.has_trailing_var, conditions\n          next\n        end\n\n        state.mark += 3 + state.mark_tail + item.pattern.size - prefix_length\n        state.mark_tail = 2 + state.mark.digits.size\n\n        state.regex += \"|#{item.pattern[prefix_length..]}(*:#{state.mark})\"\n\n        previous_regex = compiled_route.regex\n        state.routes[state.mark.to_s] = [self.compile_dynamic_route item.route, item.name, vars, item.has_trailing_slash, item.has_trailing_var, conditions] of DynamicRouteData\n      in ART::RouteProvider::StaticPrefixCollection::StaticTreeNamedRoute\n        raise \"BUG: StaticTreeNamedRoute\"\n      in ART::RouteProvider::StaticPrefixCollection::StaticTreeName\n        raise \"BUG: StaticTreeName\"\n      end\n    end\n  end\n\n  private alias StaticRoutes = Hash(String, Hash(String, PreCompiledStaticRoute))\n\n  private def self.compile_static_routes(static_routes : StaticRoutes, conditions : Array(Condition)) : Nil\n    return if static_routes.empty?\n\n    sr = Hash(String, Array(StaticRouteData)).new\n\n    static_routes.each do |url, routes|\n      sr[url] = Array(StaticRouteData).new routes.size\n\n      routes.each do |name, pre_compiled_route|\n        route = pre_compiled_route.route\n\n        host = if route.compile.host_variables.empty?\n                 route.host\n               elsif regex = route.compile.host_regex\n                 regex\n               end\n\n        sr[url] << self.compile_static_route(\n          route,\n          name,\n          host,\n          pre_compiled_route.has_trailing_slash,\n          false,\n          conditions\n        )\n      end\n    end\n\n    @@static_routes = sr\n  end\n\n  private def self.compile_dynamic_route(route : ART::Route, name : String, vars : Set(String)?, has_trailing_slash : Bool, has_trailing_var : Bool, conditions : Array(Condition)) : DynamicRouteData\n    defaults = route.defaults.dup\n\n    if canonical_route = defaults[\"_canonical_route\"]?\n      name = canonical_route\n      defaults.delete \"_canonical_route\"\n    end\n\n    if condition = route.condition\n      @@conditions[condition_key = 1 * @@conditions.size] = condition\n    end\n\n    {\n      ART::Parameters.new({\"_route\" => name}).merge!(defaults),\n      vars,\n      route.methods,\n      route.schemes,\n      has_trailing_slash,\n      has_trailing_var,\n      condition_key,\n    }\n  end\n\n  private def self.compile_static_route(route : ART::Route, name : String, host : String | Regex | Nil, has_trailing_slash : Bool, has_trailing_var : Bool, conditions : Array(Condition)) : StaticRouteData\n    defaults = route.defaults.dup\n\n    if canonical_route = defaults[\"_canonical_route\"]?\n      name = canonical_route\n      defaults.delete \"_canonical_route\"\n    end\n\n    if condition = route.condition\n      @@conditions[condition_key = 1 * @@conditions.size] = condition\n    end\n\n    {\n      ART::Parameters.new({\"_route\" => name}).merge!(defaults),\n      host,\n      route.methods,\n      route.schemes,\n      has_trailing_slash,\n      has_trailing_var,\n      condition_key,\n    }\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  private def self.group_static_routes(routes : ART::RouteCollection) : Tuple(StaticRoutes, ART::RouteCollection)\n    static_routes = Hash(String, Hash(String, PreCompiledStaticRoute)).new { |hash, key| hash[key] = Hash(String, PreCompiledStaticRoute).new }\n    dynamic_regex = Array(PreCompiledDynamicRegex).new\n    dynamic_routes = ART::RouteCollection.new\n\n    routes.each do |name, route|\n      compiled_route = route.compile\n      static_prefix = compiled_route.static_prefix.rstrip '/'\n      host_regex = compiled_route.host_regex\n      regex = compiled_route.regex\n\n      has_trailing_slash = \"/\" != route.path\n\n      if has_trailing_slash\n        pos = regex.source.index!('$')\n        has_trailing_slash = '/' == regex.source[pos - 1]\n        regex = Regex.new regex.source.sub (1 + pos - (has_trailing_slash ? 1 : 0))..-((has_trailing_slash ? 1 : 0)), \"/?$\"\n      end\n\n      if compiled_route.path_variables.empty?\n        host = compiled_route.host_variables.empty? ? \"\" : route.host\n        url = route.path\n\n        if has_trailing_slash\n          url = url.rstrip '/'\n        end\n\n        should_next = dynamic_regex.each do |dr|\n          host_regex_matches = host ? dr.host_regex.try &.matches?(host) : false\n\n          if (dr.static_prefix.empty? || url.starts_with?(dr.static_prefix)) &&\n             (dr.regex.matches?(url) || dr.regex.matches?(\"#{url}/\")) &&\n             (host.presence.nil? || host_regex.nil? || host_regex_matches)\n            dynamic_regex << PreCompiledDynamicRegex.new host_regex, regex, static_prefix\n            dynamic_routes.add name, route\n            break true\n          end\n        end\n\n        next if should_next\n\n        static_routes[url][name] = PreCompiledStaticRoute.new route, has_trailing_slash\n      else\n        dynamic_regex << PreCompiledDynamicRegex.new host_regex, regex, static_prefix\n        dynamic_routes.add name, route\n      end\n    end\n\n    {static_routes, dynamic_routes}\n  end\n\n  private def self.routes : ART::RouteCollection\n    @@routes || raise \"Routes have not been compiled. Did you forget to call `ART.compile` for #{self.class}?\"\n  end\n\n  private def initialize; end\nend\n"
  },
  {
    "path": "src/components/routing/src/router.cr",
    "content": "require \"./router_interface\"\nrequire \"./matcher/request_matcher_interface\"\n\nclass Athena::Routing::Router\n  include Athena::Routing::RouterInterface\n  include Athena::Routing::Matcher::RequestMatcherInterface\n\n  # :inherit:\n  getter route_collection : ART::RouteCollection\n\n  # :inherit:\n  getter context : ART::RequestContext\n\n  # TODO: Should the matcher/generator types be customizable?\n\n  getter matcher : ART::Matcher::URLMatcherInterface do\n    ART::Matcher::URLMatcher.new(@context, @route_provider)\n  end\n\n  getter generator : ART::Generator::Interface do\n    generator = ART::Generator::URLGenerator.new @context, @default_locale, @route_provider\n    generator.strict_requirements = @strict_requirements\n    generator\n  end\n\n  def initialize(\n    @route_collection : ART::RouteCollection,\n    @default_locale : String? = nil,\n    @strict_requirements : Bool? = true,\n    context : ART::RequestContext? = nil,\n    @route_provider : ART::RouteProvider.class = ART::RouteProvider,\n  )\n    @context = context || ART::RequestContext.new\n  end\n\n  # :inherit:\n  def generate(route : String, reference_type : ART::Generator::ReferenceType = :absolute_path, **params) : String\n    self.generate route, params.to_h.transform_keys(&.to_s), reference_type\n  end\n\n  # :inherit:\n  def generate(route : String, params : Hash = Hash(String, String?).new, reference_type : ART::Generator::ReferenceType = :absolute_path) : String\n    self.generator.generate route, params, reference_type\n  end\n\n  # :inherit:\n  def match(path : String) : ART::Parameters\n    self.matcher.match path\n  end\n\n  # :inherit:\n  def match(request : ART::Request) : ART::Parameters\n    matcher = self.matcher\n\n    unless matcher.is_a? ART::Matcher::RequestMatcherInterface\n      return matcher.match request.path\n    end\n\n    matcher.match request\n  end\n\n  # :inherit:\n  def match?(path : String) : ART::Parameters?\n    self.matcher.match? path\n  end\n\n  # :inherit:\n  def match?(request : ART::Request) : ART::Parameters?\n    matcher = self.matcher\n\n    unless matcher.is_a? ART::Matcher::RequestMatcherInterface\n      return matcher.match? request.path\n    end\n\n    matcher.match? request\n  end\n\n  # :inherit:\n  def context=(@context : ART::RequestContext)\n    if matcher = @matcher\n      matcher.context = context\n    end\n\n    if generator = @generator\n      generator.context = context\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/router_interface.cr",
    "content": "require \"./matcher/url_matcher_interface\"\nrequire \"./generator/interface\"\n\nmodule Athena::Routing::RouterInterface\n  include Athena::Routing::Matcher::URLMatcherInterface\n  include Athena::Routing::Generator::Interface\n\n  abstract def route_collection : ART::RouteCollection\nend\n"
  },
  {
    "path": "src/components/routing/src/routing_handler.cr",
    "content": "require \"log\"\n\n# Provides basic routing functionality to an [HTTP::Server](https://crystal-lang.org/api/HTTP/Server.html).\n#\n# This type works as both a [HTTP::Handler](https://crystal-lang.org/api/HTTP/Handler.html) and\n# an `ART::RouteCollection` that accepts a block that will handle that particular route.\n#\n# ```\n# handler = ART::RoutingHandler.new\n#\n# # The `methods` property can be used to limit the route to a particular HTTP method.\n# handler.add \"new_article\", ART::Route.new(\"/article\", methods: \"post\") do |ctx|\n#   pp ctx.request.body.try &.gets_to_end\n# end\n#\n# # The match parameters from the route are passed to the callback as an `ART::Parameters`.\n# handler.add \"article\", ART::Route.new(\"/article/{id<\\\\d+>}\", methods: \"get\") do |ctx, params|\n#   pp params # => ART::Parameters{\"_route\" => \"article\", \"id\" => \"10\"}\n# end\n#\n# # Call the `#compile` method when providing the handler to the handler array.\n# server = HTTP::Server.new([\n#   handler.compile,\n# ])\n#\n# address = server.bind_tcp 8080\n# puts \"Listening on http://#{address}\"\n# server.listen\n# ```\n#\n# NOTE: This handler should be the last one, as it is terminal.\n#\n# ## Bubbling Exceptions\n#\n# By default, requests that result in an exception, either from `Athena::Routing` or the callback block itself,\n# are gracefully handled by returning a proper error response to the client via [HTTP::Server::Response#respond_with_status](https://crystal-lang.org/api/HTTP/Server/Response.html#respond_with_status%28status%3AHTTP%3A%3AStatus%2Cmessage%3AString%3F%3Dnil%29%3ANil-instance-method).\n#\n# You can set `bubble_exceptions: true` when instantiating the routing handler to have full control over the returned response.\n# This would allow you to define your own [HTTP::Handler](https://crystal-lang.org/api/HTTP/Handler.html) that can rescue the exceptions and apply your custom logic for how to handle the error.\n#\n# ```\n# class ErrorHandler\n#   include HTTP::Handler\n#\n#   def call(context)\n#     call_next context\n#   rescue ex\n#     # Do something based on the ex, such as rendering the appropriate template, etc.\n#   end\n# end\n#\n# handler = ART::RoutingHandler.new bubble_exceptions: true\n#\n# # Add the routes...\n#\n# # Have the `ErrorHandler` run _before_ the routing handler.\n# server = HTTP::Server.new([\n#   ErrorHandler.new,\n#   handler.compile,\n# ])\n#\n# address = server.bind_tcp 8080\n# puts \"Listening on http://#{address}\"\n# server.listen\n# ```\nclass Athena::Routing::RoutingHandler\n  include ::HTTP::Handler\n\n  private LOG = Log.for \"athena.routing\"\n\n  # :nodoc:\n  class RouteProvider < ::Athena::Routing::RouteProvider\n  end\n\n  @handlers : Hash(String, Proc(::HTTP::Server::Context, ART::Parameters, Nil)) = {} of String => ::HTTP::Server::Context, ART::Parameters -> Nil\n\n  # :nodoc:\n  forward_missing_to @collection\n\n  @collection : ART::RouteCollection\n\n  def initialize(\n    matcher : ART::Matcher::URLMatcherInterface? = nil,\n    @collection : ART::RouteCollection = ART::RouteCollection.new,\n    @bubble_exceptions : Bool = false,\n  )\n    @matcher = matcher || ART::Matcher::URLMatcher.new ART::RequestContext.new, RouteProvider\n  end\n\n  # :inherit:\n  def call(context)\n    request : ART::Request\n\n    {% if @top_level.has_constant?(\"Athena\") && Athena.has_constant?(\"HTTP\") && Athena::HTTP.has_constant?(\"Request\") %}\n      request = AHTTP::Request.new context.request\n    {% else %}\n      request = context.request\n    {% end %}\n\n    @matcher.context.apply request\n\n    begin\n      parameters = if @matcher.is_a? ART::Matcher::RequestMatcherInterface\n                     @matcher.match request\n                   else\n                     @matcher.match request.path\n                   end\n    rescue ex : ART::Exception::ResourceNotFound\n      raise ex if @bubble_exceptions\n      return context.response.respond_with_status(:not_found)\n    rescue ex : ART::Exception::MethodNotAllowed\n      raise ex if @bubble_exceptions\n      return context.response.respond_with_status(:method_not_allowed)\n    end\n\n    begin\n      @handlers[parameters[\"_route\"]].call context, parameters\n    rescue ex : ::Exception\n      raise ex if @bubble_exceptions\n      LOG.error(exception: ex) { \"Unhandled exception\" }\n      context.response.respond_with_status(:internal_server_error)\n    end\n  end\n\n  # :nodoc:\n  def add(collection : ART::RouteCollection) : NoReturn\n    raise ArgumentError.new \"Cannot add an existing collection to a routing handler.\"\n  end\n\n  # Adds the provided *route* with the provided *name* to this collection, optionally with the provided *priority*.\n  # The passed *block* will be called when a request matching this route is encountered.\n  def add(name : String, route : ART::Route, priority : Int32 = 0, &block : ::HTTP::Server::Context, ART::Parameters -> Nil) : Nil\n    @handlers[name] = block\n    @collection.add name, route, priority\n  end\n\n  # Helper method that calls `ART.compile` with the internal `ART::RouteCollection`,\n  # and returns `self` to make setting up the routes easier.\n  #\n  # ```\n  # handler = ART::RoutingHandler.new\n  #\n  # # Register routes\n  #\n  # server = HTTP::Server.new([\n  #   handler.compile,\n  # ])\n  # ```\n  def compile : self\n    ART.compile @collection, route_provider: RouteProvider\n\n    self\n  end\nend\n"
  },
  {
    "path": "src/components/routing/src/static_prefix_collection.cr",
    "content": "class Athena::Routing::RouteProvider; end\n\n# :nodoc:\nclass Athena::Routing::RouteProvider::StaticPrefixCollection\n  # :nodoc:\n  #\n  # name, regex pattern, variables, route, trailing slash?, trailing var?\n  record StaticPrefixTreeRoute, name : String, pattern : String, variables : Set(String), route : ART::Route, has_trailing_slash : Bool, has_trailing_var : Bool\n\n  # :nodoc:\n  record StaticTreeNamedRoute, name : String, route : ART::Route\n  record StaticTreeName, name : String\n\n  private alias RouteInfo = Array(StaticTreeNamedRoute | StaticPrefixTreeRoute | StaticTreeName | self)\n\n  protected def self.handle_error?(message : String) : Bool\n    message.starts_with?(\"lookbehind assertion is not fixed length\") ||\n      message.starts_with?(\"length of lookbehind assertion is not limited\")\n  end\n\n  getter prefix : String\n  getter items : RouteInfo = RouteInfo.new\n\n  protected setter items : RouteInfo\n\n  protected getter static_prefixes = Array(String).new\n  protected getter prefixes = Array(String).new\n\n  def initialize(@prefix : String = \"/\"); end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  def add_route(prefix : String, route : StaticTreeNamedRoute | StaticPrefixTreeRoute | StaticTreeName | self) : Nil\n    prefix, static_prefix = self.common_prefix prefix, prefix\n\n    idx = @items.size - 1\n    while 0 <= idx\n      item = @items[idx]\n\n      common_prefix, common_static_prefix = self.common_prefix prefix, @prefixes[idx]\n\n      if @prefix == common_prefix\n        if @prefix != static_prefix && @prefix != @static_prefixes[idx]\n          idx -= 1\n          next\n        end\n\n        break if @prefix == static_prefix && @prefix == @static_prefixes[idx]\n        break if @prefixes[idx] != @static_prefixes[idx] && @prefix == @static_prefixes[idx]\n        break if prefix != static_prefix && @prefix == static_prefix\n\n        idx -= 1\n\n        next\n      end\n\n      if item.is_a? self && @prefixes[idx] == common_prefix\n        item.add_route prefix, route\n      else\n        child = self.class.new common_prefix\n        common_child_prefix, common_child_static_prefix = child.common_prefix @prefixes[idx], @prefixes[idx]\n        child.prefixes << common_child_prefix\n        child.static_prefixes << common_child_static_prefix\n\n        common_child_prefix, common_child_static_prefix = child.common_prefix prefix, prefix\n        child.prefixes << common_child_prefix\n        child.static_prefixes << common_child_static_prefix\n\n        child.items << @items[idx]\n        child.items << route\n\n        @static_prefixes[idx] = common_static_prefix\n        @prefixes[idx] = common_prefix\n        @items[idx] = child\n      end\n\n      return\n    end\n\n    @static_prefixes << static_prefix\n    @prefixes << prefix\n    @items << route\n  end\n\n  def populate_collection(routes : ART::RouteCollection) : ART::RouteCollection\n    @items.each do |item|\n      case item\n      in ART::RouteProvider::StaticPrefixCollection then item.populate_collection routes\n      in StaticTreeNamedRoute                       then routes.add item.name, item.route\n      in StaticPrefixTreeRoute, StaticTreeName\n        # Skip\n      end\n    end\n\n    routes\n  end\n\n  # ameba:disable Metrics/CyclomaticComplexity\n  protected def common_prefix(prefix : String, other_prefix : String) : Tuple(String, String)\n    base_length = @prefix.size\n    end_size = Math.min(prefix.size, other_prefix.size)\n    static_length = nil\n\n    idx = base_length\n    begin\n      while idx < end_size && prefix[idx] == other_prefix[idx]\n        if '(' == prefix[idx]\n          static_length = static_length || idx\n          jdx = 1 + idx\n          n = 1\n\n          should_break = while jdx < end_size && 0 < n\n            break true if prefix[jdx] != other_prefix[jdx]\n\n            if '(' == prefix[jdx]\n              n += 1\n            elsif ')' == prefix[jdx]\n              n -= 1\n            elsif '\\\\' == prefix[jdx] && ((jdx += 1) == end_size || prefix[jdx] != other_prefix[jdx])\n              jdx -= 1\n              break false\n            end\n\n            jdx += 1\n          end\n\n          break if should_break\n          break if 0 < n\n          break if ('?' == (prefix[jdx]? || \"\") || '?' == (other_prefix[jdx]? || \"\")) && ((prefix[jdx]? || \"\") != (other_prefix[jdx]? || \"\"))\n\n          sub_pattern = prefix[idx, jdx - idx]\n\n          break if prefix != other_prefix && !sub_pattern.matches?(/^\\(\\[[^\\]]++\\]\\+\\+\\)$/) && !\"\".matches?(/(?<!#{sub_pattern})/)\n\n          idx = jdx - 1\n        elsif '\\\\' == prefix[idx] && ((idx += 1) == end_size || prefix[idx] != other_prefix[idx])\n          idx -= 1\n          break\n        end\n\n        idx += 1\n      end\n    rescue e : ArgumentError\n      if !(msg = e.message) || !self.class.handle_error?(msg)\n        raise e\n      end\n    end\n\n    {prefix[0, idx], prefix[0, static_length || idx]}\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/serializer/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/serializer/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.4.3] - 2026-04-19\n\n### Changed\n\n- Improve compile time error messages ([#646]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Removed\n\n- Remove `ASR::PropertyMetadata#class` method and generic variable ([#672]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.3]: https://github.com/athena-framework/serializer/releases/tag/v0.4.3\n[#646]: https://github.com/athena-framework/athena/pull/646\n[#672]: https://github.com/athena-framework/athena/pull/672\n\n## [0.4.2] - 2025-08-12\n\n### Fixed\n\n- Fix nightly type incompatibility with `ASR::Any` ([#562]) (George Dietrich)\n\n[0.4.2]: https://github.com/athena-framework/serializer/releases/tag/v0.4.2\n[#562]: https://github.com/athena-framework/athena/pull/562\n\n## [0.4.1] - 2025-02-08\n\n### Fixed\n\n- Fix serialization of value when its type is different type than the ivar ([#514]) (George Dietrich)\n\n[0.4.1]: https://github.com/athena-framework/serializer/releases/tag/v0.4.1\n[#514]: https://github.com/athena-framework/athena/pull/514\n\n## [0.4.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.13.0` ([#428]) (George Dietrich)\n\n[0.4.0]: https://github.com/athena-framework/serializer/releases/tag/v0.4.0\n[#428]: https://github.com/athena-framework/athena/pull/428\n\n## [0.3.6] - 2024-04-27\n\n### Fixed\n\n- Fix misnamed modules being defined in incorrect namespace ([#402]) (George Dietrich)\n\n[0.3.6]: https://github.com/athena-framework/serializer/releases/tag/v0.3.6\n[#402]: https://github.com/athena-framework/athena/pull/402\n\n## [0.3.5] - 2024-04-09\n\n### Changed\n\n- Change `Config` dependency to `DependencyInjection` for the custom annotation feature ([#392]) (George Dietrich)\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.3.5]: https://github.com/athena-framework/serializer/releases/tag/v0.3.5\n[#392]: https://github.com/athena-framework/athena/pull/392\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.3.4] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.3.4]: https://github.com/athena-framework/serializer/releases/tag/v0.3.4\n\n## [0.3.3] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/serializer/releases/tag/v0.3.3\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.3.2] - 2023-01-07\n\n### Fixed\n\n- Fix deserializing `JSON::Any` and `YAML::Any` ([#215]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/serializer/releases/tag/v0.3.2\n[#215]: https://github.com/athena-framework/athena/pull/215\n\n## [0.3.1] - 2022-09-05\n\n### Changed\n\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/serializer/releases/tag/v0.3.1\n[#188]: https://github.com/athena-framework/athena/pull/188\n\n## [0.3.0] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Added\n\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** change serialization of [Enums](https://crystal-lang.org/api/Enum.html) to underscored strings by default ([#173]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Fixed\n\n- Fix compiler error when trying to deserialize a `Hash` ([#165]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/serializer/releases/tag/v0.3.0\n[#165]: https://github.com/athena-framework/athena/pull/165\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n[#173]: https://github.com/athena-framework/athena/pull/173\n\n## [0.2.10] - 2021-11-12\n\n### Fixed\n\n- Fix issue with empty YAML input ([#22]) (George Dietrich)\n\n[0.2.10]: https://github.com/athena-framework/serializer/releases/tag/v0.2.10\n[#22]: https://github.com/athena-framework/serializer/pull/22\n\n## [0.2.9] - 2021-10-30\n\n### Added\n\n- Add `VERSION` constant to `Athena::Serializer` namespace ([#20]) (George Dietrich)\n\n### Fixed\n\n- Fix broken type link ([#19]) (George Dietrich)\n\n[0.2.9]: https://github.com/athena-framework/serializer/releases/tag/v0.2.9\n[#19]: https://github.com/athena-framework/serializer/pull/19\n[#20]: https://github.com/athena-framework/serializer/pull/20\n\n## [0.2.8] - 2021-05-17\n\n### Fixed\n\n- Fixes incorrect `nil` check in macro logic ([#17]) (George Dietrich)\n\n[0.2.8]: https://github.com/athena-framework/serializer/releases/tag/v0.2.8\n[#17]: https://github.com/athena-framework/serializer/pull/17\n\n## [0.2.7] - 2021-04-09\n\n### Added\n\n- Add some more specialized exception types ([#16]) (George Dietrich)\n\n[0.2.7]: https://github.com/athena-framework/serializer/releases/tag/v0.2.7\n[#16]: https://github.com/athena-framework/serializer/pull/16\n\n## [0.2.6] - 2021-03-16\n\n### Added\n\n- Expose a setter for `ASR::Context#version=` ([#15]) (George Dietrich)\n\n### Changed\n\n- Change `athena-framework/config` version constraint to `>= 2.0.0` ([#15]) (George Dietrich)\n\n[0.2.6]: https://github.com/athena-framework/serializer/releases/tag/v0.2.6\n[#15]: https://github.com/athena-framework/serializer/pull/15\n\n## [0.2.5] - 2021-01-29\n\n### Changed\n\n- Migrate documentation to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#14]) (George Dietrich)\n\n[0.2.5]: https://github.com/athena-framework/serializer/releases/tag/v0.2.5\n[#14]: https://github.com/athena-framework/serializer/pull/14\n\n## [0.2.4] - 2021-01-29\n\n### Changed\n\n- Bump min `athena-framework/config` version to `~> 2.0.0` ([#13]) (George Dietrich)\n\n[0.2.4]: https://github.com/athena-framework/serializer/releases/tag/v0.2.4\n[#13]: https://github.com/athena-framework/serializer/pull/13\n\n## [0.2.3] - 2021-01-20\n\n### Fixed\n\n- Fix since/until and group annotations not working for virtual properties ([#12]) (George Dietrich)\n\n[0.2.3]: https://github.com/athena-framework/serializer/releases/tag/v0.2.3\n[#12]: https://github.com/athena-framework/serializer/pull/12\n\n## [0.2.2] - 2020-12-03\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#11]) (George Dietrich)\n\n[0.2.2]: https://github.com/athena-framework/serializer/releases/tag/v0.2.2\n[#11]: https://github.com/athena-framework/serializer/pull/11\n\n## [0.2.1] - 2020-11-08\n\n### Added\n\n- Add deserialization support to `ASRA::Name` ([#9]) (Joakim Repomaa)\n\n[0.2.1]: https://github.com/athena-framework/serializer/releases/tag/v0.2.1\n[#9]: https://github.com/athena-framework/serializer/pull/9\n\n## [0.2.0] - 2020-07-08\n\n### Added\n\n- Add dependency on `athena-framework/config` ([#8]) (George Dietrich)\n- Add ability to use custom annotations within [exclusion strategies](https://athenaframework.org/Serializer/ExclusionStrategies/ExclusionStrategyInterface/#Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface--annotation-configurations) ([#8]) (George Dietrich)\n- Add [ASR::Context#direction](https://athenaframework.org/Serializer/Context/#Athena::Serializer::Context#direction) to represent which direction the context object represents ([#8]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/serializer/releases/tag/v0.2.0\n[#8]: https://github.com/athena-framework/serializer/pull/8\n\n## [0.1.3] - 2020-07-08\n\n### Fixed\n\n- Fix overflow error when deserializing `Int64` values ([#7]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/serializer/releases/tag/v0.1.3\n[#7]: https://github.com/athena-framework/serializer/pull/7\n\n## [0.1.2] - 2020-07-05\n\n### Added\n\n- Add improved documentation to various types ([#6]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/serializer/releases/tag/v0.1.2\n[#6]: https://github.com/athena-framework/serializer/pull/6\n\n## [0.1.1] - 2020-06-27\n\n### Added\n\n- Add [naming strategies](https://athenaframework.org/Serializer/Annotations/Name/#Athena::Serializer::Annotations::Name--naming-strategies) to `ASRA::Name` ([#5]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/serializer/releases/tag/v0.1.1\n[#5]: https://github.com/athena-framework/serializer/pull/5\n\n## [0.1.0] - 2020-06-23\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/serializer/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/serializer/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/serializer/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020 George Dietrich\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": "src/components/serializer/README.md",
    "content": "# Serializer\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/serializer.svg)](https://github.com/athena-framework/serializer/releases)\n\nFlexible object (de)serialization library\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Serializer).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/serializer/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.4.0\n\n### Normalization of Exception types\n\nThe namespace exception types live in has changed from `ASR::Exceptions` to `ASR::Exception`.\nAny usages of `serializer` exception types will need to be updated.\n\nSome additional types have also been removed/renamed:\n\n* `ASR::Exceptions::SerializerException` has been removed in favor of using `ASR::Exception` directly\n\nIf using a `rescue` statement with a parent exception type, either from the `serializer` component or Crystal stdlib, double check it to ensure it'll still rescue what you are expecting it will.\n"
  },
  {
    "path": "src/components/serializer/docs/README.md",
    "content": "The `Athena::Serializer` component provides enhanced (de)serialization features,\nwith most leveraging [annotations](https://crystal-lang.org/reference/syntax_and_semantics/annotations/index.html).\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-serializer:\n    github: athena-framework/serializer\n    version: ~> 0.4.0\n```\n\n## Usage\n\nThe `Athena::Serializer` component focuses around [ASR::Serializer](/Serializer/Serializer/) implementations, with the default being [ASR::Serializer] as the main entrypoint into (de)serializing objects.\nUsage wise, the component functions much like the `*::Serializable` modules in the stdlib, such as [JSON::Serializable](https://crystal-lang.org/api/JSON/Serializable.html).\nThe [ASR::Serializable](/Serializer/Serializable/) module can be included into a type to make it (de)serializable.\nFrom here various [annotations](/Serializer/Annotations/) may be used to control how the object is (de)serialized.\n\n```crystal\n# ExclusionPolicy specifies that all properties should not be (de)serialized\n# unless exposed via the `ASRA::Expose` annotation.\n@[ASRA::ExclusionPolicy(:all)]\n@[ASRA::AccessorOrder(:alphabetical)]\nclass Example\n  include ASR::Serializable\n\n  # Groups can be used to create different \"views\" of a type.\n  @[ASRA::Expose]\n  @[ASRA::Groups(\"details\")]\n  property name : String\n\n  # The `ASRA::Name` controls the name that this property\n  # should be deserialized from or be serialized to.\n  # It can also be used to set the default serialized naming strategy on the type.\n  @[ASRA::Expose]\n  @[ASRA::Name(deserialize: \"a_prop\", serialize: \"a_prop\")]\n  property some_prop : String\n\n  # Define a custom accessor used to get the value for serialization.\n  @[ASRA::Expose]\n  @[ASRA::Groups(\"default\", \"details\")]\n  @[ASRA::Accessor(getter: get_title)]\n  property title : String\n\n  # ReadOnly properties cannot be set on deserialization\n  @[ASRA::Expose]\n  @[ASRA::ReadOnly]\n  property created_at : Time = Time.utc\n\n  # Allows the property to be set via deserialization,\n  # but not exposed when serialized.\n  @[ASRA::IgnoreOnSerialize]\n  property password : String?\n\n  # Because of the `:all` exclusion policy, and not having the `ASRA::Expose` annotation,\n  # these properties are not exposed.\n  getter first_name : String?\n  getter last_name : String?\n\n  # Runs directly after `self` is deserialized\n  @[ASRA::PostDeserialize]\n  def split_name : Nil\n    @first_name, @last_name = @name.split(' ')\n  end\n\n  # Allows using the return value of a method as a key/value in the serialized output.\n  @[ASRA::VirtualProperty]\n  def get_val : String\n    \"VAL\"\n  end\n\n  private def get_title : String\n    @title.downcase\n  end\nend\n\nobj = ASR.serializer.deserialize Example,\n  %({\"name\":\"FIRST LAST\",\"a_prop\":\"STR\",\"title\":\"TITLE\",\"password\":\"monkey123\",\"created_at\":\"2020-10-10T12:34:56Z\"}), :json\n\nobj\n# => #<Example:0x7f3e3b106740 @created_at=2020-07-05 23:06:58.943298289 UTC, @name=\"FIRST LAST\",\n        @some_prop=\"STR\", @title=\"TITLE\", @password=\"monkey123\", @first_name=\"FIRST\", @last_name=\"LAST\">\n\nASR.serializer.serialize obj, :json\n# => {\"a_prop\":\"STR\",\"created_at\":\"2020-07-05T23:06:58.94Z\",\"get_val\":\"VAL\",\"name\":\"FIRST LAST\",\"title\":\"title\"}\n\nASR.serializer.serialize obj, :json, ASR::SerializationContext.new.groups = [\"details\"]\n# => {\"name\":\"FIRST LAST\",\"title\":\"title\"}\n```\n\n## Learn More\n\n* Customize how objects are [constructed](/Serializer/ObjectConstructorInterface/)\n* Make use of inheritance with [ASR::Model](/Serializer/Model/)s\n* Conditionally determine which (if any) properties should be [excluded](/Serializer/ExclusionStrategies/ExclusionStrategyInterface/)\n"
  },
  {
    "path": "src/components/serializer/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Serializer\nsite_url: https://athenaframework.org/Serializer/\nrepo_url: https://github.com/athena-framework/serializer\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-dependency_injection/src/athena-dependency_injection.cr\n            - ./lib/athena-serializer/src/athena-serializer.cr\n          source_locations:\n            lib/athena-serializer: https://github.com/athena-framework/serializer/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/serializer/shard.yml",
    "content": "name: athena-serializer\n\nversion: 0.4.3\n\ncrystal: ~> 1.19\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/serializer\n\ndocumentation: https://athenaframework.org/Serializer\n\ndescription: |\n  Object (de)serialization library.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\ndependencies:\n  athena-dependency_injection:\n    github: athena-framework/dependency-injection\n    version: '>= 0.4.0'\n"
  },
  {
    "path": "src/components/serializer/spec/athena-serializer_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe ASR::Serializable do\n  describe \"#serialization_properties\" do\n    describe ASRA::Accessor do\n      it \"should use the value of the method\" do\n        properties = GetterAccessor.new.serialization_properties\n        properties.size.should eq 1\n\n        p = properties[0]\n\n        p.name.should eq \"foo\"\n        p.external_name.should eq \"foo\"\n        p.value.should eq \"FOO\"\n        p.skip_when_empty?.should be_false\n        p.type.should eq String\n      end\n\n      it \"handles when getter value has a diff type than ivar\" do\n        properties = GetterAccessorDiffType.new.serialization_properties\n        properties.size.should eq 1\n\n        p = properties[0]\n\n        p.name.should eq \"value\"\n        p.external_name.should eq \"value\"\n        p.value.should eq \"100\"\n        p.value.should be_a String\n        p.skip_when_empty?.should be_false\n        p.type.should eq Int32\n      end\n    end\n\n    describe ASRA::AccessorOrder do\n      describe :default do\n        it \"should used the order in which the properties were defined\" do\n          properties = Default.new.serialization_properties\n          properties.size.should eq 6\n\n          properties.map(&.name).should eq %w(a z two one a_a get_val)\n          properties.map(&.external_name).should eq %w(a z two one a_a get_val)\n        end\n      end\n\n      describe :alphabetical do\n        it \"should order the properties alphabetically by their name\" do\n          properties = Abc.new.serialization_properties\n          properties.size.should eq 6\n\n          properties.map(&.name).should eq %w(a a_a get_val one zzz z)\n          properties.map(&.external_name).should eq %w(a a_a get_val one two z)\n        end\n      end\n\n      describe :custom do\n        it \"should use the order defined by the user\" do\n          properties = Custom.new.serialization_properties\n          properties.size.should eq 6\n\n          properties.map(&.name).should eq %w(two z get_val a one a_a)\n          properties.map(&.external_name).should eq %w(two z get_val a one a_a)\n        end\n      end\n    end\n\n    describe ASRA::Skip do\n      it \"should not include skipped properties\" do\n        properties = Skip.new.serialization_properties\n        properties.size.should eq 1\n\n        p = properties[0]\n\n        p.name.should eq \"one\"\n        p.external_name.should eq \"one\"\n        p.value.should eq \"one\"\n        p.skip_when_empty?.should be_false\n        p.type.should eq String\n      end\n    end\n\n    describe ASRA::ExclusionPolicy do\n      describe :all do\n        describe ASRA::Expose do\n          it \"should only return properties that are exposed\" do\n            properties = Expose.new.serialization_properties\n            properties.size.should eq 1\n\n            p = properties[0]\n\n            p.name.should eq \"name\"\n            p.external_name.should eq \"name\"\n            p.value.should eq \"Jim\"\n            p.skip_when_empty?.should be_false\n            p.type.should eq String\n          end\n        end\n      end\n\n      describe :none do\n        describe ASRA::Exclude do\n          it \"should only return properties that are not excluded\" do\n            properties = Exclude.new.serialization_properties\n            properties.size.should eq 1\n\n            p = properties[0]\n\n            p.name.should eq \"name\"\n            p.external_name.should eq \"name\"\n            p.value.should eq \"Jim\"\n            p.skip_when_empty?.should be_false\n            p.type.should eq String\n          end\n        end\n      end\n    end\n\n    describe ASRA::Name do\n      describe :serialize do\n        it \"should use the value in the annotation or property name if it wasnt defined\" do\n          properties = SerializedName.new.serialization_properties\n          properties.size.should eq 3\n\n          p = properties[0]\n\n          p.name.should eq \"my_home_address\"\n          p.external_name.should eq \"myAddress\"\n          p.value.should eq \"123 Fake Street\"\n          p.skip_when_empty?.should be_false\n          p.type.should eq String\n\n          p = properties[1]\n\n          p.name.should eq \"value\"\n          p.external_name.should eq \"a_value\"\n          p.value.should eq \"str\"\n          p.skip_when_empty?.should be_false\n          p.type.should eq String\n\n          p = properties[2]\n\n          p.name.should eq \"myZipCode\"\n          p.external_name.should eq \"myZipCode\"\n          p.value.should eq 90210\n          p.skip_when_empty?.should be_false\n          p.type.should eq Int32\n        end\n      end\n\n      describe :deserialize do\n        it \"should use the value in the annotation or property name if it wasnt defined\" do\n          properties = DeserializedName.deserialization_properties\n          properties.size.should eq 2\n\n          p = properties[0]\n\n          p.name.should eq \"custom_name\"\n          p.external_name.should eq \"des\"\n          p.skip_when_empty?.should be_false\n          p.type.should eq Int32?\n\n          p = properties[1]\n\n          p.name.should eq \"default_name\"\n          p.external_name.should eq \"default_name\"\n          p.skip_when_empty?.should be_false\n          p.type.should eq Bool?\n        end\n      end\n\n      describe :key do\n        it \"should use the value in the annotation or property name if it wasnt defined\" do\n          both_properties = [\n            SerializedNameKey.new.serialization_properties,\n            SerializedNameKey.deserialization_properties,\n          ]\n\n          both_properties.each do |properties|\n            properties.size.should eq 3\n\n            p = properties[0]\n\n            p.name.should eq \"my_home_address\"\n            p.external_name.should eq \"myAddress\"\n            p.skip_when_empty?.should be_false\n            p.type.should eq String\n\n            p = properties[1]\n\n            p.name.should eq \"value\"\n            p.external_name.should eq \"some_key\"\n            p.skip_when_empty?.should be_false\n            p.type.should eq String\n\n            p = properties[2]\n\n            p.name.should eq \"myZipCode\"\n            p.external_name.should eq \"myZipCode\"\n            p.skip_when_empty?.should be_false\n            p.type.should eq Int32\n          end\n        end\n      end\n\n      describe :serialization_strategy do\n        it :camelcase do\n          properties = SerializedNameCamelcaseSerializationStrategy.new.serialization_properties\n          properties.size.should eq 3\n\n          p = properties[0]\n\n          p.name.should eq \"my_home_address\"\n          p.external_name.should eq \"myAdd_ress\"\n\n          p = properties[1]\n\n          p.name.should eq \"two_wOrds\"\n          p.external_name.should eq \"twoWOrds\"\n\n          p = properties[2]\n\n          p.name.should eq \"myZipCode\"\n          p.external_name.should eq \"myZipCode\"\n        end\n\n        it :underscore do\n          properties = SerializedNameUnderscoreSerializationStrategy.new.serialization_properties\n          properties.size.should eq 3\n\n          p = properties[0]\n\n          p.name.should eq \"my_home_address\"\n          p.external_name.should eq \"myAdd_ress\"\n\n          p = properties[1]\n\n          p.name.should eq \"two_wOrds\"\n          p.external_name.should eq \"two_w_ords\"\n\n          p = properties[2]\n\n          p.name.should eq \"myZipCode\"\n          p.external_name.should eq \"my_zip_code\"\n        end\n\n        it :identical do\n          properties = SerializedNameIdenticalSerializationStrategy.new.serialization_properties\n          properties.size.should eq 3\n\n          p = properties[0]\n\n          p.name.should eq \"my_home_address\"\n          p.external_name.should eq \"myAdd_ress\"\n\n          p = properties[1]\n\n          p.name.should eq \"two_wOrds\"\n          p.external_name.should eq \"two_wOrds\"\n\n          p = properties[2]\n\n          p.name.should eq \"myZipCode\"\n          p.external_name.should eq \"myZipCode\"\n        end\n      end\n\n      describe :deserialization_strategy do\n        it :camelcase do\n          properties = DeserializedNameCamelcaseDeserializationStrategy.deserialization_properties\n          properties.size.should eq 3\n\n          p = properties[0]\n\n          p.name.should eq \"my_home_address\"\n          p.external_name.should eq \"myAdd_ress\"\n\n          p = properties[1]\n\n          p.name.should eq \"two_wOrds\"\n          p.external_name.should eq \"twoWOrds\"\n\n          p = properties[2]\n\n          p.name.should eq \"myZipCode\"\n          p.external_name.should eq \"myZipCode\"\n        end\n\n        it :underscore do\n          properties = DeserializedNameUnderscoreDeserializationStrategy.deserialization_properties\n          properties.size.should eq 3\n\n          p = properties[0]\n\n          p.name.should eq \"my_home_address\"\n          p.external_name.should eq \"myAdd_ress\"\n\n          p = properties[1]\n\n          p.name.should eq \"two_wOrds\"\n          p.external_name.should eq \"two_w_ords\"\n\n          p = properties[2]\n\n          p.name.should eq \"myZipCode\"\n          p.external_name.should eq \"my_zip_code\"\n        end\n\n        it :identical do\n          properties = DeserializedNameIdenticalDeserializationStrategy.deserialization_properties\n          properties.size.should eq 3\n\n          p = properties[0]\n\n          p.name.should eq \"my_home_address\"\n          p.external_name.should eq \"myAdd_ress\"\n\n          p = properties[1]\n\n          p.name.should eq \"two_wOrds\"\n          p.external_name.should eq \"two_wOrds\"\n\n          p = properties[2]\n\n          p.name.should eq \"myZipCode\"\n          p.external_name.should eq \"myZipCode\"\n        end\n      end\n\n      describe :strategy do\n        it :camelcase do\n          both_properties = [\n            SerializedNameCamelcaseStrategy.new.serialization_properties,\n            SerializedNameCamelcaseStrategy.deserialization_properties,\n          ]\n\n          both_properties.each do |properties|\n            properties.size.should eq 3\n\n            p = properties[0]\n\n            p.name.should eq \"my_home_address\"\n            p.external_name.should eq \"myAdd_ress\"\n\n            p = properties[1]\n\n            p.name.should eq \"two_wOrds\"\n            p.external_name.should eq \"twoWOrds\"\n\n            p = properties[2]\n\n            p.name.should eq \"myZipCode\"\n            p.external_name.should eq \"myZipCode\"\n          end\n        end\n\n        it :underscore do\n          both_properties = [\n            SerializedNameUnderscoreStrategy.new.serialization_properties,\n            SerializedNameUnderscoreStrategy.deserialization_properties,\n          ]\n\n          both_properties.each do |properties|\n            properties.size.should eq 3\n\n            p = properties[0]\n\n            p.name.should eq \"my_home_address\"\n            p.external_name.should eq \"myAdd_ress\"\n\n            p = properties[1]\n\n            p.name.should eq \"two_wOrds\"\n            p.external_name.should eq \"two_w_ords\"\n\n            p = properties[2]\n\n            p.name.should eq \"myZipCode\"\n            p.external_name.should eq \"my_zip_code\"\n          end\n        end\n\n        it :identical do\n          both_properties = [\n            SerializedNameIdenticalStrategy.new.serialization_properties,\n            SerializedNameIdenticalStrategy.deserialization_properties,\n          ]\n\n          both_properties.each do |properties|\n            properties.size.should eq 3\n\n            p = properties[0]\n\n            p.name.should eq \"my_home_address\"\n            p.external_name.should eq \"myAdd_ress\"\n\n            p = properties[1]\n\n            p.name.should eq \"two_wOrds\"\n            p.external_name.should eq \"two_wOrds\"\n\n            p = properties[2]\n\n            p.name.should eq \"myZipCode\"\n            p.external_name.should eq \"myZipCode\"\n          end\n        end\n      end\n    end\n\n    describe ASRA::SkipWhenEmpty do\n      it \"should use the value of the method\" do\n        properties = SkipWhenEmpty.new.serialization_properties\n        properties.size.should eq 1\n\n        p = properties[0]\n\n        p.name.should eq \"value\"\n        p.external_name.should eq \"value\"\n        p.value.should eq \"value\"\n        p.skip_when_empty?.should be_true\n        p.type.should eq String\n      end\n    end\n\n    describe ASRA::VirtualProperty do\n      it \"should only return properties that are not excluded\" do\n        properties = VirtualProperty.new.serialization_properties\n        properties.size.should eq 3\n\n        p = properties[0]\n\n        p.name.should eq \"foo\"\n        p.groups.should eq Set{\"default\"}\n        p.since_version.should be_nil\n        p.until_version.should be_nil\n        p.external_name.should eq \"foo\"\n        p.value.should eq \"foo\"\n        p.skip_when_empty?.should be_false\n        p.type.should eq String\n\n        p = properties[1]\n\n        p.name.should eq \"get_val\"\n        p.groups.should eq Set{\"default\"}\n        p.since_version.should be_nil\n        p.until_version.should be_nil\n        p.external_name.should eq \"get_val\"\n        p.value.should eq \"VAL\"\n        p.skip_when_empty?.should be_false\n        p.type.should eq String\n\n        p = properties[2]\n\n        p.name.should eq \"group_version\"\n        p.groups.should eq Set{\"group1\"}\n        p.since_version.should eq SemanticVersion.parse \"1.3.2\"\n        p.until_version.should eq SemanticVersion.parse \"1.2.3\"\n        p.external_name.should eq \"group_version\"\n        p.value.should eq \"group_version\"\n        p.skip_when_empty?.should be_false\n        p.type.should eq String\n      end\n    end\n\n    describe ASRA::IgnoreOnSerialize do\n      it \"should not include ignored properties\" do\n        properties = IgnoreOnSerialize.new.serialization_properties\n        properties.size.should eq 1\n\n        p = properties[0]\n\n        p.name.should eq \"name\"\n        p.external_name.should eq \"name\"\n        p.value.should eq \"Fred\"\n        p.skip_when_empty?.should be_false\n        p.type.should eq String\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/compiler_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"athena-serializer\")\nend\n\ndescribe Athena::Serializer, tags: \"compiled\" do\n  describe \"compiler errors\" do\n    describe ASRA::Name do\n      it \"errors if an invalid strategy is used for deserialization\" do\n        assert_compile_time_error \"Invalid ASRA::Name strategy: ':invalid'.\", <<-'CR'\n          @[ASRA::Name(strategy: :invalid)]\n          class Foo\n            include ASR::Serializable\n\n            def initialize; end\n\n            property name : String = \"foo\"\n          end\n\n          Foo.deserialization_properties\n        CR\n      end\n    end\n\n    describe \"read-only properties\" do\n      it \"errors if a read-only property is not nilable and has no default value\" do\n        assert_compile_time_error \"is read-only but is not nilable nor has a default value\", <<-'CR'\n          class Foo\n            include ASR::Serializable\n\n            @[ASRA::ReadOnly]\n            property name : String\n          end\n\n          Foo.deserialization_properties\n        CR\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/exclusion_strategies/custom_strategy_spec.cr",
    "content": "require \"../spec_helper\"\n\nADI.configuration_annotation IsActiveProperty, active : Bool = true\n\nprivate struct ActivePropertyExclusionStrategy\n  include Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface\n\n  # :inherit:\n  def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool\n    return false if context.direction.deserialization?\n\n    ann_configs = metadata.annotation_configurations\n\n    ann_configs.has?(IsActiveProperty) && !ann_configs[IsActiveProperty].active\n  end\nend\n\n# Mainly testing `Athena::DependencyInjection` integration in regards to custom annotations accessible via the property metadata.\ndescribe ActivePropertyExclusionStrategy do\n  describe \"#skip_property?\" do\n    describe :deserialization do\n      it \"it should not skip\" do\n        ActivePropertyExclusionStrategy.new.skip_property?(create_metadata, ASR::DeserializationContext.new).should be_false\n      end\n    end\n\n    describe :serialization do\n      describe \"without the annotation\" do\n        it \"should not skip\" do\n          ActivePropertyExclusionStrategy.new.skip_property?(create_metadata, ASR::SerializationContext.new).should be_false\n        end\n      end\n\n      describe \"with the annotation\" do\n        it true do\n          ann_config = ADI::AnnotationConfigurations.new({IsActiveProperty => [IsActivePropertyConfiguration.new] of ADI::AnnotationConfigurations::ConfigurationBase})\n\n          ActivePropertyExclusionStrategy.new.skip_property?(create_metadata(annotation_configurations: ann_config), ASR::SerializationContext.new).should be_false\n        end\n\n        it false do\n          ann_config = ADI::AnnotationConfigurations.new({IsActiveProperty => [IsActivePropertyConfiguration.new(false)] of ADI::AnnotationConfigurations::ConfigurationBase})\n\n          ActivePropertyExclusionStrategy.new.skip_property?(create_metadata(annotation_configurations: ann_config), ASR::SerializationContext.new).should be_true\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/exclusion_strategies/group_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ASR::ExclusionStrategies::Groups do\n  describe \"#skip_property?\" do\n    describe \"that is in the default group\" do\n      it \"should not skip\" do\n        assert_groups(groups: [\"default\"]).should be_false\n      end\n    end\n\n    describe \"that includes at least one group\" do\n      it \"should not skip\" do\n        assert_groups(groups: [\"one\", \"two\"], metadata_groups: [\"two\", \"three\"]).should be_false\n      end\n    end\n\n    describe \"that does not include any group\" do\n      it \"should skip\" do\n        assert_groups(groups: [\"one\", \"two\"], metadata_groups: [\"three\", \"four\"]).should be_true\n      end\n    end\n\n    it \"splat argument\" do\n      ASR::ExclusionStrategies::Groups.new(\"one\", \"two\", \"three\").skip_property?(create_metadata(groups: [\"default\"]), ASR::SerializationContext.new).should be_true\n      ASR::ExclusionStrategies::Groups.new(\"one\", \"default\", \"three\").skip_property?(create_metadata(groups: [\"default\"]), ASR::SerializationContext.new).should be_false\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/exclusion_strategies/version_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ASR::ExclusionStrategies::Version do\n  describe \"#skip_property?\" do\n    describe :since_version do\n      describe \"that isnt set\" do\n        it \"should not skip\" do\n          assert_version.should be_false\n        end\n      end\n\n      describe \"that is less than the version\" do\n        it \"should not skip\" do\n          assert_version(since_version: \"0.31.0\").should be_false\n        end\n      end\n\n      describe \"that is equal than the version\" do\n        it \"should not skip\" do\n          assert_version(since_version: \"1.0.0\").should be_false\n        end\n      end\n\n      describe \"that is larger than the version\" do\n        it \"should skip\" do\n          assert_version(since_version: \"1.5.0\").should be_true\n        end\n      end\n    end\n\n    describe :until_version do\n      describe \"that isnt set\" do\n        it \"should not skip\" do\n          assert_version.should be_false\n        end\n      end\n\n      describe \"that is less than the version\" do\n        it \"should skip\" do\n          assert_version(until_version: \"0.31.0\").should be_true\n        end\n      end\n\n      describe \"that is equal than the version\" do\n        it \"should skip\" do\n          assert_version(until_version: \"1.0.0\").should be_true\n        end\n      end\n\n      describe \"that is larger than the version\" do\n        it \"should not skip\" do\n          assert_version(until_version: \"1.5.0\").should be_false\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/accessor.cr",
    "content": "class GetterAccessor\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::Accessor(getter: get_foo)]\n  @foo : String = \"foo\"\n\n  private def get_foo : String\n    @foo.upcase\n  end\nend\n\nclass GetterAccessorDiffType\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::Accessor(getter: get_value)]\n  @value : Int32 = 10\n\n  private def get_value : String\n    (@value * 10).to_s\n  end\nend\n\nclass SetterAccessor\n  include ASR::Serializable\n\n  @[ASRA::Accessor(setter: set_foo)]\n  getter foo : String\n\n  private def set_foo(foo : String) : String\n    foo.should eq \"foo\"\n    @foo = \"FOO\"\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/accessor_order.cr",
    "content": "class Default\n  include ASR::Serializable\n\n  def initialize; end\n\n  property a : String = \"A\"\n  property z : String = \"Z\"\n  property two : String = \"two\"\n  property one : String = \"one\"\n  property a_a : Int32 = 123\n\n  @[ASRA::VirtualProperty]\n  def get_val : String\n    \"VAL\"\n  end\nend\n\n@[ASRA::AccessorOrder(:alphabetical)]\nclass Abc\n  include ASR::Serializable\n\n  def initialize; end\n\n  property a : String = \"A\"\n  property z : String = \"Z\"\n  property one : String = \"one\"\n  property a_a : Int32 = 123\n\n  @[ASRA::Name(serialize: \"two\")]\n  property zzz : String = \"two\"\n\n  @[ASRA::VirtualProperty]\n  def get_val : String\n    \"VAL\"\n  end\nend\n\n@[ASRA::AccessorOrder(:custom, order: [\"two\", \"z\", \"get_val\", \"a\", \"one\", \"a_a\"])]\nclass Custom\n  include ASR::Serializable\n\n  def initialize; end\n\n  property a : String = \"A\"\n  property z : String = \"Z\"\n  property two : String = \"two\"\n  property one : String = \"one\"\n  property a_a : Int32 = 123\n\n  @[ASRA::VirtualProperty]\n  def get_val : String\n    \"VAL\"\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/basic.cr",
    "content": "require \"../spec_helper\"\n\nclass TestObject\n  include ASR::Serializable\n\n  def initialize; end\n\n  getter foo : Symbol = :foo\n  getter bar : Float32 = 12.1_f32\n  getter nest : NestedType = NestedType.new\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/discriminator.cr",
    "content": "@[ASRA::Discriminator(key: \"type\", map: {\"point\" => Point, \"circle\" => Circle})]\nabstract class Shape\n  include ASR::Serializable\n\n  property type : String\nend\n\nclass Point < Shape\n  property x : Int32\n  property y : Int32\nend\n\nclass Circle < Shape\n  property x : Int32\n  property y : Int32\n  property radius : Int32\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/emit_null.cr",
    "content": "class EmitNil\n  include ASR::Serializable\n\n  def initialize; end\n\n  property name : String?\n  property age : Int32 = 1\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/empty.cr",
    "content": "require \"../spec_helper\"\n\nclass EmptyObject\n  include ASR::Serializable\n\n  def initialize; end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/exclude.cr",
    "content": "@[ASRA::ExclusionPolicy(:none)]\nclass Exclude\n  include ASR::Serializable\n\n  def initialize; end\n\n  property name : String = \"Jim\"\n\n  @[ASRA::Exclude]\n  property password : String? = \"monkey\"\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/expose.cr",
    "content": "@[ASRA::ExclusionPolicy(:all)]\nclass Expose\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::Expose]\n  property name : String = \"Jim\"\n\n  property password : String? = \"monkey\"\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/groups.cr",
    "content": "class Group\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::Groups(\"list\", \"details\")]\n  property id : Int64 = 1\n\n  @[ASRA::Groups(\"list\")]\n  property comment_summaries : Array(String) = [\"Sentence 1.\", \"Sentence 2.\"]\n\n  @[ASRA::Groups(\"details\")]\n  property comments : Array(String) = [\"Sentence 1.  Another sentence.\", \"Sentence 2.  Some other stuff.\"]\n\n  property created_at : Time = Time.utc(2019, 1, 1)\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/ignore_on_deserialize.cr",
    "content": "class IgnoreOnDeserialize\n  include ASR::Serializable\n\n  property name : String = \"Fred\"\n\n  @[ASRA::IgnoreOnDeserialize]\n  property password : String = \"monkey\"\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/ignore_on_serialize.cr",
    "content": "class IgnoreOnSerialize\n  include ASR::Serializable\n\n  def initialize; end\n\n  property name : String = \"Fred\"\n\n  @[ASRA::IgnoreOnSerialize]\n  property password : String = \"monkey\"\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/name.cr",
    "content": "class SerializedName\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::Name(serialize: \"myAddress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  @[ASRA::Name(deserialize: \"some_key\", serialize: \"a_value\")]\n  property value : String = \"str\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\nclass SerializedNameKey\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::Name(key: \"myAddress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  @[ASRA::Name(key: \"some_key\")]\n  property value : String = \"str\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\n@[ASRA::Name(serialization_strategy: :camelcase)]\nclass SerializedNameCamelcaseSerializationStrategy\n  include ASR::Serializable\n\n  def initialize; end\n\n  # Is overridable\n  @[ASRA::Name(serialize: \"myAdd_ress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  # ameba:disable Naming/VariableNames\n  property two_wOrds : String = \"two words\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\n@[ASRA::Name(serialization_strategy: :underscore)]\nclass SerializedNameUnderscoreSerializationStrategy\n  include ASR::Serializable\n\n  def initialize; end\n\n  # Is overridable\n  @[ASRA::Name(serialize: \"myAdd_ress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  # ameba:disable Naming/VariableNames\n  property two_wOrds : String = \"two words\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\n@[ASRA::Name(serialization_strategy: :identical)]\nclass SerializedNameIdenticalSerializationStrategy\n  include ASR::Serializable\n\n  def initialize; end\n\n  # Is overridable\n  @[ASRA::Name(serialize: \"myAdd_ress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  # ameba:disable Naming/VariableNames\n  property two_wOrds : String = \"two words\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\n@[ASRA::Name(deserialization_strategy: :camelcase)]\nclass DeserializedNameCamelcaseDeserializationStrategy\n  include ASR::Serializable\n\n  def initialize; end\n\n  # Is overridable\n  @[ASRA::Name(deserialize: \"myAdd_ress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  # ameba:disable Naming/VariableNames\n  property two_wOrds : String = \"two words\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\n@[ASRA::Name(deserialization_strategy: :underscore)]\nclass DeserializedNameUnderscoreDeserializationStrategy\n  include ASR::Serializable\n\n  def initialize; end\n\n  # Is overridable\n  @[ASRA::Name(deserialize: \"myAdd_ress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  # ameba:disable Naming/VariableNames\n  property two_wOrds : String = \"two words\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\n@[ASRA::Name(deserialization_strategy: :identical)]\nclass DeserializedNameIdenticalDeserializationStrategy\n  include ASR::Serializable\n\n  def initialize; end\n\n  # Is overridable\n  @[ASRA::Name(deserialize: \"myAdd_ress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  # ameba:disable Naming/VariableNames\n  property two_wOrds : String = \"two words\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\n@[ASRA::Name(strategy: :camelcase)]\nclass SerializedNameCamelcaseStrategy\n  include ASR::Serializable\n\n  def initialize; end\n\n  # Is overridable\n  @[ASRA::Name(key: \"myAdd_ress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  # ameba:disable Naming/VariableNames\n  property two_wOrds : String = \"two words\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\n@[ASRA::Name(strategy: :underscore)]\nclass SerializedNameUnderscoreStrategy\n  include ASR::Serializable\n\n  def initialize; end\n\n  # Is overridable\n  @[ASRA::Name(key: \"myAdd_ress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  # ameba:disable Naming/VariableNames\n  property two_wOrds : String = \"two words\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\n@[ASRA::Name(strategy: :identical)]\nclass SerializedNameIdenticalStrategy\n  include ASR::Serializable\n\n  def initialize; end\n\n  # Is overridable\n  @[ASRA::Name(key: \"myAdd_ress\")]\n  property my_home_address : String = \"123 Fake Street\"\n\n  # ameba:disable Naming/VariableNames\n  property two_wOrds : String = \"two words\"\n\n  # ameba:disable Naming/VariableNames\n  property myZipCode : Int32 = 90210\nend\n\nclass DeserializedName\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::Name(deserialize: \"des\")]\n  property custom_name : Int32?\n\n  property default_name : Bool?\nend\n\nclass AliasName\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::Name(aliases: [\"val\", \"value\", \"some_value\"])]\n  property some_value : String?\nend\n\nclass KeyName\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::Name(key: \"firstName\")]\n  property first_name : String?\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/nested.cr",
    "content": "require \"../spec_helper\"\n\nclass NestedType\n  include ASR::Serializable\n\n  def initialize; end\n\n  getter? active : Bool = true\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/post_deserialize.cr",
    "content": "@[ASRA::ExclusionPolicy(:all)]\nclass PostDeserialize\n  include ASR::Serializable\n\n  def initialize; end\n\n  getter first_name : String?\n  getter last_name : String?\n\n  @[ASRA::Expose]\n  getter name : String = \"First Last\"\n\n  @[ASRA::PostDeserialize]\n  def split_name : Nil\n    @first_name, @last_name = @name.split(' ')\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/post_serialize.cr",
    "content": "class PostSerialize\n  include ASR::Serializable\n\n  def initialize; end\n\n  getter name : String?\n  getter age : Int32?\n\n  @[ASRA::PreSerialize]\n  def set_name : Nil\n    @name = \"NAME\"\n  end\n\n  @[ASRA::PreSerialize]\n  def set_age : Nil\n    @age = 123\n  end\n\n  @[ASRA::PostSerialize]\n  def reset : Nil\n    @age = nil\n    @name = nil\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/pre_serialize.cr",
    "content": "class PreSerialize\n  include ASR::Serializable\n\n  def initialize; end\n\n  getter name : String?\n  getter age : Int32?\n\n  @[ASRA::PreSerialize]\n  def set_name : Nil\n    @name = \"NAME\"\n  end\n\n  @[ASRA::PreSerialize]\n  def set_age : Nil\n    @age = 123\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/read_only.cr",
    "content": "# class ReadOnly\n#   include ASR::Serializable\n\n#   property name : String\n\n#   @[ASRA::ReadOnly]\n#   property password : String?\n# end\n"
  },
  {
    "path": "src/components/serializer/spec/models/skip.cr",
    "content": "class Skip\n  include ASR::Serializable\n\n  def initialize; end\n\n  property one : String = \"one\"\n\n  @[ASRA::Skip]\n  property two : String = \"two\"\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/skip_when_empty.cr",
    "content": "class SkipWhenEmpty\n  include ASR::Serializable\n\n  def initialize; end\n\n  @[ASRA::SkipWhenEmpty]\n  property value : String = \"value\"\nend\n"
  },
  {
    "path": "src/components/serializer/spec/models/virtual_property.cr",
    "content": "class VirtualProperty\n  include ASR::Serializable\n\n  def initialize; end\n\n  property foo : String = \"foo\"\n\n  @[ASRA::VirtualProperty]\n  def get_val : String\n    \"VAL\"\n  end\n\n  @[ASRA::VirtualProperty]\n  @[ASRA::Groups(\"group1\")]\n  @[ASRA::Since(\"1.3.2\")]\n  @[ASRA::Until(\"1.2.3\")]\n  def group_version : String\n    \"group_version\"\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/navigators/deserialization_navigator_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ASR::Navigators::DeserializationNavigator do\n  describe \"#accept\" do\n    describe ASRA::PostDeserialize do\n      it \"should run post deserialize methods\" do\n        data = JSON.parse %({\"name\": \"First Last\"})\n\n        visitor = create_deserialization_visitor do |properties|\n          properties.size.should eq 1\n          p = properties[0]\n\n          p.name.should eq \"name\"\n          p.external_name.should eq \"name\"\n          p.skip_when_empty?.should be_false\n          p.groups.should eq [\"default\"] of String\n          p.type.should eq String?\n          p.class.should eq PostDeserialize\n\n          obj = PostDeserialize.new\n\n          obj.first_name.should be_nil\n          obj.last_name.should be_nil\n\n          obj\n        end\n\n        obj = ASR::Navigators::DeserializationNavigator.new(visitor, ASR::DeserializationContext.new, ASR::InstantiateObjectConstructor.new).accept(PostDeserialize, data).as(PostDeserialize)\n\n        obj.first_name.should eq \"First\"\n        obj.last_name.should eq \"Last\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/navigators/serialization_navigator_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ASR::Navigators::SerializationNavigator do\n  describe \"#accept\" do\n    describe ASRA::PreSerialize do\n      it \"should run pre serialize methods\" do\n        obj = PreSerialize.new\n        obj.name.should be_nil\n        obj.age.should be_nil\n\n        visitor = create_serialization_visitor do |properties|\n          properties.size.should eq 2\n          p = properties[0]\n\n          p.name.should eq \"name\"\n          p.external_name.should eq \"name\"\n          p.value.should eq \"NAME\"\n          p.skip_when_empty?.should be_false\n          p.groups.should eq Set{\"default\"}\n          p.type.should eq String?\n\n          p = properties[1]\n\n          p.name.should eq \"age\"\n          p.external_name.should eq \"age\"\n          p.value.should eq 123\n          p.skip_when_empty?.should be_false\n          p.groups.should eq Set{\"default\"}\n          p.type.should eq Int32?\n        end\n\n        ASR::Navigators::SerializationNavigator.new(visitor, ASR::SerializationContext.new).accept obj\n\n        obj.name.should eq \"NAME\"\n        obj.age.should eq 123\n      end\n    end\n\n    describe ASRA::PostSerialize do\n      it \"should run pre serialize methods\" do\n        obj = PostSerialize.new\n        obj.name.should be_nil\n        obj.age.should be_nil\n\n        visitor = create_serialization_visitor do |properties|\n          properties.size.should eq 2\n          p = properties[0]\n\n          p.name.should eq \"name\"\n          p.external_name.should eq \"name\"\n          p.value.should eq \"NAME\"\n          p.skip_when_empty?.should be_false\n          p.groups.should eq Set{\"default\"}\n          p.type.should eq String?\n\n          p = properties[1]\n\n          p.name.should eq \"age\"\n          p.external_name.should eq \"age\"\n          p.value.should eq 123\n          p.skip_when_empty?.should be_false\n          p.groups.should eq Set{\"default\"}\n          p.type.should eq Int32?\n        end\n\n        ASR::Navigators::SerializationNavigator.new(visitor, ASR::SerializationContext.new).accept obj\n\n        obj.name.should be_nil\n        obj.age.should be_nil\n      end\n    end\n\n    describe ASRA::SkipWhenEmpty do\n      it \"should not serialize empty properties\" do\n        obj = SkipWhenEmpty.new\n        obj.value = \"\"\n\n        visitor = create_serialization_visitor do |properties|\n          properties.should be_empty\n        end\n\n        ASR::Navigators::SerializationNavigator.new(visitor, ASR::SerializationContext.new).accept obj\n      end\n\n      it \"should serialize non-empty properties\" do\n        obj = SkipWhenEmpty.new\n\n        visitor = create_serialization_visitor do |properties|\n          properties.size.should eq 1\n          p = properties[0]\n\n          p.name.should eq \"value\"\n          p.external_name.should eq \"value\"\n          p.value.should eq \"value\"\n          p.skip_when_empty?.should be_true\n          p.groups.should eq Set{\"default\"}\n          p.type.should eq String\n        end\n\n        ASR::Navigators::SerializationNavigator.new(visitor, ASR::SerializationContext.new).accept obj\n      end\n    end\n\n    describe :emit_nil do\n      describe \"with the default value\" do\n        it \"should not include nil values\" do\n          obj = EmitNil.new\n\n          visitor = create_serialization_visitor do |properties|\n            properties.size.should eq 1\n            p = properties[0]\n\n            p.name.should eq \"age\"\n            p.external_name.should eq \"age\"\n            p.value.should eq 1\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"default\"}\n            p.type.should eq Int32\n          end\n\n          ASR::Navigators::SerializationNavigator.new(visitor, ASR::SerializationContext.new).accept obj\n        end\n      end\n\n      describe \"when enabled\" do\n        it \"should include nil values\" do\n          obj = EmitNil.new\n          ctx = ASR::SerializationContext.new\n          ctx.emit_nil = true\n\n          visitor = create_serialization_visitor do |properties|\n            properties.size.should eq 2\n            p = properties[0]\n\n            p.name.should eq \"name\"\n            p.external_name.should eq \"name\"\n            p.value.should be_nil\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"default\"}\n            p.type.should eq String?\n\n            p = properties[1]\n\n            p.name.should eq \"age\"\n            p.external_name.should eq \"age\"\n            p.value.should eq 1\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"default\"}\n            p.type.should eq Int32\n          end\n\n          ASR::Navigators::SerializationNavigator.new(visitor, ctx).accept obj\n        end\n      end\n    end\n\n    describe ASRA::Groups do\n      describe \"without any groups in the context\" do\n        it \"should include all properties\" do\n          obj = Group.new\n\n          visitor = create_serialization_visitor do |properties|\n            properties.size.should eq 4\n\n            p = properties[0]\n\n            p.name.should eq \"id\"\n            p.external_name.should eq \"id\"\n            p.value.should eq 1\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"list\", \"details\"}\n            p.type.should eq Int64\n\n            p = properties[1]\n\n            p.name.should eq \"comment_summaries\"\n            p.external_name.should eq \"comment_summaries\"\n            p.value.should eq [\"Sentence 1.\", \"Sentence 2.\"]\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"list\"}\n            p.type.should eq Array(String)\n\n            p = properties[2]\n\n            p.name.should eq \"comments\"\n            p.external_name.should eq \"comments\"\n            p.value.should eq [\"Sentence 1.  Another sentence.\", \"Sentence 2.  Some other stuff.\"]\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"details\"}\n            p.type.should eq Array(String)\n\n            p = properties[3]\n\n            p.name.should eq \"created_at\"\n            p.external_name.should eq \"created_at\"\n            p.value.should eq Time.utc(2019, 1, 1)\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"default\"}\n            p.type.should eq Time\n          end\n\n          ASR::Navigators::SerializationNavigator.new(visitor, ASR::SerializationContext.new).accept obj\n        end\n      end\n\n      describe \"with a group specified\" do\n        it \"should exclude properties not in the given groups\" do\n          obj = Group.new\n          ctx = ASR::SerializationContext.new.groups = [\"list\"]\n\n          # Manually call init here to set the exclusion strategies,\n          # normally this gets handled in the serializer instance\n          ctx.init\n\n          visitor = create_serialization_visitor do |properties|\n            properties.size.should eq 2\n\n            p = properties[0]\n\n            p.name.should eq \"id\"\n            p.external_name.should eq \"id\"\n            p.value.should eq 1\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"list\", \"details\"}\n            p.type.should eq Int64\n\n            p = properties[1]\n\n            p.name.should eq \"comment_summaries\"\n            p.external_name.should eq \"comment_summaries\"\n            p.value.should eq [\"Sentence 1.\", \"Sentence 2.\"]\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"list\"}\n            p.type.should eq Array(String)\n          end\n\n          ASR::Navigators::SerializationNavigator.new(visitor, ctx).accept obj\n        end\n      end\n\n      describe \"that is in the default group\" do\n        it \"should include properties without groups explicitly defined\" do\n          obj = Group.new\n          ctx = ASR::SerializationContext.new.groups = [\"list\", \"default\"]\n\n          # Manually call init here to set the exclusion strategies,\n          # normally this gets handled in the serializer instance\n          ctx.init\n\n          visitor = create_serialization_visitor do |properties|\n            properties.size.should eq 3\n\n            p = properties[0]\n\n            p.name.should eq \"id\"\n            p.external_name.should eq \"id\"\n            p.value.should eq 1\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"list\", \"details\"}\n            p.type.should eq Int64\n\n            p = properties[1]\n\n            p.name.should eq \"comment_summaries\"\n            p.external_name.should eq \"comment_summaries\"\n            p.value.should eq [\"Sentence 1.\", \"Sentence 2.\"]\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"list\"}\n            p.type.should eq Array(String)\n\n            p = properties[2]\n\n            p.name.should eq \"created_at\"\n            p.external_name.should eq \"created_at\"\n            p.value.should eq Time.utc(2019, 1, 1)\n            p.skip_when_empty?.should be_false\n            p.groups.should eq Set{\"default\"}\n            p.type.should eq Time\n          end\n\n          ASR::Navigators::SerializationNavigator.new(visitor, ctx).accept obj\n        end\n      end\n    end\n\n    describe \"primitive type\" do\n      it \"should write the value\" do\n        io = IO::Memory.new\n        ASR::Navigators::SerializationNavigator.new(TestSerializationVisitor.new(io, NamedTuple.new), ASR::SerializationContext.new).accept \"FOO\"\n        io.rewind.gets_to_end.should eq \"FOO\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/serialization_context_spec.cr",
    "content": "require \"./spec_helper\"\n\nstruct False\n  include ASR::ExclusionStrategies::ExclusionStrategyInterface\n\n  # :inherit:\n  def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool\n    false\n  end\nend\n\ndescribe ASR::SerializationContext do\n  describe \"#init\" do\n    it \"that wasn't already inited\" do\n      context = ASR::SerializationContext.new\n      context.groups = {\"group1\"}\n      context.version = \"1.0.0\"\n\n      context.exclusion_strategy.should be_nil\n\n      context.init\n\n      context.exclusion_strategy.should be_a ASR::ExclusionStrategies::Disjunct\n      context.exclusion_strategy.try &.as(ASR::ExclusionStrategies::Disjunct).members.size.should eq 2\n    end\n\n    it \"that was already inited\" do\n      context = ASR::SerializationContext.new\n\n      context.init\n\n      expect_raises ASR::Exception::Logic, \"This context was already initialized, and cannot be re-used.\" do\n        context.init\n      end\n    end\n  end\n\n  describe \"#add_exclusion_strategy\" do\n    describe \"with no previous strategy\" do\n      it \"should set it directly\" do\n        context = ASR::SerializationContext.new\n        context.exclusion_strategy.should be_nil\n\n        context.add_exclusion_strategy False.new\n\n        context.exclusion_strategy.should be_a False\n      end\n    end\n\n    describe \"with a strategy already set\" do\n      it \"should use a Disjunct strategy\" do\n        context = ASR::SerializationContext.new\n        context.exclusion_strategy.should be_nil\n\n        context.add_exclusion_strategy False.new\n        context.add_exclusion_strategy False.new\n\n        context.exclusion_strategy.should be_a ASR::ExclusionStrategies::Disjunct\n        context.exclusion_strategy.try &.as(ASR::ExclusionStrategies::Disjunct).members.size.should eq 2\n      end\n    end\n\n    describe \"with a multiple strategies already set\" do\n      it \"should push the member to the Disjunct strategy\" do\n        context = ASR::SerializationContext.new\n        context.exclusion_strategy.should be_nil\n\n        context.add_exclusion_strategy False.new\n        context.add_exclusion_strategy False.new\n        context.add_exclusion_strategy False.new\n\n        context.exclusion_strategy.should be_a ASR::ExclusionStrategies::Disjunct\n        context.exclusion_strategy.try &.as(ASR::ExclusionStrategies::Disjunct).members.size.should eq 3\n      end\n    end\n  end\n\n  describe \"#groups=\" do\n    it \"sets the groups\" do\n      context = ASR::SerializationContext.new.groups = [\"one\", \"two\"]\n      context.groups.should eq Set{\"one\", \"two\"}\n    end\n\n    it \"raises if the groups are empty\" do\n      expect_raises ArgumentError, \"Groups cannot be empty\" do\n        ASR::SerializationContext.new.groups = [] of String\n      end\n    end\n  end\n\n  describe \"#version=\" do\n    it \"sets the version as a `SemanticVersion`\" do\n      context = ASR::SerializationContext.new.version = \"1.1.1\"\n      context.version.should eq SemanticVersion.new 1, 1, 1\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/serializer_spec.cr",
    "content": "require \"./spec_helper\"\n\nclass Unserializable\n  getter id : Int64?\nend\n\nclass IsSerializable\n  include ASR::Serializable\n\n  getter id : Int64\nend\n\nclass NotNilableModel\n  include ASR::Serializable\n\n  getter not_nilable : String\n  getter not_nilable_not_serializable : Unserializable\nend\n\nclass NilableModel\n  include ASR::Serializable\n\n  getter nilable : String?\n  getter nilable_not_serializable : Unserializable?\nend\n\nclass NilableArrayModel\n  include ASR::Serializable\n\n  getter nilable_array : Array(Unserializable)?\n  getter default_array : Array(Unserializable)? = [] of Unserializable\n  getter nilable_nilable_array : Array(Unserializable?)?\nend\n\nclass TestingModel\n  include ASR::Serializable\n\n  getter id : Int64\n  @array : Array(IsSerializable)\n  property obj : IsSerializable\n\n  def get_array\n    @array\n  end\nend\n\nmodule ReverseConverter\n  def self.deserialize(navigator : ASR::Navigators::DeserializationNavigatorInterface, metadata : ASR::PropertyMetadataBase, data : ASR::Any) : String\n    data.as_s.reverse\n  end\nend\n\nclass ReverseConverterModel\n  include ASR::Serializable\n\n  @[ASRA::Accessor(converter: ReverseConverter)]\n  getter str : String\nend\n\nclass SingleNilablePropertyModel\n  include ASR::Serializable\n\n  property my_prop : String?\nend\n\nabstract struct BaseModel\n  include ASR::Model\nend\n\nrecord ModelOne < BaseModel, id : Int32, name : String do\n  include ASR::Serializable\nend\n\nrecord ModelTwo < BaseModel, id : Int32, name : String do\n  include ASR::Serializable\nend\n\nrecord Unionable, type : BaseModel.class\n\nstruct JSONAnyThing\n  include ASR::Serializable\n\n  getter json : Hash(String, JSON::Any)\nend\n\nstruct YAMLAnyThing\n  include ASR::Serializable\n\n  getter yaml : Hash(String, YAML::Any)\nend\n\ndescribe ASR::Serializer do\n  describe \"#deserialize\" do\n    describe ASR::Serializable do\n      describe NotNilableModel do\n        it \"missing\" do\n          ex = expect_raises ASR::Exception::MissingRequiredProperty, \"Missing required property: 'not_nilable'.\" do\n            ASR.serializer.deserialize NotNilableModel, %({}), :json\n          end\n\n          ex.property_name.should eq \"not_nilable\"\n          ex.property_type.should eq \"String\"\n        end\n\n        it nil do\n          ex = expect_raises ASR::Exception::NilRequiredProperty, \"Required property 'not_nilable_not_serializable' cannot be nil.\" do\n            ASR.serializer.deserialize NotNilableModel, %({\"not_nilable\":\"FOO\",\"not_nilable_not_serializable\":null}), :json\n          end\n\n          ex.property_name.should eq \"not_nilable_not_serializable\"\n          ex.property_type.should eq \"Unserializable\"\n        end\n      end\n\n      describe ASRA::Accessor do\n        it :setter do\n          ASR.serializer.deserialize(SetterAccessor, %({\"foo\":\"foo\"}), :json).foo.should eq \"FOO\"\n        end\n      end\n\n      describe ASRA::Discriminator do\n        it \"happy path\" do\n          ASR.serializer.deserialize(Shape, %({\"x\":1,\"y\":2,\"type\":\"point\"}), :json).should be_a Point\n        end\n\n        it \"missing discriminator\" do\n          ex = expect_raises ASR::Exception::PropertyException, \"Missing discriminator field 'type'.\" do\n            ASR.serializer.deserialize Shape, %({\"x\":1,\"y\":2}), :json\n          end\n\n          ex.property_name.should eq \"type\"\n        end\n\n        it \"unknown discriminator value\" do\n          ex = expect_raises(ASR::Exception::PropertyException, \"Unknown 'type' discriminator value: 'triangle'.\") do\n            ASR.serializer.deserialize Shape, %({\"x\":1,\"y\":2,\"type\":\"triangle\"}), :json\n          end\n\n          ex.property_name.should eq \"type\"\n        end\n      end\n\n      describe NilableModel do\n        it \"should be set to `nil`\" do\n          obj = ASR.serializer.deserialize NilableModel, %({\"nilable\":\"FOO\",\"nilable_not_serializable\":{\"id\":10}}), :json\n          obj.nilable.should eq \"FOO\"\n          obj.nilable_not_serializable.should be_nil\n        end\n\n        it \"should still return an instance if the input is empty\" do\n          ASR.serializer.deserialize(SingleNilablePropertyModel, \"{}\", :json).my_prop.should be_nil\n          ASR.serializer.deserialize(SingleNilablePropertyModel, \"\", :yaml).my_prop.should be_nil\n        end\n      end\n\n      describe NilableArrayModel do\n        it \"should be set to `nil` or default if not provided\" do\n          obj = ASR.serializer.deserialize NilableArrayModel, %({}), :json\n          obj.nilable_array.should be_nil\n          obj.default_array.should eq [] of Unserializable\n          obj.nilable_nilable_array.should be_nil\n        end\n\n        it \"should default to an empty array if provided or `nil` if possible\" do\n          obj = ASR.serializer.deserialize NilableArrayModel, %({\"nilable_array\":[{\"id\":1}],\"default_array\":[{\"id\":1}],\"nilable_nilable_array\":[{\"id\":1}]}), :json\n          obj.nilable_array.should eq [] of Unserializable\n          obj.default_array.should eq [] of Unserializable\n          obj.nilable_nilable_array.should eq [nil]\n        end\n      end\n\n      describe TestingModel do\n        it \"should deserialize correctly\" do\n          obj = ASR.serializer.deserialize TestingModel, %({\"id\":1,\"array\":[{\"id\":2},{\"id\":3}],\"obj\":{\"id\":4}}), :json\n          obj.id.should eq 1\n\n          array = obj.get_array\n          array.size.should eq 2\n          array[0].id.should eq 2\n          array[1].id.should eq 3\n\n          obj.obj.id.should eq 4\n        end\n      end\n\n      describe ReverseConverterModel do\n        it \"should use the converter when deserializing\" do\n          ASR.serializer.deserialize(ReverseConverterModel, %({\"str\":\"jim\"}), :json).str.should eq \"mij\"\n        end\n      end\n    end\n\n    describe \"primitive\" do\n      it nil do\n        expect_raises ASR::Exception::DeserializationException, \"Could not parse String from ''.\" do\n          ASR.serializer.deserialize String, \"null\", :json\n        end\n      end\n\n      it Int32 do\n        value = ASR.serializer.deserialize Int32, \"17\", :json\n        value.should eq 17\n        value.should be_a Int32\n      end\n    end\n\n    describe Unionable do\n      it \"it works with a class union\" do\n        model = ASR.serializer.deserialize Unionable.new(ModelOne).type, %({\"id\":1,\"name\":\"Fred\"}), :json\n        model.should be_a ModelOne\n        model.id.should eq 1\n        model.name.should eq \"Fred\"\n      end\n    end\n\n    describe ASR::Any do\n      it \"works with base JSON type\" do\n        model = ASR.serializer.deserialize JSONAnyThing, %({\"json\":{\"foo\":\"bar\"}}), :json\n        model.json.should be_a Hash(String, JSON::Any)\n\n        model.json[\"foo\"].as_s.should eq \"bar\"\n      end\n\n      it \"works with base YAML type\" do\n        model = ASR.serializer.deserialize YAMLAnyThing, %({\"yaml\":{\"biz\":\"baz\"}}), :yaml\n        model.yaml.should be_a Hash(String, YAML::Any)\n\n        model.yaml[\"biz\"].as_s.should eq \"baz\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"../src/athena-serializer\"\nrequire \"athena-spec\"\n\nrequire \"./models/*\"\n\nenum TestEnum\n  Zero\n  One\n  Two\n  Three\nend\n\ndef get_test_property_metadata : Array(ASR::PropertyMetadataBase)\n  [ASR::PropertyMetadata(String, String).new(\n    name: \"name\",\n    external_name: \"external_name\",\n    annotation_configurations: ADI::AnnotationConfigurations.new,\n    value: \"YES\",\n    skip_when_empty: false,\n    groups: [\"default\"],\n    since_version: nil,\n    until_version: nil,\n  )] of ASR::PropertyMetadataBase\nend\n\nprivate struct TestSerializationNavigator\n  include Athena::Serializer::Navigators::SerializationNavigatorInterface\n\n  def initialize(@visitor : ASR::Visitors::SerializationVisitorInterface, @context : ASR::SerializationContext); end\n\n  def accept(data : ASR::Serializable) : Nil\n    @visitor.visit data.serialization_properties\n  end\n\n  def accept(data : _) : Nil\n    @visitor.visit data\n  end\nend\n\n# Asserts the output of the given *visitor_type*.\ndef assert_serialized_output(visitor_type : ASR::Visitors::SerializationVisitorInterface.class, expected : String, **named_args, & : ASR::Visitors::SerializationVisitorInterface -> Nil)\n  io = IO::Memory.new\n\n  visitor = visitor_type.new io, named_args\n  navigator = TestSerializationNavigator.new(visitor, ASR::SerializationContext.new)\n  visitor.navigator = navigator\n\n  visitor.prepare\n\n  yield visitor\n\n  visitor.finish\n\n  io.rewind.gets_to_end.should eq expected\nend\n\n# Test implementation of `ASR::Visitors::SerializationVisitorInterface` that writes the data to the `io`.\nclass TestSerializationVisitor\n  include Athena::Serializer::Visitors::SerializationVisitorInterface\n\n  def initialize(@io : IO, named_args : NamedTuple); end\n\n  def assert_properties(handler : Proc(Array(ASR::PropertyMetadataBase), Nil)) : Nil\n    @assert_properties = handler\n  end\n\n  def prepare : Nil\n  end\n\n  def finish : Nil\n  end\n\n  def visit(data : Array(ASR::PropertyMetadataBase)) : Nil\n    @assert_properties.try &.call data\n  end\n\n  def visit(data : _) : Nil\n    @io << data\n  end\nend\n\ndef create_serialization_visitor(&block : Array(ASR::PropertyMetadataBase) -> Nil)\n  visitor = TestSerializationVisitor.new IO::Memory.new, NamedTuple.new\n  visitor.assert_properties block\n  visitor\nend\n\n# Test implementation of `ASR::Visitors::DeserializationVisitorInterface` that writes the data to the `io`.\nclass TestDeserializationVisitor\n  include Athena::Serializer::Visitors::DeserializationVisitorInterface\n\n  def initialize(@io : IO); end\n\n  def assert_properties(handler : Proc(Array(ASR::PropertyMetadataBase), ASR::Serializable)) : Nil\n    @assert_properties = handler\n  end\n\n  def prepare(data : IO | String) : ASR::Any\n    ASR::Any.new \"\"\n  end\n\n  def finish : Nil\n  end\n\n  def visit(type : _, properties : Array(ASR::PropertyMetadataBase), data : _)\n    @assert_properties.not_nil!.call properties\n  end\n\n  def visit(type : _, data : _) : Nil\n    @io << data\n  end\nend\n\ndef create_deserialization_visitor(&block : Array(ASR::PropertyMetadataBase) -> ASR::Serializable)\n  visitor = TestDeserializationVisitor.new IO::Memory.new\n  visitor.assert_properties block\n  visitor\nend\n\ndef assert_deserialized_output(visitor_type : ASR::Visitors::DeserializationVisitorInterface.class, type : _, data : _, expected : _)\n  context = ASR::DeserializationContext.new\n  context.init\n\n  visitor = visitor_type.new\n  navigator = ASR::Navigators::DeserializationNavigator.new visitor, context, ASR::InstantiateObjectConstructor.new\n\n  visitor.navigator = navigator\n\n  result = navigator.accept type, visitor.prepare data\n  result.should eq expected\nend\n\ndef create_metadata(\n  *,\n  name : String = \"name\",\n  external_name : String = \"external_name\",\n  value : I = \"value\", skip_when_empty : Bool = false,\n  groups : Array(String) = [\"default\"],\n  since_version : String? = nil,\n  until_version : String? = nil,\n  annotation_configurations : ADI::AnnotationConfigurations = ADI::AnnotationConfigurations.new,\n) : ASR::PropertyMetadata forall I\n  context = ASR::PropertyMetadata(I, I).new name, external_name, annotation_configurations, value, skip_when_empty, groups\n\n  context.since_version = SemanticVersion.parse since_version if since_version\n  context.until_version = SemanticVersion.parse until_version if until_version\n\n  context\nend\n\ndef assert_version(*, since_version : String? = nil, until_version : String? = nil) : Bool\n  ASR::ExclusionStrategies::Version.new(SemanticVersion.parse \"1.0.0\").skip_property?(create_metadata(since_version: since_version, until_version: until_version), ASR::SerializationContext.new)\nend\n\ndef assert_groups(*, groups : Array(String), metadata_groups : Array(String) = [\"default\"]) : Bool\n  ASR::ExclusionStrategies::Groups.new(groups).skip_property?(create_metadata(groups: metadata_groups), ASR::SerializationContext.new)\nend\n"
  },
  {
    "path": "src/components/serializer/spec/visitors/json_deserialization_visitor_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ASR::Visitors::JSONDeserializationVisitor do\n  describe \"#visit\" do\n    describe \"primitive types\" do\n      it String do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, String, %(\"Foo\"), \"Foo\"\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, String?, %(\"Foo\"), \"Foo\"\n      end\n\n      it Int32 do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Int32, \"17\", 17\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Int32?, \"17\", 17\n      end\n\n      it Int64 do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Int64, \"17\", 17_i64\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Int64?, \"17\", 17_i64\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Int64?, \"1033488268764\", 1_033_488_268_764\n      end\n\n      it Float32 do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Float32, \"17.145\", 17.145_f32\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Float32?, \"17.145\", 17.145_f32\n      end\n\n      it Float64 do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Float64, \"17.145\", 17.145\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Float64?, \"17.145\", 17.145\n      end\n\n      it String | Int32 do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, String | Int32, \"100000\", 100_000\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, String | Int32, %(\"Bar\"), \"Bar\"\n\n        expect_raises(ASR::Exception::DeserializationException, \"Couldn't parse (Int32 | String) from 'false'\") do\n          assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, String | Int32, \"false\", false\n        end\n      end\n\n      it Array do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Array(Int32), \"[1,2,3]\", [1, 2, 3]\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Array(Int32?), \"[1,2,null]\", [1, 2, nil]\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Array(Int32)?, \"[1,2,3]\", [1, 2, 3]\n      end\n\n      it Set do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Set(Int32), \"[1,2,3]\", Set{1, 2, 3}\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Set(Int32?), \"[1,2,null]\", Set{1, 2, nil}\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Set(Int32)?, \"[1,2,3]\", Set{1, 2, 3}\n      end\n\n      it Tuple do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Tuple(Int32, Int32, Int32), \"[1,2,3]\", {1, 2, 3}\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Tuple(Int32, Int32, Int32)?, \"[1,2,3]\", {1, 2, 3}\n      end\n\n      it NamedTuple do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, NamedTuple(numbers: Array(Int32), data: Hash(String, String | Int32)), %({\"numbers\":[1,2,3],\"data\":{\"name\":\"Jim\",\"age\":19}}), {numbers: [1, 2, 3], data: {\"name\" => \"Jim\", \"age\" => 19}}\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, NamedTuple(numbers: Array(Int32), data: Hash(String, String | Int32))?, %({\"numbers\":[1,2,3],\"data\":{\"name\":\"Jim\",\"age\":19}}), {numbers: [1, 2, 3], data: {\"name\" => \"Jim\", \"age\" => 19}}\n      end\n\n      it TestEnum do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, TestEnum, \"0\", TestEnum::Zero\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, TestEnum, %(\"Three\"), TestEnum::Three\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, TestEnum?, \"1\", TestEnum::One\n\n        expect_raises(ASR::Exception::DeserializationException, \"Couldn't parse (TestEnum | Nil) from 'asdf'\") do\n          assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, TestEnum?, %(\"asdf\"), nil\n        end\n      end\n\n      it Time do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Time, %(\"2020-04-07T12:34:56Z\"), Time.utc 2020, 4, 7, 12, 34, 56\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Time?, %(\"2020-04-07T12:34:56Z\"), Time.utc 2020, 4, 7, 12, 34, 56\n      end\n\n      it Hash do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Hash(String, String), %({\"foo\": \"bar\"}), {\"foo\" => \"bar\"}\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Hash(String, String)?, %({\"foo\": \"bar\"}), {\"foo\" => \"bar\"}\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Hash(String, String?)?, %({\"foo\": \"bar\"}), {\"foo\" => \"bar\"}\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Hash(String, String?), %({\"foo\": \"bar\"}), {\"foo\" => \"bar\"}\n      end\n\n      it JSON::Any do\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Hash(String, JSON::Any), %({\"foo\":\"bar\"}), {\"foo\" => \"bar\"}\n        assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, Hash(String, JSON::Any)?, %({\"foo\":\"bar\"}), {\"foo\" => \"bar\"}\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/visitors/json_serialization_visitor_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ASR::Visitors::JSONSerializationVisitor do\n  describe \"#visit\" do\n    describe \"primitive types\" do\n      it \"with indent\" do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %({\\n    \"key\": \"value\"\\n}), indent: 4) do |visitor|\n          visitor.visit({\"key\" => \"value\"})\n        end\n      end\n\n      it String do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %(\"Foo\")) do |visitor|\n          visitor.visit \"Foo\"\n        end\n      end\n\n      it Symbol do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %(\"Bar\")) do |visitor|\n          visitor.visit :Bar\n        end\n      end\n\n      it Int do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"14\") do |visitor|\n          visitor.visit 14\n        end\n      end\n\n      it Float do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"15.5\") do |visitor|\n          visitor.visit 15.5\n        end\n      end\n\n      it Bool do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"false\") do |visitor|\n          visitor.visit false\n        end\n      end\n\n      it Nil do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"null\") do |visitor|\n          visitor.visit nil\n        end\n      end\n\n      it UUID do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %(\"f89dc089-2c6c-411a-af20-ea98f90376ef\")) do |visitor|\n          visitor.visit UUID.new(\"f89dc089-2c6c-411a-af20-ea98f90376ef\")\n        end\n      end\n\n      describe Enumerable do\n        it Array do\n          assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"[1,2,3]\") do |visitor|\n            visitor.visit [1, 2, 3]\n          end\n        end\n\n        it Set do\n          assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"[1,2,3]\") do |visitor|\n            visitor.visit Set{1, 2, 3}\n          end\n        end\n\n        it Deque do\n          assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"[1,2,3]\") do |visitor|\n            visitor.visit Deque{1, 2, 3}\n          end\n        end\n\n        it Tuple do\n          assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"[1,2,3]\") do |visitor|\n            visitor.visit({1, 2, 3})\n          end\n        end\n      end\n\n      it Time do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %(\"2020-01-18T10:20:30Z\")) do |visitor|\n          visitor.visit Time.utc 2020, 1, 18, 10, 20, 30\n        end\n      end\n\n      it Hash do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %({\"key\":\"value\",\"values\":[1,\"foo\",false]})) do |visitor|\n          visitor.visit({\"key\" => \"value\", \"values\" => [1, \"foo\", false]})\n        end\n      end\n\n      it NamedTuple do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %({\"space key\":123.12})) do |visitor|\n          visitor.visit({\"space key\": 123.12})\n        end\n      end\n\n      it Enum do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %(\"two\")) do |visitor|\n          visitor.visit TestEnum::Two\n        end\n      end\n\n      it YAML::Any do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"2020\") do |visitor|\n          visitor.visit YAML.parse(\"2020\")\n        end\n      end\n\n      it JSON::Any do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"2020\") do |visitor|\n          visitor.visit JSON.parse(\"2020\")\n        end\n      end\n    end\n\n    describe ASR::Serializable do\n      it \"empty object\" do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, \"{}\") do |visitor|\n          visitor.visit EmptyObject.new\n        end\n      end\n\n      it \"valid object\" do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %({\"foo\":\"foo\",\"bar\":12.1,\"nest\":{\"active\":true}})) do |visitor|\n          visitor.visit TestObject.new\n        end\n      end\n\n      it Array(ASR::PropertyMetadataBase) do\n        assert_serialized_output(ASR::Visitors::JSONSerializationVisitor, %({\"external_name\":\"YES\"})) do |visitor|\n          visitor.visit get_test_property_metadata\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/visitors/yaml_deserialization_visitor_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe ASR::Visitors::YAMLDeserializationVisitor do\n  describe \"#visit\" do\n    describe \"primitive types\" do\n      it String do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, String, %(\"Foo\"), \"Foo\"\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, String?, %(\"Foo\"), \"Foo\"\n      end\n\n      it Int32 do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Int32, \"17\", 17\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Int32?, \"17\", 17\n      end\n\n      it Int64 do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Int64, \"17\", 17_i64\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Int64?, \"17\", 17_i64\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Int64?, \"1033488268764\", 1_033_488_268_764\n      end\n\n      it Float32 do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Float32, \"17.145\", 17.145_f32\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Float32?, \"17.145\", 17.145_f32\n      end\n\n      it Float64 do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Float64, \"17.145\", 17.145\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Float64?, \"17.145\", 17.145\n      end\n\n      it String | Int32 do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, String | Int32, \"100000\", 100_000\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, String | Int32, %(\"Bar\"), \"Bar\"\n\n        expect_raises(ASR::Exception::DeserializationException, \"Couldn't parse (Int32 | String) from 'false'\") do\n          assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, String | Int32, \"false\", false\n        end\n      end\n\n      it Array do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Array(Int32), \"---\\n- 1\\n- 2\\n- 3\", [1, 2, 3]\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Array(Int32?), \"[1,2,~]\", [1, 2, nil]\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Array(Int32)?, \"[1,2,3]\", [1, 2, 3]\n      end\n\n      it Set do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Set(Int32), \"---\\n- 1\\n- 2\\n- 3\", Set{1, 2, 3}\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Set(Int32?), \"[1,2,null]\", Set{1, 2, nil}\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Set(Int32)?, \"[1,2,3]\", Set{1, 2, 3}\n      end\n\n      it Tuple do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Tuple(Int32, Int32, Int32), \"[1,2,3]\", {1, 2, 3}\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Tuple(Int32, Int32, Int32)?, \"[1,2,3]\", {1, 2, 3}\n      end\n\n      it NamedTuple do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, NamedTuple(numbers: Array(Int32), data: Hash(String, String | Int32)), %(---\\nnumbers:\\n  - 1\\n  - 2\\n  - 3\\ndata:\\n  name: Jim\\n  age: 19), {numbers: [1, 2, 3], data: {\"name\" => \"Jim\", \"age\" => 19}}\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, NamedTuple(numbers: Array(Int32), data: Hash(String, String | Int32))?, %(---\\nnumbers:\\n  - 1\\n  - 2\\n  - 3\\ndata:\\n  name: Jim\\n  age: 19), {numbers: [1, 2, 3], data: {\"name\" => \"Jim\", \"age\" => 19}}\n      end\n\n      it TestEnum do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, TestEnum, \"0\", TestEnum::Zero\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, TestEnum, %(\"Three\"), TestEnum::Three\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, TestEnum?, \"1\", TestEnum::One\n\n        expect_raises(ASR::Exception::DeserializationException, \"Couldn't parse (TestEnum | Nil) from 'asdf'\") do\n          assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, TestEnum?, %(\"asdf\"), nil\n        end\n      end\n\n      it Time do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Time, %(\"2020-04-07T12:34:56Z\"), Time.utc 2020, 4, 7, 12, 34, 56\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Time?, %(\"2020-04-07T12:34:56Z\"), Time.utc 2020, 4, 7, 12, 34, 56\n      end\n\n      it Hash do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Hash(String, String), %(---\\nfoo: bar), {\"foo\" => \"bar\"}\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Hash(String, String)?, %(---\\nfoo: bar), {\"foo\" => \"bar\"}\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Hash(String, String?)?, %(---\\nfoo: bar), {\"foo\" => \"bar\"}\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Hash(String, String?), %(---\\nfoo: bar), {\"foo\" => \"bar\"}\n      end\n\n      it YAML::Any do\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Hash(String, YAML::Any), %(---\\nfoo: bar), {\"foo\" => \"bar\"}\n        assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, Hash(String, YAML::Any)?, %(---\\nfoo: bar), {\"foo\" => \"bar\"}\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/spec/visitors/yaml_serialization_visitor_spec.cr",
    "content": "require \"../spec_helper\"\n\n# Used to conditionally add the document end marker after some scalar strings based on the libyaml version.\nprivate def build_expected_yaml_string(expected : String) : String\n  expected += \"...\\n\" if YAML.libyaml_version < SemanticVersion.new(0, 2, 1)\n  expected\nend\n\ndescribe ASR::Visitors::YAMLSerializationVisitor do\n  describe \"#visit\" do\n    describe \"primitive types\" do\n      it String do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string %(--- Foo\\n)) do |visitor|\n          visitor.visit \"Foo\"\n        end\n      end\n\n      it Symbol do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string %(--- Bar\\n)) do |visitor|\n          visitor.visit :Bar\n        end\n      end\n\n      it Int do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string \"--- 14\\n\") do |visitor|\n          visitor.visit 14\n        end\n      end\n\n      it Float do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string \"--- 15.5\\n\") do |visitor|\n          visitor.visit 15.5\n        end\n      end\n\n      it Bool do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string \"--- false\\n\") do |visitor|\n          visitor.visit false\n        end\n      end\n\n      it Nil do\n        str = \"---\"\n        str += \" \" if YAML.libyaml_version < SemanticVersion.new(0, 2, 5)\n        str += '\\n'\n\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string str) do |visitor|\n          visitor.visit nil\n        end\n      end\n\n      it UUID do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string \"--- f89dc089-2c6c-411a-af20-ea98f90376ef\\n\") do |visitor|\n          visitor.visit UUID.new(\"f89dc089-2c6c-411a-af20-ea98f90376ef\")\n        end\n      end\n\n      describe Enumerable do\n        it Array do\n          assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, \"---\\n- 1\\n- 2\\n- 3\\n\") do |visitor|\n            visitor.visit [1, 2, 3]\n          end\n        end\n\n        it Set do\n          assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, \"---\\n- 1\\n- 2\\n- 3\\n\") do |visitor|\n            visitor.visit Set{1, 2, 3}\n          end\n        end\n\n        it Deque do\n          assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, \"---\\n- 1\\n- 2\\n- 3\\n\") do |visitor|\n            visitor.visit Deque{1, 2, 3}\n          end\n        end\n\n        it Tuple do\n          assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, \"---\\n- 1\\n- 2\\n- 3\\n\") do |visitor|\n            visitor.visit({1, 2, 3})\n          end\n        end\n      end\n\n      it Time do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string %(--- 2020-01-18T10:20:30Z\\n)) do |visitor|\n          visitor.visit Time.utc 2020, 1, 18, 10, 20, 30\n        end\n      end\n\n      it Hash do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, %(---\\nkey: value\\nvalues:\\n- 1\\n- foo\\n- false\\n)) do |visitor|\n          visitor.visit({\"key\" => \"value\", \"values\" => [1, \"foo\", false]})\n        end\n      end\n\n      it NamedTuple do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, %(---\\nspace key: 123.12\\n)) do |visitor|\n          visitor.visit({\"space key\": 123.12})\n        end\n      end\n\n      it Enum do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string \"--- two\\n\") do |visitor|\n          visitor.visit TestEnum::Two\n        end\n      end\n\n      it YAML::Any do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string \"--- 2020\\n\") do |visitor|\n          visitor.visit YAML.parse(\"2020\")\n        end\n      end\n\n      it JSON::Any do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, build_expected_yaml_string \"--- 2020\\n\") do |visitor|\n          visitor.visit JSON.parse(\"2020\")\n        end\n      end\n    end\n\n    describe ASR::Serializable do\n      it \"empty object\" do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, \"--- {}\\n\") do |visitor|\n          visitor.visit EmptyObject.new\n        end\n      end\n\n      it \"valid object\" do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, %(---\\nfoo: foo\\nbar: 12.1\\nnest:\\n  active: true\\n)) do |visitor|\n          visitor.visit TestObject.new\n        end\n      end\n\n      it Array(ASR::PropertyMetadataBase) do\n        assert_serialized_output(ASR::Visitors::YAMLSerializationVisitor, %(---\\nexternal_name: YES\\n)) do |visitor|\n          visitor.visit get_test_property_metadata\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/annotations.cr",
    "content": "# `Athena::Serializer` uses annotations to control how an object gets serialized and deserialized.\n# This module includes all the default serialization and deserialization annotations. The `ASRA` alias can be used as a shorthand when applying the annotations.\nmodule Athena::Serializer::Annotations\n  # Allows using methods/modules to control how a property is retrieved/set.\n  #\n  # ## Fields\n  # * `getter` - A method name whose return value will be used as the serialized value.\n  # * `setter` - A method name that accepts the deserialized value.  Can be used to apply additional logic before setting the properties value.\n  # * `converter` - A module that defines a `.deserialize` method.  Can be used to share common deserialization between types.\n  # * `path : Tuple` - A set of keys used to navigate to a value during deserialization.  The value of the last key will be used as the property's value.\n  #\n  # ## Example\n  #\n  # ### Getter/Setter\n  #\n  # ```\n  # class AccessorExample\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   @[ASRA::Accessor(getter: get_foo, setter: set_foo)]\n  #   property foo : String = \"foo\"\n  #\n  #   private def set_foo(foo : String) : String\n  #     @foo = foo.upcase\n  #   end\n  #\n  #   private def get_foo : String\n  #     @foo.upcase\n  #   end\n  # end\n  #\n  # ASR.serializer.serialize AccessorExample.new, :json                 # => {\"foo\":\"FOO\"}\n  # ASR.serializer.deserialize AccessorExample, %({\"foo\":\"bar\"}), :json # => #<AccessorExample:0x7f5915e25c20 @foo=\"BAR\">\n  # ```\n  #\n  # ### Converter\n  #\n  # ```\n  # module ReverseConverter\n  #   def self.deserialize(navigator : ASR::Navigators::DeserializationNavigatorInterface, metadata : ASR::PropertyMetadataBase, data : ASR::Any) : String\n  #     data.as_s.reverse\n  #   end\n  # end\n  #\n  # class ConverterExample\n  #   include ASR::Serializable\n  #\n  #   @[ASRA::Accessor(converter: ReverseConverter)]\n  #   getter str : String\n  # end\n  #\n  # ASR.serializer.deserialize ConverterExample, %({\"str\":\"jim\"}), :json # => #<ConverterExample:0x7f9745fa6d60 @str=\"mij\">\n  # ```\n  #\n  # ### Path\n  #\n  # ```\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   getter id : Int64\n  #\n  #   @[ASRA::Accessor(path: {\"stats\", \"HP\"})]\n  #   getter hp : Int32\n  #\n  #   @[ASRA::Accessor(path: {\"stats\", \"Attack\"})]\n  #   getter attack : Int32\n  #\n  #   @[ASRA::Accessor(path: {\"downs\", -1, \"last_down\"})]\n  #   getter last_down : Time\n  # end\n  #\n  # DATA = <<-JSON\n  # {\n  #   \"id\": 1,\n  #   \"stats\": {\n  #     \"HP\": 45,\n  #     \"Attack\": 49\n  #   },\n  #   \"downs\": [\n  #     {\n  #       \"id\": 1,\n  #       \"last_down\": \"2020-05-019T05:23:17Z\"\n  #     },\n  #     {\n  #       \"id\": 2,\n  #       \"last_down\": \"2020-04-07T12:34:56Z\"\n  #     }\n  #   ]\n  #\n  # }\n  # JSON\n  #\n  # ASR.serializer.deserialize Example, DATA, :json\n  # # #<Example:0x7f43c4ddf580\n  # #  @attack=49,\n  # #  @hp=45,\n  # #  @id=1,\n  # #  @last_down=2020-04-07 12:34:56.0 UTC>\n  # ```\n  annotation Accessor; end\n\n  # Can be applied to a type to control the order of properties when serialized.  Valid values: `:alphabetical`, and `:custom`.\n  #\n  # By default properties are ordered in the order in which they are defined.\n  #\n  # ## Fields\n  # * `order` - Used to specify the order of the properties when using `:custom` ordering.\n  #\n  # ## Example\n  #\n  # ```\n  # class Default\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   property a : String = \"A\"\n  #   property z : String = \"Z\"\n  #   property two : String = \"two\"\n  #   property one : String = \"one\"\n  #   property a_a : Int32 = 123\n  #\n  #   @[ASRA::VirtualProperty]\n  #   def get_val : String\n  #     \"VAL\"\n  #   end\n  # end\n  #\n  # ASR.serializer.serialize Default.new, :json # => {\"a\":\"A\",\"z\":\"Z\",\"two\":\"two\",\"one\":\"one\",\"a_a\":123,\"get_val\":\"VAL\"}\n  #\n  # @[ASRA::AccessorOrder(:alphabetical)]\n  # class Abc\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   property a : String = \"A\"\n  #   property z : String = \"Z\"\n  #   property two : String = \"two\"\n  #   property one : String = \"one\"\n  #   property a_a : Int32 = 123\n  #\n  #   @[ASRA::VirtualProperty]\n  #   def get_val : String\n  #     \"VAL\"\n  #   end\n  # end\n  #\n  # ASR.serializer.serialize Abc.new, :json # => {\"a\":\"A\",\"a_a\":123,\"get_val\":\"VAL\",\"one\":\"one\",\"two\":\"two\",\"z\":\"Z\"}\n  #\n  # @[ASRA::AccessorOrder(:custom, order: [\"two\", \"z\", \"get_val\", \"a\", \"one\", \"a_a\"])]\n  # class Custom\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   property a : String = \"A\"\n  #   property z : String = \"Z\"\n  #   property two : String = \"two\"\n  #   property one : String = \"one\"\n  #   property a_a : Int32 = 123\n  #\n  #   @[ASRA::VirtualProperty]\n  #   def get_val : String\n  #     \"VAL\"\n  #   end\n  # end\n  #\n  # ASR.serializer.serialize Custom.new, :json # => {\"two\":\"two\",\"z\":\"Z\",\"get_val\":\"VAL\",\"a\":\"A\",\"one\":\"one\",\"a_a\":123}\n  # ```\n  annotation AccessorOrder; end\n\n  # Allows deserializing an object based on the value of a specific field.\n  #\n  # ## Fields\n  # * `key : String` - The field that should be read from the data to determine the correct type.\n  # * `map : Hash | NamedTuple` - Maps the possible `key` values to their corresponding types.\n  #\n  # ## Example\n  #\n  # ```\n  # @[ASRA::Discriminator(key: \"type\", map: {point: Point, circle: Circle})]\n  # abstract class Shape\n  #   include ASR::Serializable\n  #\n  #   property type : String\n  # end\n  #\n  # class Point < Shape\n  #   property x : Int32\n  #   property y : Int32\n  # end\n  #\n  # class Circle < Shape\n  #   property x : Int32\n  #   property y : Int32\n  #   property radius : Int32\n  # end\n  #\n  # ASR.serializer.deserialize Shape, %({\"type\":\"point\",\"x\":10,\"y\":20}), :json              # => #<Point:0x7fbbf7f8bc20 @type=\"point\", @x=10, @y=20>\n  # ASR.serializer.deserialize Shape, %({\"type\":\"circle\",\"x\":30,\"y\":40,\"radius\":12}), :json # => #<Circle:0x7fbbf7f93c60 @radius=12, @type=\"circle\", @x=30, @y=40>\n  # ```\n  annotation Discriminator; end\n\n  # Indicates that a property should not be serialized/deserialized when used with `:none` `ASRA::ExclusionPolicy`.\n  #\n  # Also see, `ASRA::IgnoreOnDeserialize` and `ASRA::IgnoreOnSerialize`.\n  #\n  # ## Example\n  #\n  # ```\n  # @[ASRA::ExclusionPolicy(:none)]\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   property name : String = \"Jim\"\n  #\n  #   @[ASRA::Exclude]\n  #   property password : String = \"monkey\"\n  # end\n  #\n  # ASR.serializer.serialize Example.new, :json                                          # => {\"name\":\"Jim\"}\n  # ASR.serializer.deserialize Example, %({\"name\":\"Jim\",\"password\":\"password1!\"}), :json # => #<Example:0x7f6eec4b6a60 @name=\"Jim\", @password=\"monkey\">\n  # ```\n  #\n  # !!!warning\n  #     On deserialization, the excluded properties must be nilable, or have a default value.\n  annotation Exclude; end\n\n  # Defines the default exclusion policy to use on a class.  Valid values: `:none`, and `:all`.\n  #\n  # Used with `ASRA::Expose` and `ASRA::Exclude`.\n  annotation ExclusionPolicy; end\n\n  # Indicates that a property should be serialized/deserialized when used with `:all` `ASRA::ExclusionPolicy`.\n  #\n  # Also see, `ASRA::IgnoreOnDeserialize` and `ASRA::IgnoreOnSerialize`.\n  #\n  # ## Example\n  #\n  # ```\n  # @[ASRA::ExclusionPolicy(:all)]\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   @[ASRA::Expose]\n  #   property name : String = \"Jim\"\n  #\n  #   property password : String = \"monkey\"\n  # end\n  #\n  # ASR.serializer.serialize Example.new, :json                                          # => {\"name\":\"Jim\"}\n  # ASR.serializer.deserialize Example, %({\"name\":\"Jim\",\"password\":\"password1!\"}), :json # => #<Example:0x7f6eec4b6a60 @name=\"Jim\", @password=\"monkey\">\n  # ```\n  #\n  # !!!warning\n  #     On deserialization, the excluded properties must be nilable, or have a default value.\n  annotation Expose; end\n\n  # Defines the group(s) a property belongs to.  Properties are automatically added to the `default` group\n  # if no groups are explicitly defined.\n  #\n  # See `ASR::ExclusionStrategies::Groups`.\n  annotation Groups; end\n\n  # Indicates that a property should not be set on deserialization, but should be serialized.\n  #\n  # ## Example\n  #\n  # ```\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   property name : String\n  #\n  #   @[ASRA::IgnoreOnDeserialize]\n  #   property password : String?\n  # end\n  #\n  # obj = ASR.serializer.deserialize Example, %({\"name\":\"Jim\",\"password\":\"monkey123\"}), :json\n  #\n  # obj.password # => nil\n  # obj.name     # => Jim\n  #\n  # obj.password = \"foobar\"\n  #\n  # ASR.serializer.serialize obj, :json # => {\"name\":\"Jim\",\"password\":\"foobar\"}\n  # ```\n  annotation IgnoreOnDeserialize; end\n\n  # Indicates that a property should be set on deserialization, but should not be serialized.\n  #\n  # ## Example\n  #\n  # ```\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   property name : String\n  #\n  #   @[ASRA::IgnoreOnSerialize]\n  #   property password : String?\n  # end\n  #\n  # obj = ASR.serializer.deserialize Example, %({\"name\":\"Jim\",\"password\":\"monkey123\"}), :json\n  #\n  # obj.password # => monkey123\n  # obj.name     # => Jim\n  #\n  # obj.password = \"foobar\"\n  #\n  # ASR.serializer.serialize obj, :json # => {\"name\":\"Jim\"}\n  # ```\n  annotation IgnoreOnSerialize; end\n\n  # Defines the `key` to use during (de)serialization.  If not provided, the name of the property is used.\n  # Also allows defining aliases that can be used for that property when deserializing.\n  #\n  # ## Fields\n  #\n  # * `serialize : String` - The key to use for this property during serialization.\n  # * `deserialize : String` - The key to use for this property during deserialization.\n  # * `key` : String - The key to use for this property during (de)serialization.\n  # * `aliases : Array(String)` - A set of keys to use for this property during deserialization; is equivalent to multiple `deserialize` keys.\n  # * `serialization_strategy : Symbol` - Defines the default serialization naming strategy for this type.  Can be overridden using the `serialize` or `key` field.\n  # * `deserialization_strategy : Symbol` - Defines the default deserialization naming strategy for this type.  Can be overridden using the `deserialize` or `key` field.\n  # * `strategy : Symbol` - Defines the default (de)serialization naming strategy for this type.  Can be overridden using the `serialize`, `deserialize` or `key` fields.\n  #\n  # ## Example\n  #\n  # ```\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   @[ASRA::Name(serialize: \"myAddress\")]\n  #   property my_home_address : String = \"123 Fake Street\"\n  #\n  #   @[ASRA::Name(deserialize: \"some_key\", serialize: \"a_value\")]\n  #   property both_names : String = \"str\"\n  #\n  #   @[ASRA::Name(key: \"same\")]\n  #   property same_in_both_directions : String = \"same for both\"\n  #\n  #   @[ASRA::Name(aliases: [\"val\", \"value\", \"some_value\"])]\n  #   property some_value : String = \"some_val\"\n  # end\n  #\n  # ASR.serializer.serialize Example.new, :json # => {\"myAddress\":\"123 Fake Street\",\"a_value\":\"str\",\"same\":\"same for both\",\"some_value\":\"some_val\"}\n  #\n  # obj = ASR.serializer.deserialize Example, %({\"my_home_address\":\"555 Mason Ave\",\"some_key\":\"deserialized from diff key\",\"same\":\"same again\",\"value\":\"some_other_val\"}), :json\n  #\n  # obj.my_home_address         # => \"555 Mason Ave\"\n  # obj.both_names              # => \"deserialized from diff key\"\n  # obj.same_in_both_directions # => \"same again\"\n  # obj.some_value              # => \"some_other_val\"\n  # ```\n  #\n  # ### Naming Strategies\n  #\n  # By default the keys in the serialized data match exactly to the name of the property.\n  # Naming strategies allow changing this behavior for all properties within the type.\n  # The serialized name can still be overridden on a per-property basis via\n  # using the `ASRA::Name` annotation with the `serialize`, `deserialize` or `key` field.\n  # The strategy will be applied on serialization, deserialization or both, depending\n  # on whether `serialization_strategy`, `deserialization_strategy` or `strategy` is used.\n  #\n  # The available naming strategies include:\n  # * `:camelcase`\n  # * `:underscore`\n  # * `:identical`\n  #\n  # ```\n  # @[ASRA::Name(strategy: :camelcase)]\n  # class User\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   property id : Int32 = 1\n  #   property first_name : String = \"Jon\"\n  #   property last_name : String = \"Snow\"\n  # end\n  #\n  # ASR.serializer.serialize User.new, :json # => {\"id\":1,\"firstName\":\"Jon\",\"lastName\":\"Snow\"}\n  # ```\n  annotation Name; end\n\n  # Defines a callback method(s) that are ran directly after the object has been deserialized.\n  #\n  # ## Example\n  #\n  # ```\n  # record Example, name : String, first_name : String?, last_name : String? do\n  #   include ASR::Serializable\n  #\n  #   @[ASRA::PostDeserialize]\n  #   private def split_name : Nil\n  #     @first_name, @last_name = @name.split(' ')\n  #   end\n  # end\n  #\n  # obj = ASR.serializer.deserialize Example, %({\"name\":\"Jon Snow\"}), :json\n  #\n  # obj.name       # => Jon Snow\n  # obj.first_name # => Jon\n  # obj.last_name  # => Snow\n  # ```\n  annotation PostDeserialize; end\n\n  # Defines a callback method that is executed directly after the object has been serialized.\n  #\n  # ## Example\n  #\n  # ```\n  # @[ASRA::ExclusionPolicy(:all)]\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   @[ASRA::Expose]\n  #   @name : String?\n  #\n  #   property first_name : String = \"Jon\"\n  #   property last_name : String = \"Snow\"\n  #\n  #   @[ASRA::PreSerialize]\n  #   private def pre_serialize : Nil\n  #     @name = \"#{first_name} #{last_name}\"\n  #   end\n  #\n  #   @[ASRA::PostSerialize]\n  #   private def post_serialize : Nil\n  #     @name = nil\n  #   end\n  # end\n  #\n  # ASR.serializer.serialize Example.new, :json # => {\"name\":\"Jon Snow\"}\n  # ```\n  annotation PostSerialize; end\n\n  # Defines a callback method that is executed directly before the object has been serialized.\n  #\n  # ## Example\n  #\n  # ```\n  # @[ASRA::ExclusionPolicy(:all)]\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   @[ASRA::Expose]\n  #   @name : String?\n  #\n  #   property first_name : String = \"Jon\"\n  #   property last_name : String = \"Snow\"\n  #\n  #   @[ASRA::PreSerialize]\n  #   private def pre_serialize : Nil\n  #     @name = \"#{first_name} #{last_name}\"\n  #   end\n  #\n  #   @[ASRA::PostSerialize]\n  #   private def post_serialize : Nil\n  #     @name = nil\n  #   end\n  # end\n  #\n  # ASR.serializer.serialize Example.new, :json # => {\"name\":\"Jon Snow\"}\n  # ```\n  annotation PreSerialize; end\n\n  # Indicates that a property is read-only and cannot be set during deserialization.\n  #\n  # ## Example\n  #\n  # ```\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   property name : String\n  #\n  #   @[ASRA::ReadOnly]\n  #   property password : String?\n  # end\n  #\n  # obj = ASR.serializer.deserialize Example, %({\"name\":\"Fred\",\"password\":\"password1\"}), :json\n  #\n  # obj.name     # => \"Fred\"\n  # obj.password # => nil\n  # ```\n  #\n  # !!!warning\n  #     The property must be nilable, or have a default value.\n  annotation ReadOnly; end\n\n  # Represents the first version a property was available.\n  #\n  # See `ASR::ExclusionStrategies::Version`.\n  #\n  # !!!note\n  #     Value must be a `SemanticVersion` version.\n  annotation Since; end\n\n  # Indicates that a property should not be serialized or deserialized.\n  #\n  # ## Example\n  #\n  # ```\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   property name : String = \"Jim\"\n  #\n  #   @[ASRA::Skip]\n  #   property password : String = \"monkey\"\n  # end\n  #\n  # ASR.serializer.deserialize Example, %({\"name\":\"Fred\",\"password\":\"foobar\"}), :json # => #<Example:0x7fe4dc98bce0 @name=\"Fred\", @password=\"monkey\">\n  # ASR.serializer.serialize Example.new, :json                                       # => {\"name\":\"Fred\"}\n  # ```\n  annotation Skip; end\n\n  # Indicates that a property should not be serialized when it is empty.\n  #\n  # ## Example\n  #\n  # ```\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   property id : Int64 = 1\n  #\n  #   @[ASRA::SkipWhenEmpty]\n  #   property value : String = \"value\"\n  #\n  #   @[ASRA::SkipWhenEmpty]\n  #   property values : Array(String) = %w(one two three)\n  # end\n  #\n  # obj = Example.new\n  #\n  # ASR.serializer.serialize obj, :json # => {\"id\":1,\"value\":\"value\",\"values\":[\"one\",\"two\",\"three\"]}\n  #\n  # obj.value = \"\"\n  # obj.values = [] of String\n  #\n  # ASR.serializer.serialize obj, :json # => {\"id\":1}\n  # ```\n  #\n  # !!!tip:\n  #     Can be used on any type that defines an `#empty?` method.\n  annotation SkipWhenEmpty; end\n\n  # Represents the last version a property was available.\n  #\n  # See `ASR::ExclusionStrategies::Version`.\n  #\n  # !!!note\n  #     Value must be a `SemanticVersion` version.\n  annotation Until; end\n\n  # Can be applied to a method to make it act like a property.\n  #\n  # ## Example\n  #\n  # ```\n  # class Example\n  #   include ASR::Serializable\n  #\n  #   def initialize; end\n  #\n  #   property foo : String = \"foo\"\n  #\n  #   @[ASRA::VirtualProperty]\n  #   @[ASRA::Name(serialize: \"testing\")]\n  #   def some_method : Bool\n  #     false\n  #   end\n  #\n  #   @[ASRA::VirtualProperty]\n  #   def get_val : String\n  #     \"VAL\"\n  #   end\n  # end\n  #\n  # ASR.serializer.serialize Example.new, :json # => {\"foo\":\"foo\",\"testing\":false,\"get_val\":\"VAL\"}\n  # ```\n  #\n  # !!!warning\n  #     The return type restriction _MUST_ be defined.\n  annotation VirtualProperty; end\nend\n"
  },
  {
    "path": "src/components/serializer/src/any.cr",
    "content": "# Defines an abstraction that format specific types, such as `JSON::Any`, or `YAML::Any` must implement.\nmodule Athena::Serializer::Any\n  abstract def as_bool : Bool\n  abstract def as_i : Int32\n  abstract def as_i? : Int32?\n  abstract def as_f : Float64\n  abstract def as_f? : Float64?\n  abstract def as_f32 : Float32\n  abstract def as_f32? : Float32?\n  abstract def as_i64 : Int64\n  abstract def as_i64? : Int64?\n  abstract def as_s : String\n  abstract def as_s? : String?\n  abstract def as_a\n  abstract def as_a?\n\n  # ameba:disable Naming/PredicateName\n  abstract def is_nil? : Bool\n  abstract def dig(index_or_key : String | Int, *subkeys : Int | String)\n\n  abstract def raw\nend\n\n# :nodoc:\nstruct JSON::Any\n  include Athena::Serializer::Any\n\n  # ameba:disable Naming/PredicateName\n  def is_nil? : Bool\n    @raw.nil?\n  end\nend\n\n# :nodoc:\nstruct YAML::Any\n  include Athena::Serializer::Any\n\n  # ameba:disable Naming/PredicateName\n  def is_nil? : Bool\n    @raw.nil?\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/athena-serializer.cr",
    "content": "require \"semantic_version\"\nrequire \"uuid\"\n\nrequire \"json\"\nrequire \"yaml\"\n\nrequire \"athena-dependency_injection\"\n\nrequire \"./annotations\"\nrequire \"./any\"\nrequire \"./context\"\nrequire \"./serializable\"\nrequire \"./serializer_interface\"\nrequire \"./serializer\"\nrequire \"./property_metadata\"\nrequire \"./deserialization_context\"\nrequire \"./serialization_context\"\n\nrequire \"./construction/*\"\nrequire \"./exception/*\"\nrequire \"./exclusion_strategies/*\"\nrequire \"./navigators/*\"\nrequire \"./visitors/*\"\n\n# Convenience alias to make referencing `Athena::Serializer` types easier.\nalias ASR = Athena::Serializer\n\n# Convenience alias to make referencing `Athena::Serializer::Annotations` types easier.\nalias ASRA = Athena::Serializer::Annotations\n\n# :nodoc:\nmodule JSON; end\n\n# :nodoc:\nmodule YAML; end\n\n# Provides enhanced (de)serialization features.\nmodule Athena::Serializer\n  VERSION = \"0.4.3\"\n\n  # Returns an `ASR::SerializerInterface` instance for ad-hoc (de)serialization.\n  #\n  # The serializer is cached and only instantiated once.\n  class_getter serializer : ASR::SerializerInterface { ASR::Serializer.new }\n\n  # The built-in supported formats.\n  enum Format\n    JSON\n    YAML\n\n    # Returns the `ASR::Visitors::SerializationVisitorInterface` related to `self`.\n    def serialization_visitor\n      case self\n      in .json? then ASR::Visitors::JSONSerializationVisitor\n      in .yaml? then ASR::Visitors::YAMLSerializationVisitor\n      end\n    end\n\n    # Returns the `ASR::Visitors::DeserializationVisitorInterface` related to `self`.\n    def deserialization_visitor\n      case self\n      in .json? then ASR::Visitors::JSONDeserializationVisitor\n      in .yaml? then ASR::Visitors::YAMLDeserializationVisitor\n      end\n    end\n  end\n\n  # Contains all custom exceptions defined within `Athena::Serializer`.\n  # Also acts as a marker that can be used to rescue all serializer related exceptions.\n  module Exception; end\n\n  # Exclusion Strategies allow controlling which properties should be (de)serialized.\n  #\n  # `Athena::Serializer` includes two common strategies: `ASR::ExclusionStrategies::Groups`, and `ASR::ExclusionStrategies::Version`.\n  #\n  # Custom strategies can be implemented by via `ExclusionStrategies::ExclusionStrategyInterface`.\n  #\n  # !!!todo\n  #     Once feasible, support compile time exclusion strategies.\n  module ExclusionStrategies; end\n\n  # Used to denote a type that is (de)serializable.\n  #\n  # This module can be used to make the compiler happy in some situations, it doesn't do anything on its own.\n  # You most likely want to use `ASR::Serializable` instead.\n  #\n  # ```\n  # require \"athena-serializer\"\n  #\n  # abstract struct BaseModel\n  #   # `ASR::Model` is needed here to ensure typings are correct for the deserialization process.\n  #   # Child types should still include `ASR::Serializable`.\n  #   include ASR::Model\n  # end\n  #\n  # record ModelOne < BaseModel, id : Int32, name : String do\n  #   include ASR::Serializable\n  # end\n  #\n  # record ModelTwo < BaseModel, id : Int32, name : String do\n  #   include ASR::Serializable\n  # end\n  #\n  # record Unionable, type : BaseModel.class\n  # ```\n  module Model; end\nend\n"
  },
  {
    "path": "src/components/serializer/src/construction/instantiate_object_constructor.cr",
    "content": "require \"./object_constructor_interface\"\n\n# Default `ASR::ObjectConstructorInterface` implementation.\n#\n# Directly instantiates the object via a custom initializer added by `ASR::Serializable`.\nstruct Athena::Serializer::InstantiateObjectConstructor\n  include Athena::Serializer::ObjectConstructorInterface\n\n  # :inherit:\n  def construct(navigator : ASR::Navigators::DeserializationNavigatorInterface, properties : Array(PropertyMetadataBase), data : ASR::Any, type)\n    type.new navigator, properties, data\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/construction/object_constructor_interface.cr",
    "content": "# Determines how a new object is constructed during deserialization.\n#\n# By default it is directly instantiated via `.new` as part of `ASR::InstantiateObjectConstructor`.\n#\n# However custom constructors can be defined.  A use case could be retrieving the object from the database as part of a `PUT` request in order\n# to apply the deserialized data onto it.  This would allow it to retain the PK, any timestamps, or `ASRA::ReadOnly` values.\nmodule Athena::Serializer::ObjectConstructorInterface\n  # Creates an instance of *type* and applies the provided *properties* onto it, with the provided *data*.\n  abstract def construct(navigator : ASR::Navigators::DeserializationNavigatorInterface, properties : Array(PropertyMetadataBase), data : ASR::Any, type)\nend\n"
  },
  {
    "path": "src/components/serializer/src/context.cr",
    "content": "# Stores runtime data about the current action.\n#\n# Such as what serialization groups/version to use when serializing.\n#\n# !!!warning\n#     Cannot be used for more than one action.\nabstract class Athena::Serializer::Context\n  # The possible (de)serialization actions.\n  enum Direction\n    Deserialization\n    Serialization\n  end\n\n  # The `ASR::ExclusionStrategies::ExclusionStrategyInterface` being used.\n  getter exclusion_strategy : ASR::ExclusionStrategies::ExclusionStrategyInterface?\n\n  @initialized : Bool = false\n\n  # Returns the serialization groups, if any, currently set on `self`.\n  getter groups : Set(String)? = nil\n\n  # Returns the version, if any, currently set on `self`.\n  property version : SemanticVersion? = nil\n\n  # Returns which (de)serialization action `self` represents.\n  abstract def direction : ASR::Context::Direction\n\n  # Adds *strategy* to `self`.\n  #\n  # * `exclusion_strategy` is set to *strategy* if there previously was no strategy.\n  # * `exclusion_strategy` is set to `ASR::ExclusionStrategies::Disjunct` if there was a `exclusion_strategy` already set.\n  # * *strategy* is added to the `ASR::ExclusionStrategies::Disjunct` if there are multiple strategies.\n  def add_exclusion_strategy(strategy : ASR::ExclusionStrategies::ExclusionStrategyInterface) : self\n    current_strategy = @exclusion_strategy\n    case current_strategy\n    when Nil                                then @exclusion_strategy = strategy\n    when ASR::ExclusionStrategies::Disjunct then current_strategy.members << strategy\n    else\n      @exclusion_strategy = ASR::ExclusionStrategies::Disjunct.new [current_strategy, strategy]\n    end\n\n    self\n  end\n\n  # :nodoc:\n  def init : Nil\n    raise ASR::Exception::Logic.new \"This context was already initialized, and cannot be re-used.\" if @initialized\n\n    if v = @version\n      add_exclusion_strategy ASR::ExclusionStrategies::Version.new v\n    end\n\n    if g = @groups\n      add_exclusion_strategy ASR::ExclusionStrategies::Groups.new g\n    end\n\n    @initialized = true\n  end\n\n  # Sets the group(s) to compare against properties' `ASRA::Groups` annotations.\n  #\n  # Adds a `ASR::ExclusionStrategies::Groups` automatically if set.\n  def groups=(groups : Enumerable(String)) : self\n    raise ArgumentError.new \"Groups cannot be empty\" if groups.empty?\n\n    @groups = groups.to_set\n\n    self\n  end\n\n  # Sets the *version* to compare against properties' `ASRA::Since` and `ASRA::Until` annotations.\n  #\n  # Adds an `ASR::ExclusionStrategies::Version` automatically if set.\n  def version=(version : String) : self\n    @version = SemanticVersion.parse version\n\n    self\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/deserialization_context.cr",
    "content": "# The `ASR::Context` specific to deserialization.\nclass Athena::Serializer::DeserializationContext < Athena::Serializer::Context\n  def direction : ASR::Context::Direction\n    ASR::Context::Direction::Deserialization\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/exception/deserialization_exception.cr",
    "content": "# Represents an error that occurred during deserialization.\nclass Athena::Serializer::Exception::DeserializationException < RuntimeError\n  include Athena::Serializer::Exception\nend\n"
  },
  {
    "path": "src/components/serializer/src/exception/logic.cr",
    "content": "class Athena::Serializer::Exception::Logic < ::Exception\n  include Athena::Serializer::Exception\nend\n"
  },
  {
    "path": "src/components/serializer/src/exception/missing_required_property.cr",
    "content": "require \"./property_exception\"\n\n# Represents an error due to a missing required property that was not included in the input data.\n#\n# Exposes the missing property's name and type.\nclass Athena::Serializer::Exception::MissingRequiredProperty < Athena::Serializer::Exception::PropertyException\n  getter property_type : String\n\n  def initialize(property_name : String, @property_type : String)\n    super \"Missing required property: '#{property_name}'.\", property_name\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/exception/nil_required_property.cr",
    "content": "require \"./property_exception\"\n\n# Represents an error due to a required property that was `nil`.\n#\n# Exposes the property's name and type.\nclass Athena::Serializer::Exception::NilRequiredProperty < Athena::Serializer::Exception::PropertyException\n  getter property_type : String\n\n  def initialize(property_name : String, @property_type : String)\n    super \"Required property '#{property_name}' cannot be nil.\", property_name\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/exception/property_exception.cr",
    "content": "# Represents an error due to an invalid property.\n#\n# Exposes the property's name.\nclass Athena::Serializer::Exception::PropertyException < RuntimeError\n  include Athena::Serializer::Exception\n\n  getter property_name : String\n\n  def initialize(message : String, @property_name : String)\n    super message\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/exception/serialization_exception.cr",
    "content": "# Represents an error that occurred during serialization.\nclass Athena::Serializer::Exception::SerializationException < RuntimeError\n  include Athena::Serializer::Exception\nend\n"
  },
  {
    "path": "src/components/serializer/src/exclusion_strategies/disjunct.cr",
    "content": "require \"./exclusion_strategy_interface\"\n\n# Wraps an `Array(ASR::ExclusionStrategies::ExclusionStrategyInterface)`, excluding a property if any member skips it.\n#\n# Used internally to allow multiple exclusion strategies to be used within a single instance variable for `ASR::Context#add_exclusion_strategy`.\nstruct Athena::Serializer::ExclusionStrategies::Disjunct\n  include Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface\n\n  # The wrapped exclusion strategies.\n  getter members : Array(ASR::ExclusionStrategies::ExclusionStrategyInterface)\n\n  def initialize(@members : Array(ASR::ExclusionStrategies::ExclusionStrategyInterface)); end\n\n  # :inherit:\n  def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool\n    @members.any?(&.skip_property?(metadata, context))\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/exclusion_strategies/exclusion_strategy_interface.cr",
    "content": "# Represents a specific exclusion strategy.\n#\n# Custom logic can be implemented by defining a type with this interface.\n# It can then be used via `ASR::Context#add_exclusion_strategy`.\n#\n# ## Example\n#\n# ```\n# struct OddNumberExclusionStrategy\n#   include Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface\n#\n#   # :inherit:\n#   #\n#   # Skips serializing odd numbered values\n#   def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool\n#     # Don't skip if the value is nil\n#     return false unless value = (metadata.value)\n#\n#     # Only skip on serialization, if the value is an number, and if it's odd.\n#     context.is_a?(ASR::SerializationContext) && value.is_a?(Number) && value.odd?\n#   end\n# end\n#\n# serialization_context = ASR::SerializationContext.new\n# serialization_context.add_exclusion_strategy OddNumberExclusionStrategy.new\n#\n# deserialization_context = ASR::DeserializationContext.new\n# deserialization_context.add_exclusion_strategy OddNumberExclusionStrategy.new\n#\n# record Values, one : Int32 = 1, two : Int32 = 2, three : Int32 = 3 do\n#   include ASR::Serializable\n# end\n#\n# ASR.serializer.serialize Values.new, :json, serialization_context                                 # => {\"two\":2}\n# ASR.serializer.deserialize Values, %({\"one\":4,\"two\":5,\"three\":6}), :json, deserialization_context # => Values(@one=4, @three=6, @two=5)\n# ```\n#\n# ### Annotation Configurations\n#\n# Custom annotations can be defined using `ADI.configuration_annotation`.\n# These annotations will be exposed at runtime as part of the properties' metadata within exclusion strategies via `ASR::PropertyMetadata#annotation_configurations`.\n# The main purpose of this is to allow for more advanced annotation based exclusion strategies.\n#\n# ```\n# # Define an annotation called `IsActiveProperty` that accepts an optional `active` field.\n# ADI.configuration_annotation IsActiveProperty, active : Bool = true\n#\n# # Define an exclusion strategy that should skip \"inactive\" properties.\n# struct ActivePropertyExclusionStrategy\n#   include Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface\n#\n#   # :inherit:\n#   def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool\n#     # Don't skip on deserialization.\n#     return false if context.direction.deserialization?\n#\n#     ann_configs = metadata.annotation_configurations\n#\n#     # Skip if the property has the annotation and it's \"inactive\".\n#     ann_configs.has?(IsActiveProperty) && !ann_configs[IsActiveProperty].active\n#   end\n# end\n#\n# record Example, id : Int32, first_name : String, last_name : String, zip_code : Int32 do\n#   include ASR::Serializable\n#\n#   @[IsActiveProperty]\n#   @first_name : String\n#\n#   @[IsActiveProperty(active: false)]\n#   @last_name : String\n#\n#   # Can also be defined as a positional argument.\n#   @[IsActiveProperty(false)]\n#   @zip_code : Int32\n# end\n#\n# serialization_context = ASR::SerializationContext.new\n# serialization_context.add_exclusion_strategy ActivePropertyExclusionStrategy.new\n#\n# ASR.serializer.serialize Example.new(1, \"Jon\", \"Snow\", 90210), :json, serialization_context # => {\"id\":1,\"first_name\":\"Jon\"}\n# ```\nmodule Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface\n  # Returns `true` if a property should _NOT_ be (de)serialized.\n  abstract def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool\nend\n"
  },
  {
    "path": "src/components/serializer/src/exclusion_strategies/groups.cr",
    "content": "require \"./exclusion_strategy_interface\"\n\n# Allows creating different views of your objects by limiting which properties get serialized, based on the group(s) each property is a part of.\n#\n# It is enabled by default when using `ASR::Context#groups=`.\n#\n# ```\n# class Example\n#   include ASR::Serializable\n#\n#   def initialize; end\n#\n#   @[ASRA::Groups(\"list\", \"details\")]\n#   property id : Int64 = 1\n#\n#   @[ASRA::Groups(\"list\", \"details\")]\n#   property title : String = \"TITLE\"\n#\n#   @[ASRA::Groups(\"list\")]\n#   property comment_summaries : Array(String) = [\"Sentence 1.\", \"Sentence 2.\"]\n#\n#   @[ASRA::Groups(\"details\")]\n#   property comments : Array(String) = [\"Sentence 1.  Another sentence.\", \"Sentence 2.  Some other stuff.\"]\n#\n#   # Properties not explicitly given a group are added to the `\"default\"` group.\n#   property created_at : Time = Time.utc(2019, 1, 1)\n#   property updated_at : Time?\n# end\n#\n# obj = Example.new\n#\n# ASR.serializer.serialize obj, :json, ASR::SerializationContext.new.groups = [\"list\"]            # => {\"id\":1,\"title\":\"TITLE\",\"comment_summaries\":[\"Sentence 1.\",\"Sentence 2.\"]}\n# ASR.serializer.serialize obj, :json, ASR::SerializationContext.new.groups = [\"details\"]         # => {\"id\":1,\"title\":\"TITLE\",\"comments\":[\"Sentence 1.  Another sentence.\",\"Sentence 2.  Some other stuff.\"]}\n# ASR.serializer.serialize obj, :json, ASR::SerializationContext.new.groups = [\"list\", \"default\"] # => {\"id\":1,\"title\":\"TITLE\",\"comment_summaries\":[\"Sentence 1.\",\"Sentence 2.\"],\"created_at\":\"2019-01-01T00:00:00Z\"}\n# ```\nstruct Athena::Serializer::ExclusionStrategies::Groups\n  include Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface\n\n  @groups : Set(String)\n\n  def initialize(groups : Enumerable(String))\n    @groups = groups.to_set\n  end\n\n  def self.new(*groups : String)\n    new groups\n  end\n\n  # :inherit:\n  def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool\n    (metadata.groups & @groups).empty?\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/exclusion_strategies/version.cr",
    "content": "require \"./exclusion_strategy_interface\"\n\n# Serialize properties based on a `SemanticVersion` string.\n#\n# It is enabled by default when using `ASR::Context#version=`.\n#\n# ```\n# class Example\n#   include ASR::Serializable\n#\n#   def initialize; end\n#\n#   @[ASRA::Until(\"1.0.0\")]\n#   property name : String = \"Legacy Name\"\n#\n#   @[ASRA::Since(\"1.1.0\")]\n#   property name2 : String = \"New Name\"\n# end\n#\n# obj = Example.new\n#\n# ASR.serializer.serialize obj, :json, ASR::SerializationContext.new.version = \"0.30.0\" # => {\"name\":\"Legacy Name\"}\n# ASR.serializer.serialize obj, :json, ASR::SerializationContext.new.version = \"1.2.0\"  # => {\"name2\":\"New Name\"}\n# ```\nstruct Athena::Serializer::ExclusionStrategies::Version\n  include Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface\n\n  getter version : SemanticVersion\n\n  def initialize(@version : SemanticVersion); end\n\n  # :inherit:\n  def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool\n    # Skip if *version* is not at least *since_version*.\n    return true if (since_version = metadata.since_version) && @version < since_version\n\n    # Skip if *version* is greater than or equal to than *until_version*.\n    return true if (until_version = metadata.until_version) && @version >= until_version\n\n    false\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/navigators/deserialization_navigator.cr",
    "content": "module Athena::Serializer::Navigators::DeserializationNavigatorInterface\n  abstract def accept(type : T.class, data : ASR::Any) forall T\nend\n\nstruct Athena::Serializer::Navigators::DeserializationNavigator\n  include Athena::Serializer::Navigators::DeserializationNavigatorInterface\n\n  def initialize(\n    @visitor : ASR::Visitors::DeserializationVisitorInterface,\n    @context : ASR::DeserializationContext,\n    @object_constructor : ASR::ObjectConstructorInterface,\n  ); end\n\n  def accept(type : T.class, data : ASR::Any) forall T\n    {% unless T.instance <= ASR::Model %}\n      {% if T.class.has_method? :deserialize %}\n        @visitor.visit type, data\n      {% end %}\n    {% else %}\n      {% if ann = T.instance.annotation(ASRA::Discriminator) %}\n        if key = data[{{ann[:key]}}]?\n          type = case key\n            {% for k, t in ann[:map] %}\n              when {{k.id.stringify}} then {{t}}\n            {% end %}\n          else\n            raise ASR::Exception::PropertyException.new \"Unknown '#{{{ann[:key]}}}' discriminator value: '#{key}'.\", {{ann[:key].id.stringify}}\n          end\n        else\n          raise ASR::Exception::PropertyException.new \"Missing discriminator field '#{{{ann[:key]}}}'.\", {{ann[:key].id.stringify}}\n        end\n      {% end %}\n\n      properties = type.deserialization_properties\n\n      # Apply exclusion strategies if one is defined\n      if strategy = @context.exclusion_strategy\n        properties.reject! { |property| strategy.skip_property? property, @context }\n      end\n\n      object = @object_constructor.construct self, properties, data, type\n\n      object.run_postdeserialize\n\n      object\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/navigators/navigator_factory.cr",
    "content": "module Athena::Serializer::Navigators::NavigatorFactoryInterface\n  abstract def get_serialization_navigator(visitor : ASR::Visitors::SerializationVisitorInterface, context : ASR::SerializationContext) : ASR::Navigators::SerializationNavigatorInterface\n  abstract def get_deserialization_navigator(visitor : ASR::Visitors::DeserializationVisitorInterface, context : ASR::DeserializationContext) : ASR::Navigators::DeserializationNavigatorInterface\nend\n\nstruct Athena::Serializer::Navigators::NavigatorFactory\n  include Athena::Serializer::Navigators::NavigatorFactoryInterface\n\n  def initialize(@object_constructor : ASR::ObjectConstructorInterface = ASR::InstantiateObjectConstructor.new); end\n\n  def get_serialization_navigator(visitor : ASR::Visitors::SerializationVisitorInterface, context : ASR::SerializationContext) : ASR::Navigators::SerializationNavigatorInterface\n    ASR::Navigators::SerializationNavigator.new visitor, context\n  end\n\n  def get_deserialization_navigator(visitor : ASR::Visitors::DeserializationVisitorInterface, context : ASR::DeserializationContext) : ASR::Navigators::DeserializationNavigatorInterface\n    ASR::Navigators::DeserializationNavigator.new visitor, context, @object_constructor\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/navigators/serialization_navigator.cr",
    "content": "module Athena::Serializer::Navigators::SerializationNavigatorInterface\n  abstract def accept(data : ASR::Model) : Nil\n  abstract def accept(data : _) : Nil\nend\n\nstruct Athena::Serializer::Navigators::SerializationNavigator\n  include Athena::Serializer::Navigators::SerializationNavigatorInterface\n\n  def initialize(@visitor : ASR::Visitors::SerializationVisitorInterface, @context : ASR::SerializationContext); end\n\n  def accept(data : ASR::Model) : Nil\n    data.run_preserialize\n\n    properties = data.serialization_properties\n\n    # Apply exclusion strategies if one is defined\n    if strategy = @context.exclusion_strategy\n      properties.reject! { |property| strategy.skip_property? property, @context }\n    end\n\n    # Reject properties that should be skipped when empty\n    # or properties that should be skipped when nil\n    properties.reject! do |property|\n      val = property.value\n      skip_when_empty = property.skip_when_empty? && val.responds_to? :empty? && val.empty?\n      skip_nil = !@context.emit_nil? && val.nil?\n\n      skip_when_empty || skip_nil\n    end\n\n    # Process properties\n    @visitor.visit properties\n\n    data.run_postserialize\n  end\n\n  def accept(data : _) : Nil\n    @visitor.visit data\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/property_metadata.cr",
    "content": "# Parent type of a property metadata just used for typing.\n#\n# See `ASR::PropertyMetadata`.\nmodule Athena::Serializer::PropertyMetadataBase; end\n\n# Stores metadata related to a specific property.\n#\n# This includes its name (internal and external), value, versions/groups, and any aliases.\nstruct Athena::Serializer::PropertyMetadata(IvarType, ValueType)\n  include Athena::Serializer::PropertyMetadataBase\n\n  # The name of the property.\n  getter name : String\n\n  # The name that should be used for serialization/deserialization.\n  getter external_name : String\n\n  # The value of the property (when serializing).\n  getter value : ValueType\n\n  # The type of the property.\n  getter type : IvarType.class = IvarType\n\n  # Represents the first version this property is available.\n  #\n  # See `ASR::ExclusionStrategies::Version`.\n  property since_version : SemanticVersion?\n\n  # Represents the last version this property was available.\n  #\n  # See `ASR::ExclusionStrategies::Version`.\n  property until_version : SemanticVersion?\n\n  # The serialization groups this property belongs to.\n  #\n  # See `ASR::ExclusionStrategies::Groups`.\n  getter groups : Set(String) = Set{\"default\"}\n\n  # Deserialize this property from the property's name or any name in *aliases*.\n  #\n  # See `ASRA::Name`.\n  getter aliases : Array(String)\n\n  # If this property should not be serialized if it is empty.\n  #\n  # See `ASRA::SkipWhenEmpty`.\n  getter? skip_when_empty : Bool\n\n  # Returns annotations configurations registered via `ADI..configuration_annotation` and applied to this property.\n  #\n  # These configurations could then be accessed within an `ASR::ExclusionStrategies::ExclusionStrategyInterface`.\n  getter annotation_configurations : ADI::AnnotationConfigurations\n\n  def initialize(\n    @name : String,\n    @external_name : String,\n    @annotation_configurations : ADI::AnnotationConfigurations,\n    @value : ValueType = nil,\n    @skip_when_empty : Bool = false,\n    groups : Enumerable(String) = [\"default\"],\n    @aliases : Array(String) = [] of String,\n    @since_version : SemanticVersion? = nil,\n    @until_version : SemanticVersion? = nil,\n    @type : IvarType.class = IvarType,\n  )\n    @groups = groups.to_set\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/serializable.cr",
    "content": "# Adds the necessary methods to a `struct`/`class` to allow for (de)serialization of that type.\n#\n# ```\n# require \"athena-serializer\"\n#\n# record Example, id : Int32, name : String do\n#   include ASR::Serializable\n# end\n#\n# obj = ASR.serializer.deserialize Example, %({\"id\":1,\"name\":\"George\"}), :json\n# obj                                 # => Example(@id=1, @name=\"George\")\n# ASR.serializer.serialize obj, :yaml # =>\n# # ---\n# # id: 1\n# # name: George\n# ```\nmodule Athena::Serializer::Serializable\n  # :nodoc:\n  abstract def serialization_properties : Array(ASR::PropertyMetadataBase)\n\n  # :nodoc:\n  abstract def run_preserialize : Nil\n\n  # :nodoc:\n  abstract def run_postserialize : Nil\n\n  # :nodoc:\n  abstract def run_postdeserialize : Nil\n\n  macro included\n    {% verbatim do %}\n      include ASR::Model\n\n      # :nodoc:\n      def run_preserialize : Nil\n        {% for method in @type.methods.select &.annotation(ASRA::PreSerialize) %}\n          {{method.name}}\n        {% end %}\n      end\n\n      # :nodoc:\n      def run_postserialize : Nil\n        {% for method in @type.methods.select &.annotation(ASRA::PostSerialize) %}\n          {{method.name}}\n        {% end %}\n      end\n\n      # :nodoc:\n      def run_postdeserialize : Nil\n        {% for method in @type.methods.select &.annotation(ASRA::PostDeserialize) %}\n          {{method.name}}\n        {% end %}\n      end\n\n      # :nodoc:\n      def serialization_properties : Array(ASR::PropertyMetadataBase)\n        {% begin %}\n          # Construct the array of metadata from the properties on `self`.\n          # Takes into consideration some annotations to control how/when a property should be serialized\n          {%\n            instance_vars = @type.instance_vars\n              .reject(&.annotation(ASRA::Skip))\n              .reject(&.annotation(ASRA::IgnoreOnSerialize))\n              .reject do |ivar|\n                not_exposed = (ann = @type.annotation(ASRA::ExclusionPolicy)) && ann[0] == :all && !ivar.annotation(ASRA::Expose)\n                excluded = (ann = @type.annotation(ASRA::ExclusionPolicy)) && ann[0] == :none && ivar.annotation(ASRA::Exclude)\n\n                !ivar.annotation(ASRA::IgnoreOnDeserialize) && (not_exposed || excluded)\n              end\n          %}\n\n          {% property_hash = {} of Nil => Nil %}\n\n          {% for ivar in instance_vars %}\n            {% ivar_name = ivar.name.stringify %}\n\n            # Determine the serialized name of the ivar:\n            # 1. If the ivar has an `ASRA::Name` annotation with a `serialize` field, use that\n            # 2. If the type has an `ASRA::Name` annotation with a `strategy`, use that strategy\n            # 3. Fallback on the name of the ivar\n            {% external_name = if (name_ann = ivar.annotation(ASRA::Name)) && (serialized_name = name_ann[:serialize] || name_ann[:key])\n                                 serialized_name\n                               elsif (name_ann = @type.annotation(ASRA::Name)) && (strategy = name_ann[:serialization_strategy] || name_ann[:strategy])\n                                 if strategy == :camelcase\n                                   ivar_name.camelcase lower: true\n                                 elsif strategy == :underscore\n                                   ivar_name.underscore\n                                 elsif strategy == :identical\n                                   ivar_name\n                                 else\n                                   strategy.raise \"Invalid ASRA::Name strategy: '#{strategy}'.\"\n                                 end\n                               else\n                                 ivar_name\n                               end %}\n\n            {% annotation_configurations = {} of Nil => Nil %}\n\n            {% for ann_class in ADI::CUSTOM_ANNOTATIONS %}\n              {% ann_class = ann_class.resolve %}\n              {% annotations = [] of Nil %}\n\n              {% for ann in ivar.annotations ann_class %}\n                {% pos_args = ann.args.empty? ? \"Tuple.new\".id : ann.args %}\n                {% named_args = ann.named_args.empty? ? \"NamedTuple.new\".id : ann.named_args %}\n\n                {% annotations << \"#{ann_class}Configuration.new(#{ann.args.empty? ? \"\".id : \"#{ann.args.splat},\".id}#{ann.named_args.double_splat})\".id %}\n              {% end %}\n\n              {% annotation_configurations[ann_class] = \"#{annotations} of ADI::AnnotationConfigurations::ConfigurationBase\".id unless annotations.empty? %}\n            {% end %}\n\n            {%\n              value = (accessor = ivar.annotation(ASRA::Accessor)) && nil != accessor[:getter] ? accessor[:getter].id : %(@#{ivar.id}).id\n\n              property_hash[external_name] = %(ASR::PropertyMetadata(#{ivar.type}, typeof(#{value})).new(\n                name: #{ivar.name.stringify},\n                external_name: #{external_name},\n                annotation_configurations: ADI::AnnotationConfigurations.new(#{annotation_configurations} of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase)),\n                value: #{value},\n                skip_when_empty: #{!!ivar.annotation(ASRA::SkipWhenEmpty)},\n                groups: #{(ann = ivar.annotation(ASRA::Groups)) && !ann.args.empty? ? [ann.args.splat] : [\"default\"]},\n                since_version: #{(ann = ivar.annotation(ASRA::Since)) && nil != ann[0] ? \"SemanticVersion.parse(#{ann[0]})\".id : nil},\n                until_version: #{(ann = ivar.annotation(ASRA::Until)) && nil != ann[0] ? \"SemanticVersion.parse(#{ann[0]})\".id : nil},\n              )).id\n            %}\n            {% end %}\n\n          {% for m in @type.methods.select &.annotation(ASRA::VirtualProperty) %}\n            {% method_name = m.name %}\n            {% m.raise \"ASRA::VirtualProperty return type must be set for '#{@type.name}##{method_name}'.\" if m.return_type.is_a? Nop %}\n            {% external_name = (ann = m.annotation(ASRA::Name)) && (name = ann[:serialize]) ? name : m.name.stringify %}\n\n            {% method_annotation_configurations = {} of Nil => Nil %}\n\n            {% for ann_class in ADI::CUSTOM_ANNOTATIONS %}\n              {% ann_class = ann_class.resolve %}\n              {% annotations = [] of Nil %}\n\n              {% for ann in m.annotations ann_class %}\n                {% pos_args = ann.args.empty? ? \"Tuple.new\".id : ann.args %}\n                {% named_args = ann.named_args.empty? ? \"NamedTuple.new\".id : ann.named_args %}\n\n                {% annotations << \"#{ann_class}Configuration.new(#{ann.args.empty? ? \"\".id : \"#{ann.args.splat},\".id}#{ann.named_args.double_splat})\".id %}\n              {% end %}\n\n              {% method_annotation_configurations[ann_class] = \"#{annotations} of ADI::AnnotationConfigurations::ConfigurationBase\".id unless annotations.empty? %}\n            {% end %}\n\n            {% property_hash[external_name] = %(ASR::PropertyMetadata(#{m.return_type}, #{m.return_type}).new(\n                name: #{m.name.stringify},\n                external_name: #{external_name},\n                annotation_configurations: ADI::AnnotationConfigurations.new(#{method_annotation_configurations} of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase)),\n                value: #{m.name.id},\n                skip_when_empty: #{!!m.annotation(ASRA::SkipWhenEmpty)},\n                groups: #{(ann = m.annotation(ASRA::Groups)) && !ann.args.empty? ? [ann.args.splat] : [\"default\"]},\n                since_version: #{(ann = m.annotation(ASRA::Since)) && nil != ann[0] ? \"SemanticVersion.parse(#{ann[0]})\".id : nil},\n                until_version: #{(ann = m.annotation(ASRA::Until)) && nil != ann[0] ? \"SemanticVersion.parse(#{ann[0]})\".id : nil},\n              )).id %}\n          {% end %}\n\n          {% if (ann = @type.annotation(ASRA::AccessorOrder)) && nil != ann[0] %}\n            {% if ann[0] == :alphabetical %}\n              {% properties = property_hash.keys.sort.map { |key| property_hash[key] } %}\n            {% elsif ann[0] == :custom && nil != ann[:order] %}\n              {% ann.raise \"Not all properties were defined in the custom order for '#{@type}'.\" unless property_hash.keys.all? { |prop| ann[:order].map(&.id.stringify).includes? prop } %}\n              {% properties = ann[:order].map { |val| property_hash[val.id.stringify] || raise \"Unknown instance variable: '#{val.id}'.\" } %}\n            {% else %}\n              {% ann.raise \"Invalid ASR::AccessorOrder value: '#{ann[0].id}'.\" %}\n            {% end %}\n          {% else %}\n            {% properties = property_hash.values %}\n          {% end %}\n\n          {{properties}} of ASR::PropertyMetadataBase\n        {% end %}\n      end\n\n      # :nodoc:\n      def self.deserialization_properties : Array(ASR::PropertyMetadataBase)\n        {% verbatim do %}\n          {% begin %}\n            # Construct the array of metadata from the properties on `self`.\n            # Takes into consideration some annotations to control how/when a property should be serialized\n            {% instance_vars = @type.instance_vars\n                 .reject(&.annotation(ASRA::Skip))\n                 .reject { |ivar| (ann = ivar.annotation(ASRA::ReadOnly)); ann && !ivar.has_default_value? && !ivar.type.nilable? ? ivar.raise \"#{@type}##{ivar.name} is read-only but is not nilable nor has a default value\" : ann }\n                 .reject(&.annotation(ASRA::IgnoreOnDeserialize))\n                 .reject do |ivar|\n                   not_exposed = (ann = @type.annotation(ASRA::ExclusionPolicy)) && ann[0] == :all && !ivar.annotation(ASRA::Expose)\n                   excluded = (ann = @type.annotation(ASRA::ExclusionPolicy)) && ann[0] == :none && ivar.annotation(ASRA::Exclude)\n\n                   !ivar.annotation(ASRA::IgnoreOnSerialize) && (not_exposed || excluded)\n                 end %}\n\n            {{instance_vars.map do |ivar|\n                ivar_name = ivar.name.stringify\n                annotation_configurations = {} of Nil => Nil\n\n                ADI::CUSTOM_ANNOTATIONS.each do |ann_class|\n                  ann_class = ann_class.resolve\n                  annotations = [] of Nil\n\n                  ivar.annotations(ann_class).each do |ann|\n                    pos_args = ann.args.empty? ? \"Tuple.new\".id : ann.args\n                    named_args = ann.named_args.empty? ? \"NamedTuple.new\".id : ann.named_args\n\n                    annotations << \"#{ann_class}Configuration.new(#{ann.args.empty? ? \"\".id : \"#{ann.args.splat},\".id}#{ann.named_args.double_splat})\".id\n                  end\n\n                  annotation_configurations[ann_class] = \"#{annotations} of ADI::AnnotationConfigurations::ConfigurationBase\".id unless annotations.empty?\n                end\n\n                # Determine the serialized name of the ivar:\n                # 1. If the ivar has an `ASRA::Name` annotation with a `deserialize` field, use that\n                # 2. If the type has an `ASRA::Name` annotation with a `strategy`, use that strategy\n                # 3. Fallback on the name of the ivar\n                external_name = if (name_ann = ivar.annotation(ASRA::Name)) && (deserialized_name = name_ann[:deserialize] || name_ann[:key])\n                                  deserialized_name\n                                elsif (name_ann = @type.annotation(ASRA::Name)) && (strategy = name_ann[:deserialization_strategy] || name_ann[:strategy])\n                                  if strategy == :camelcase\n                                    ivar_name.camelcase lower: true\n                                  elsif strategy == :underscore\n                                    ivar_name.underscore\n                                  elsif strategy == :identical\n                                    ivar_name\n                                  else\n                                    strategy.raise \"Invalid ASRA::Name strategy: '#{strategy}'.\"\n                                  end\n                                else\n                                  ivar_name\n                                end\n\n                %(ASR::PropertyMetadata(#{ivar.type}, #{ivar.type}?).new(\n                  name: #{ivar.name.stringify},\n                  external_name: #{external_name},\n                  annotation_configurations: ADI::AnnotationConfigurations.new(#{annotation_configurations} of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase)),\n                  aliases: #{(ann = ivar.annotation(ASRA::Name)) && (aliases = ann[:aliases]) ? aliases : \"[] of String\".id},\n                  groups: #{(ann = ivar.annotation(ASRA::Groups)) && !ann.args.empty? ? [ann.args.splat] : [\"default\"]},\n                  since_version: #{(ann = ivar.annotation(ASRA::Since)) && nil != ann[0] ? \"SemanticVersion.parse(#{ann[0]})\".id : nil},\n                  until_version: #{(ann = ivar.annotation(ASRA::Until)) && nil != ann[0] ? \"SemanticVersion.parse(#{ann[0]})\".id : nil},\n                )).id\n              end}} of ASR::PropertyMetadataBase\n          {% end %}\n        {% end %}\n      end\n\n      # :nodoc:\n      def apply(navigator : ASR::Navigators::DeserializationNavigator, properties : Array(ASR::PropertyMetadataBase), data : ASR::Any)\n        self.initialize navigator, properties, data\n      end\n\n      # :nodoc:\n      def initialize(navigator : ASR::Navigators::DeserializationNavigatorInterface, properties : Array(ASR::PropertyMetadataBase), data : ASR::Any)\n        {% begin %}\n          {% for ivar, idx in @type.instance_vars %}\n            if (prop = properties.find { |p| p.name == {{ivar.name.stringify}} }) && (val = extract_value(prop, data, {{(ann = ivar.annotation(ASRA::Accessor)) ? ann[:path] : nil}}))\n              value = {% if (ann = ivar.annotation(ASRA::Accessor)) && (converter = ann[:converter]) %}\n                        {{converter.id}}.deserialize navigator, prop, val\n                      {% else %}\n                        navigator.accept {{ivar.type}}, val\n                      {% end %}\n\n              unless value.nil?\n                @{{ivar.id}} = value\n              else\n                {% if !ivar.type.nilable? && !ivar.has_default_value? %}\n                  raise ASR::Exception::NilRequiredProperty.new {{ivar.name.id.stringify}}, {{ivar.type.id.stringify}}\n                {% end %}\n              end\n            else\n              {% if !ivar.type.nilable? && !ivar.has_default_value? %}\n                raise ASR::Exception::MissingRequiredProperty.new {{ivar.name.id.stringify}}, {{ivar.type.id.stringify}}\n              {% end %}\n            end\n\n            {% if (ann = ivar.annotation(ASRA::Accessor)) && (setter = ann[:setter]) %}\n              self.{{setter.id}}(@{{ivar.id}})\n            {% end %}\n          {% end %}\n        {% end %}\n      end\n\n      # Attempts to extract a value from the *data* for the given *property*.\n      # Returns `nil` if a value could not be extracted.\n      private def extract_value(property : ASR::PropertyMetadataBase, data : ASR::Any, path : Tuple?) : ASR::Any?\n        return nil if data.raw.nil?\n\n        if path && (value = data.dig?(*path))\n          return value\n        end\n\n         if (key = property.aliases.find { |a| data[a]? }) && (value = data[key]?)\n          return value\n        end\n\n        if value = data[property.external_name]?\n          return value\n        end\n\n        nil\n      end\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/serialization_context.cr",
    "content": "# The `ASR::Context` specific to serialization.\n#\n# Allows specifying if `nil` values should be serialized.\nclass Athena::Serializer::SerializationContext < Athena::Serializer::Context\n  # If `nil` values should be serialized.\n  property? emit_nil : Bool = false\n\n  def direction : ASR::Context::Direction\n    ASR::Context::Direction::Serialization\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/serializer.cr",
    "content": "# Default implementation of `ASR::SerializerInterface`.\n#\n# Provides the main API used to (de)serialize objects.\n#\n# Custom formats can be implemented by creating the required visitors for that type, then overriding `#get_deserialization_visitor_class` and `#get_serialization_visitor_class`.\n#\n# ```\n# # Redefine the visitor class getters in order to first check for custom formats.\n# # This assumes these visitor types are defined, with the proper logic to handle\n# # the (de)serialization process.\n# struct Athena::Serializer::Serializer\n#   protected def get_deserialization_visitor_class(format : ASR::Format | String)\n#     return MessagePackDeserializationVisitor if format == \"message_pack\"\n#\n#     previous_def\n#   end\n#\n#   protected def get_serialization_visitor_class(format : ASR::Format | String)\n#     return MessagePackSerializationVisitor if format == \"message_pack\"\n#\n#     previous_def\n#   end\n# end\n# ```\nstruct Athena::Serializer::Serializer\n  include Athena::Serializer::SerializerInterface\n\n  def initialize(@navigator_factory : ASR::Navigators::NavigatorFactoryInterface = ASR::Navigators::NavigatorFactory.new); end\n\n  # :inherit:\n  def deserialize(type : _, data : String | IO, format : ASR::Format | String, context : ASR::DeserializationContext = ASR::DeserializationContext.new)\n    # Initialize the context.  Currently just used to apply default exclusion strategies\n    context.init\n\n    visitor = self.get_deserialization_visitor_class(format).new\n    navigator = @navigator_factory.get_deserialization_navigator visitor, context\n\n    visitor.navigator = navigator\n\n    navigator.accept type, visitor.prepare data\n  end\n\n  # :inherit:\n  def serialize(data : _, format : ASR::Format | String, context : ASR::SerializationContext = ASR::SerializationContext.new, **named_args) : String\n    String.build do |str|\n      serialize data, format, str, context, **named_args\n    end\n  end\n\n  # :inherit:\n  def serialize(data : _, format : ASR::Format | String, io : IO, context : ASR::SerializationContext = ASR::SerializationContext.new, **named_args) : Nil\n    # Initialize the context.  Currently just used to apply default exclusion strategies\n    context.init\n\n    visitor = self.get_serialization_visitor_class(format).new(io, named_args)\n    navigator = @navigator_factory.get_serialization_navigator visitor, context\n\n    visitor.navigator = navigator\n\n    visitor.prepare\n\n    navigator.accept data\n\n    visitor.finish\n  end\n\n  # Returns the `ASR::Visitors::DeserializationVisitorInterface.class` for the given *format*.\n  #\n  # Can be redefined in order to allow resolving custom formats.\n  protected def get_deserialization_visitor_class(format : ASR::Format | String)\n    return format.deserialization_visitor if format.is_a? ASR::Format\n\n    ASR::Format.parse(format).deserialization_visitor\n  end\n\n  # Returns the `ASR::Visitors::SerializationVisitorInterface.class` for the given *format*.\n  #\n  # Can be redefined in order to allow resolving custom formats.\n  protected def get_serialization_visitor_class(format : ASR::Format | String)\n    return format.serialization_visitor if format.is_a? ASR::Format\n\n    ASR::Format.parse(format).serialization_visitor\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/serializer_interface.cr",
    "content": "# The main entrypoint of `Athena::Serializer`.\nmodule Athena::Serializer::SerializerInterface\n  # Deserializes the provided *input_data* in the provided *format* into an instance of *type*, optionally with the provided *context*.\n  abstract def deserialize(type : ASR::Model.class, data : String | IO, format : ASR::Format | String, context : ASR::DeserializationContext = ASR::DeserializationContext.new)\n\n  # Serializes the provided *data* into *format*, optionally with the provided *context*.\n  abstract def serialize(data : _, format : ASR::Format | String, context : ASR::SerializationContext = ASR::SerializationContext.new, **named_args) : String\n\n  # Serializes the provided *data* into *format* writing it to the provided *io*, optionally with the provided *context*.=\n  abstract def serialize(data : _, format : ASR::Format | String, io : IO, context : ASR::SerializationContext = ASR::SerializationContext.new, **named_args) : Nil\nend\n"
  },
  {
    "path": "src/components/serializer/src/visitors/deserialization_visitor.cr",
    "content": "require \"./deserialization_visitor_interface\"\n\n# Implement deserialization logic based on `ASR::Any` common to all formats.\nabstract class Athena::Serializer::Visitors::DeserializationVisitor\n  include Athena::Serializer::Visitors::DeserializationVisitorInterface\n\n  property! navigator : Athena::Serializer::Navigators::DeserializationNavigatorInterface\n\n  def visit(type : Nil.class, data : ASR::Any) : Nil\n  end\n\n  def visit(type : _, data : ASR::Any)\n    type.deserialize self, data\n  end\n\n  def visit(type : T.class, data : _) forall T\n    data.as T\n  end\nend\n\n# Use a macro to build out primitive types\n{% begin %}\n  {%\n    primitives = {\n      Bool    => \".as_bool\",\n      Float32 => \".as_f32\",\n      Float64 => \".as_f\",\n      Int8    => \".as_i.to_i8\",\n      Int16   => \".as_i.to_i16\",\n      Int32   => \".as_i\",\n      Int64   => \".as_i64\",\n      UInt8   => \".as_i64.to_u8\",\n      UInt16  => \".as_i64.to_u16\",\n      UInt32  => \".as_i64.to_u32\",\n      UInt64  => \".as_i64.to_u64\",\n      String  => \".as_s\",\n    }\n  %}\n\n  {% for type, method in primitives %}\n    def {{type}}.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n      data{{method.id}}\n    rescue ex : TypeCastError\n      raise ASR::Exception::DeserializationException.new \"Could not parse {{type}} from '#{data}'.\"\n    end\n  {% end %}\n{% end %}\n\n# :nodoc:\ndef Array.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  collection = new\n  data.as_a.each do |item|\n    value = visitor.navigator.accept(T, item)\n\n    {% if T.nilable? %}\n      collection << value\n    {% else %}\n      value.try { |v| collection << v }\n    {% end %}\n  end\n  collection\nend\n\n# :nodoc:\ndef Set.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  collection = new\n  data.as_a.each do |item|\n    value = visitor.navigator.accept(T, item)\n\n    {% if T.nilable? %}\n      collection << value\n    {% else %}\n      value.try { |v| collection << v }\n    {% end %}\n  end\n  collection\nend\n\n# :nodoc:\ndef Deque.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  collection = new\n  data.as_a.each do |item|\n    value = visitor.navigator.accept(T, item)\n\n    {% if T.nilable? %}\n      collection << value\n    {% else %}\n      value.try { |v| collection << v }\n    {% end %}\n  end\n  collection\nend\n\n# :nodoc:\ndef Hash.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  hash = new\n  data.as_h.each do |key, value|\n    value = visitor.navigator.accept(V, value)\n\n    {% if T.nilable? %}\n      hash[visitor.visit(K, key)] = value\n    {% else %}\n      value.try { |v| hash[visitor.visit(K, key)] = v }\n    {% end %}\n  end\n  hash\nend\n\n# :nodoc:\ndef Tuple.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  arr = data.as_a\n  {% begin %}\n    Tuple.new(\n      {% for type, idx in T %}\n        visitor.visit({{type}}, arr[{{idx}}]),\n      {% end %}\n    )\n  {% end %}\nend\n\n# :nodoc:\ndef NamedTuple.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  {% begin %}\n    {% for key, type in T %}\n      %var{key.id} = (val = data[{{key.id.stringify}}]?) ? visitor.visit({{type}}, val) : nil\n    {% end %}\n\n    {% for key, type in T %}\n      if %var{key.id}.nil? && !{{type.nilable?}}\n        raise ASR::Exception::MissingRequiredProperty.new {{key.id.stringify}}, {{type.id.stringify}}\n      end\n    {% end %}\n\n    {\n      {% for key, type in T %}\n        {{key.id}}: (%var{key.id}).as({{type}}),\n      {% end %}\n    }\n  {% end %}\nend\n\n# :nodoc:\ndef Enum.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  if val = data.as_i64?\n    from_value val\n  elsif val = data.as_s?\n    parse val\n  else\n    raise ASR::Exception::DeserializationException.new \"Couldn't parse #{self} from '#{data}'.\"\n  end\nend\n\n# :nodoc:\ndef Time.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  Time::Format::ISO_8601_DATE_TIME.parse(data.as_s)\nend\n\n# :nodoc:\ndef Union.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  {% begin %}\n\n    # Try to parse the value as a primitive type first\n    # as its faster than trying to parse a non-primitive type\n    {% for type, index in T %}\n      {% if type == Nil %}\n        return nil if data.is_nil?\n      {% elsif type < Int %}\n        if value = data.as_i64?\n          return {{type}}.new! value\n        end\n      {% elsif type < Float %}\n        if value = data.as_f?\n          return {{type}}.new! value\n        end\n      {% elsif type == Bool || type == String %}\n        value = data.raw.as? {{type}}\n        return value unless value.nil?\n      {% end %}\n    {% end %}\n  {% end %}\n\n  # Lastly, try to parse a non-primitive type if there are more than 1.\n  {% for type in T %}\n    {% if type == Nil %}\n      return nil if data.is_nil?\n    {% else %}\n      begin\n        return visitor.navigator.accept {{type}}, data\n      rescue ex\n        # Ignore\n      end\n    {% end %}\n  {% end %}\n\n  raise ASR::Exception::DeserializationException.new \"Couldn't parse #{self} from '#{data}'.\"\nend\n"
  },
  {
    "path": "src/components/serializer/src/visitors/deserialization_visitor_interface.cr",
    "content": "module Athena::Serializer::Visitors::DeserializationVisitorInterface\n  abstract def prepare(data : IO | String) : ASR::Any\n  abstract def visit(type : Nil.class, data : ASR::Any) : Nil\n  abstract def visit(type : _, data : ASR::Any)\n  abstract def visit(type : _, data : _)\nend\n"
  },
  {
    "path": "src/components/serializer/src/visitors/json_deserialization_visitor.cr",
    "content": "class Athena::Serializer::Visitors::JSONDeserializationVisitor < Athena::Serializer::Visitors::DeserializationVisitor\n  def prepare(data : IO | String) : ASR::Any\n    JSON.parse data\n  end\nend\n\n# :nodoc:\ndef JSON::Any.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  data.as JSON::Any\nend\n"
  },
  {
    "path": "src/components/serializer/src/visitors/json_serialization_visitor.cr",
    "content": "require \"./serialization_visitor_interface\"\n\nclass Athena::Serializer::Visitors::JSONSerializationVisitor\n  include Athena::Serializer::Visitors::SerializationVisitorInterface\n\n  property! navigator : Athena::Serializer::Navigators::SerializationNavigatorInterface\n\n  def initialize(io : IO, named_args : NamedTuple) : Nil\n    @builder = JSON::Builder.new io\n    if indent = named_args[\"indent\"]?\n      @builder.indent = indent\n    end\n  end\n\n  def prepare : Nil\n    @builder.start_document\n  end\n\n  def finish : Nil\n    @builder.end_document\n  end\n\n  # :inherit:\n  def visit(data : Array(PropertyMetadataBase)) : Nil\n    @builder.object do\n      data.each do |prop|\n        @builder.field(prop.external_name) do\n          visit prop.value\n        end\n      end\n    end\n  end\n\n  def visit(data : Nil) : Nil\n    @builder.null\n  end\n\n  def visit(data : String | Symbol) : Nil\n    @builder.string data\n  end\n\n  def visit(data : Number) : Nil\n    @builder.number data\n  end\n\n  def visit(data : Bool) : Nil\n    @builder.bool data\n  end\n\n  def visit(data : ASR::Model) : Nil\n    navigator.accept data\n  end\n\n  def visit(data : Hash | NamedTuple) : Nil\n    @builder.object do\n      data.each do |key, value|\n        @builder.field key.to_s do\n          visit value\n        end\n      end\n    end\n  end\n\n  def visit(data : Enumerable) : Nil\n    @builder.array do\n      data.each { |v| visit v }\n    end\n  end\n\n  def visit(data : ASR::Any) : Nil\n    visit data.raw\n  end\n\n  def visit(data : Time) : Nil\n    visit data.to_rfc3339\n  end\n\n  def visit(data : Enum) : Nil\n    visit data.to_s.underscore\n  end\n\n  def visit(data : UUID) : Nil\n    visit data.to_s\n  end\n\n  def visit(data : _) : Nil\n    # Set non serializable types to null\n    @builder.null\n  end\nend\n"
  },
  {
    "path": "src/components/serializer/src/visitors/serialization_visitor_interface.cr",
    "content": "module Athena::Serializer::Visitors::SerializationVisitorInterface\n  abstract def prepare : Nil\n  abstract def finish : Nil\n\n  abstract def visit(data : Array(ASR::PropertyMetadataBase)) : Nil\n  abstract def visit(data : Bool) : Nil\n  abstract def visit(data : Enum) : Nil\n  abstract def visit(data : Enumerable) : Nil\n  abstract def visit(data : Hash) : Nil\n  abstract def visit(data : ASR::Any) : Nil\n  abstract def visit(data : NamedTuple) : Nil\n  abstract def visit(data : Nil) : Nil\n  abstract def visit(data : Number) : Nil\n  abstract def visit(data : ASR::Model) : Nil\n  abstract def visit(data : String) : Nil\n  abstract def visit(data : Symbol) : Nil\n  abstract def visit(data : Time) : Nil\n  abstract def visit(data : UUID) : Nil\nend\n"
  },
  {
    "path": "src/components/serializer/src/visitors/yaml_deserialization_visitor.cr",
    "content": "class Athena::Serializer::Visitors::YAMLDeserializationVisitor < Athena::Serializer::Visitors::DeserializationVisitor\n  def prepare(data : IO | String) : ASR::Any\n    YAML.parse data\n  end\nend\n\n# :nodoc:\ndef YAML::Any.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any)\n  data.as YAML::Any\nend\n"
  },
  {
    "path": "src/components/serializer/src/visitors/yaml_serialization_visitor.cr",
    "content": "class Athena::Serializer::Visitors::YAMLSerializationVisitor\n  include Athena::Serializer::Visitors::SerializationVisitorInterface\n\n  property! navigator : Athena::Serializer::Navigators::SerializationNavigatorInterface\n\n  def initialize(io : IO, named_args : NamedTuple) : Nil\n    @builder = YAML::Builder.new io\n  end\n\n  def prepare : Nil\n    @builder.start_stream\n    @builder.start_document\n  end\n\n  def finish : Nil\n    @builder.end_document\n    @builder.end_stream\n  end\n\n  # :inherit:\n  def visit(data : Array(PropertyMetadataBase)) : Nil\n    @builder.mapping do\n      data.each do |prop|\n        @builder.scalar prop.external_name\n        visit prop.value\n      end\n    end\n  end\n\n  def visit(data : String | Symbol | Number | Bool | Nil) : Nil\n    @builder.scalar data\n  end\n\n  def visit(data : ASR::Model) : Nil\n    navigator.accept data\n  end\n\n  def visit(data : Hash | NamedTuple) : Nil\n    @builder.mapping do\n      data.each do |key, value|\n        @builder.scalar key\n        visit value\n      end\n    end\n  end\n\n  def visit(data : Enumerable) : Nil\n    @builder.sequence do\n      data.each { |v| visit v }\n    end\n  end\n\n  def visit(data : ASR::Any) : Nil\n    visit data.raw\n  end\n\n  def visit(data : Time) : Nil\n    visit data.to_rfc3339\n  end\n\n  def visit(data : Enum) : Nil\n    visit data.to_s.underscore\n  end\n\n  def visit(data : UUID) : Nil\n    visit data.to_s\n  end\n\n  def visit(data : _) : Nil\n    # Set non serializable types to null\n    @builder.scalar nil\n  end\nend\n"
  },
  {
    "path": "src/components/spec/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/spec/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/spec/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.4.2] - 2026-04-19\n\n### Added\n\n- Generate macro code coverage report for `ASPEC::Methods.assert_compiles` ([#642]) (George Dietrich) <!-- blacksmoke16 -->\n- Add `ASPEC.compile_time_assert` helper function for use with `assert_compiles` ([#686]) (George Dietrich) <!-- blacksmoke16 -->\n- Add ability to add code before/after the actual code of `ASPEC::Methods.assert_compiles` and `ASPEC::Methods.assert_compile_time_error` ([#687]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix compile time error when inadvertently using a type name that conflicts with an internal component type ([#678]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix incorrect macro code coverage line numbers ([#686]) (George Dietrich) <!-- blacksmoke16 -->\n- Fix macro code coverage output file writing on windows ([#696]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.2]: https://github.com/athena-framework/spec/releases/tag/v0.4.2\n[#642]: https://github.com/athena-framework/athena/pull/642\n[#686]: https://github.com/athena-framework/athena/pull/686\n[#687]: https://github.com/athena-framework/athena/pull/687\n[#678]: https://github.com/athena-framework/athena/pull/678\n[#696]: https://github.com/athena-framework/athena/pull/696\n\n## [0.4.1] - 2025-11-12\n\n### Fixed\n\n- Fix segfault when interacting with a test case ivar object's ivar that was left uninitialized due to an exception in its initializer, within the `tear_down` method ([#613]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.1]: https://github.com/athena-framework/spec/releases/tag/v0.4.1\n[#613]: https://github.com/athena-framework/athena/pull/613\n\n## [0.4.0] - 2025-09-04\n\n### Added\n\n- Add support for generating macro code coverage reports for `.assert_error` and `.assert_compiles` methods ([#551]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Removed\n\n- Remove `codegen` parameter from `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` ([#551]) (George Dietrich) <!-- blacksmoke16 -->\n- Remove `ASPEC::Methods.assert_error` in favor of `ASPEC::Methods.assert_compile_time_error` and `ASPEC::Methods.assert_runtime_error` ([#551]) (George Dietrich) <!-- blacksmoke16 -->\n- Remove `ASPEC::Methods.assert_success` in favor of `ASPEC::Methods.assert_compiles` and `ASPEC::Methods.assert_executes` ([#551]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.0]: https://github.com/athena-framework/spec/releases/tag/v0.4.0\n[#551]: https://github.com/athena-framework/athena/pull/551\n\n## [0.3.11] - 2025-05-19\n\n### Fixed\n\n- Fix duplicate test case runs with abstract generic parent test case ([#538]) (George Dietrich)\n\n[0.3.11]: https://github.com/athena-framework/spec/releases/tag/v0.3.11\n[#538]: https://github.com/athena-framework/athena/pull/538\n\n## [0.3.10] - 2025-02-08\n\n### Changed\n\n- **Breaking:** prevent defining `ASPEC::TestCase#initialize` methods that accepts arguments/blocks ([#516]) (George Dietrich)\n\n[0.3.10]: https://github.com/athena-framework/spec/releases/tag/v0.3.10\n[#516]: https://github.com/athena-framework/athena/pull/516\n\n## [0.3.9] - 2025-01-26\n\n_Administrative release, no functional changes_\n\n[0.3.9]: https://github.com/athena-framework/spec/releases/tag/v0.3.9\n\n## [0.3.8] - 2024-07-31\n\n### Added\n\n- Add support for using the `CRYSTAL` ENV var to customize binary used for `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` ([#424]) (George Dietrich)\n\n[0.3.8]: https://github.com/athena-framework/spec/releases/tag/v0.3.8\n[#424]: https://github.com/athena-framework/athena/pull/424\n\n## [0.3.7] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.3.7]: https://github.com/athena-framework/spec/releases/tag/v0.3.7\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.3.6] - 2023-10-09\n\n_Administrative release, no functional changes_\n\n[0.3.6]: https://github.com/athena-framework/spec/releases/tag/v0.3.6\n\n## [0.3.5] - 2023-04-26\n\n### Fixed\n\n- Ensure `#before_all` runs exactly once, and before `#initialize` ([#285]) (George Dietrich)\n\n[0.3.5]: https://github.com/athena-framework/spec/releases/tag/v0.3.5\n[#285]: https://github.com/athena-framework/athena/pull/285\n\n## [0.3.4] - 2023-03-19\n\n### Fixed\n\n- Fix exceptions not being counted as errors when raised within the `initialize` method of a test case ([#276]) (George Dietrich)\n- Fix a documentation typo in the `TestWith` example ([#269]) (George Dietrich)\n\n[0.3.4]: https://github.com/athena-framework/spec/releases/tag/v0.3.4\n[#269]: https://github.com/athena-framework/athena/pull/269\n[#276]: https://github.com/athena-framework/athena/pull/276\n\n## [0.3.3] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/spec/releases/tag/v0.3.3\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.3.2] - 2023-01-16\n\n### Added\n\n- Add `ASPEC::TestCase::TestWith` that works similar to the `ASPEC::TestCase::DataProvider` but without needing to create a dedicated method ([#254]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/spec/releases/tag/v0.3.2\n[#254]: https://github.com/athena-framework/athena/pull/254\n\n## [0.3.1] - 2023-01-07\n\n### Changed\n\n- Update the docs to clarify the component needs to be manually installed ([#247]) (George Dietrich)\n\n### Added\n\n- Add support for *codegen* for the `ASPEC.assert_error` and `ASPEC.assert_success` methods ([#219]) (George Dietrich)\n- Add ability to skip running all examples within a test case via the `ASPEC::TestCase::Skip` annotation ([#248]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/spec/releases/tag/v0.3.1\n[#219]: https://github.com/athena-framework/athena/pull/219\n[#247]: https://github.com/athena-framework/athena/pull/247\n[#248]: https://github.com/athena-framework/athena/pull/248\n\n## [0.3.0] - 2022-05-14\n\n_First release a part of the monorepo._\n\n### Changed\n\n- **Breaking:** change the `assert_error` to no longer be file based. Code should now be provided as a HEREDOC argument to the method ([#173]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Added\n\n- Add `VERSION` constant to `Athena::Spec` namespace ([#166]) (George Dietrich)\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n- Add [ASPEC::Methods.assert_success](https://athenaframework.org/Spec/Methods/#Athena::Spec::Methods#assert_success(code,*,line,file)) ([#173]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/spec/releases/tag/v0.3.0\n[#166]: https://github.com/athena-framework/athena/pull/166\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n[#173]: https://github.com/athena-framework/athena/pull/173\n\n## [0.2.6] - 2021-11-03\n\n### Fixed\n\n- Fix `test` helper macro generating invalid method names by replacing all non alphanumeric chars with `_`  ([#12]) (George Dietrich)\n\n[0.2.6]: https://github.com/athena-framework/spec/releases/tag/v0.2.6\n[#12]: https://github.com/athena-framework/spec/pull/12\n\n## [0.2.5] - 2021-11-03\n\n### Fixed\n\n- Fix `test` helper macro not actually calling `yield`  ([#11]) (George Dietrich)\n\n[0.2.5]: https://github.com/athena-framework/spec/releases/tag/v0.2.5\n[#11]: https://github.com/athena-framework/spec/pull/11\n\n## [0.2.4] - 2021-01-29\n\n### Changed\n\n- Finish migration to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#9]) (George Dietrich)\n\n[0.2.4]: https://github.com/athena-framework/spec/releases/tag/v0.2.4\n[#9]: https://github.com/athena-framework/spec/pull/9\n\n## [0.2.3] - 2020-12-03\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#7]) (George Dietrich)\n\n[0.2.3]: https://github.com/athena-framework/spec/releases/tag/v0.2.3\n[#7]: https://github.com/athena-framework/spec/pull/7\n\n## [0.2.2] - 2020-10-02\n\n### Added\n\n- Add support for data providers defined in parent types ([#6]) (George Dietrich)\n\n[0.2.2]: https://github.com/athena-framework/spec/releases/tag/v0.2.2\n[#6]: https://github.com/athena-framework/spec/pull/6\n\n## [0.2.1] - 2020-09-25\n\n### Changed\n\n- Changed data provider generated `it` blocks have proper file names and line numbers ([#4]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/spec/releases/tag/v0.2.1\n[#4]: https://github.com/athena-framework/spec/pull/4\n\n## [0.2.0] - 2020-08-08\n\n### Changed\n\n- **Breaking:** require [data providers](https://athenaframework.org/Spec/TestCase/DataProvider/) methods to declare a return type of `Hash`, `NamedTuple`, `Tuple`, or `Array` ([#3]) (George Dietrich)\n- Changed data provider generated `it` blocks to include the key/index ([#2]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/spec/releases/tag/v0.2.0\n[#2]: https://github.com/athena-framework/spec/pull/2\n[#3]: https://github.com/athena-framework/spec/pull/3\n\n## [0.1.0] - 2020-08-06\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/spec/releases/tag/v0.1.0\n"
  },
  {
    "path": "src/components/spec/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/spec/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 George Dietrich\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": "src/components/spec/README.md",
    "content": "# Spec\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/spec.svg)](https://github.com/athena-framework/spec/releases)\n\nCommon/helpful Spec compliant testing utilities\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Spec).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/spec/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.4.0\n\n### Replace `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success`\n\nThe `ASPEC::Methods.assert_error` and `ASPEC::Methods.assert_success` methods have been removed in favor new methods that more clearly show intent:\n\n* If using `.assert_error` _without_ the `codegen` argument (the default), use `.assert_compile_time_error` instead\n* If using `.assert_error` _with_ `codegen: true` argument, use `.assert_runtime_error` instead\n* If using `.assert_success` _without_ the `codegen` argument (the default), use `.assert_compiles` instead\n* If using `.assert_success` _with_ `codegen: true` argument, use `.assert_executes` instead\n\n## Upgrade to 0.3.10\n\n### `ASPEC::TestCase#initialize` must be argless\n\nPreviously it was possible to define an `#initialize` method that accepted arguments/a block.\nThis was unintended and now results in a compile time error.\n"
  },
  {
    "path": "src/components/spec/docs/README.md",
    "content": "The `Athena::Spec` component provides common/helpful [Spec](https://crystal-lang.org/api/Spec.html) compliant testing utilities.\n\nNOTE: This component is _NOT_ a standalone testing framework, but is fully intended to be mixed with standard `describe`, `it`, and/or `pending` blocks depending on which approach makes the most sense for what is being tested.\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-spec:\n    github: athena-framework/spec\n    version: ~> 0.4.0\n```\n\nNext, require the shard within your `spec/spec_helper.cr` file, being sure things are required in this order:\n\n```crystal\nrequire \"spec\"\nrequire \"../src/main\" # Or whatever the name of your entrypoint file is called\nrequire \"athena-spec\"\n```\n\nFinally, call [ASPEC.run_all](/Spec/top_level/#Athena::Spec.run_all) at the bottom of `spec/spec_helper.cr` to ensure [ASPEC::TestCase](/Spec/TestCase/) based specs are ran as expected.\n\n## Usage\n\nA core focus of this component is allowing for a more classic unit testing approach that makes it easy to share/reduce test code duplication.\n[ASPEC::TestCase](/Spec/TestCase/) being the core type of this.\n\nThe primary benefit of this approach is that logic is more easily shared/reused as compared to the normal block based approach.\nI.e. a component can provide a base test case type that can be inherited from, a few methods implemented, and tada.\nFor example, [AVD::Spec::ConstraintValidatorTestCase](/Validator/Spec/ConstraintValidatorTestCase).\n\n```crystal\nstruct ExampleSpec < ASPEC::TestCase\n  def test_add : Nil\n    (1 + 2).should eq 3\n  end\nend\n```\n\nTIP: The [ASPEC::TestCase::DataProvider](/Spec/TestCase/DataProvider/) and [ASPEC::TestCase::TestWith](/Spec/TestCase/TestWith/) annotations can make testing similar code with different inputs super easy!\n"
  },
  {
    "path": "src/components/spec/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Spec\nsite_url: https://athenaframework.org/Spec/\nrepo_url: https://github.com/athena-framework/spec\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-spec/src/athena-spec.cr\n          source_locations:\n            lib/athena-spec: https://github.com/athena-framework/spec/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/spec/shard.yml",
    "content": "name: athena-spec\n\nversion: 0.4.2\n\ncrystal: ~> 1.17\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/spec\n\ndocumentation: https://athenaframework.org/Spec\n\ndescription: |\n  Common/helpful Spec compliant testing utilities.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n"
  },
  {
    "path": "src/components/spec/spec/athena-spec_spec.cr",
    "content": "require \"./spec_helper\"\n\n# IDK how to test the testing stuff,\n# so I'll just use the example and call it good enough.\n\nprivate class Calculator\n  def add(v1, v2)\n    v1 + v2\n  end\n\n  def subtract(v1, v2)\n    raise NotImplementedError.new \"TODO\"\n  end\nend\n\n@[ASPEC::TestCase::Skip]\nstruct SkipSpec < ASPEC::TestCase\n  def test_skipped\n    fail \"Test should have been skipped\"\n  end\nend\n\nstruct ExampleSpec < ASPEC::TestCase\n  @target : Calculator\n\n  def initialize : Nil\n    @target = Calculator.new\n  end\n\n  def test_add : Nil\n    @target.add(1, 2).should eq 3\n  end\n\n  # A pending test.\n  def ptest_subtract : Nil\n    @target.subtract(10, 5).should eq 5\n  end\n\n  test \"with macro helper\" do\n    @target.add(1, 2).should eq 3\n  end\n\n  test \"GET /api/:slug\" do\n    @target.add(1, 2).should eq 3\n  end\n\n  test \"123_foo bar\" do\n    @target.add(1, 2).should eq 3\n  end\nend\n\nabstract struct SomeTypeTestCase < ASPEC::TestCase\n  protected abstract def get_object : Calculator\n\n  def test_common : Nil\n    self.get_object.is_a? Calculator\n  end\nend\n\nstruct CalculatorTest < SomeTypeTestCase\n  protected def get_object : Calculator\n    Calculator.new\n  end\n\n  def test_specific : Nil\n    self.get_object.add(1, 1).should eq 2\n  end\nend\n\nstruct DataProviderTest < ASPEC::TestCase\n  @[DataProvider(\"get_values_hash\")]\n  @[DataProvider(\"get_values_named_tuple\")]\n  def test_squares(value : Int32, expected : Int32) : Nil\n    (value ** 2).should eq expected\n  end\n\n  def get_values_hash : Hash\n    {\n      \"two\"   => {2, 4},\n      \"three\" => {3, 9},\n    }\n  end\n\n  def get_values_named_tuple : NamedTuple\n    {\n      four: {4, 16},\n      five: {5, 25},\n    }\n  end\n\n  @[DataProvider(\"get_values_array\")]\n  @[DataProvider(\"get_values_tuple\")]\n  def test_cubes(value : Int32, expected : Int32) : Nil\n    (value ** 3).should eq expected\n  end\n\n  def get_values_array : Array\n    [\n      {2, 8},\n      {3, 27},\n    ]\n  end\n\n  def get_values_tuple : Tuple\n    {\n      {4, 64},\n      {5, 125},\n    }\n  end\nend\n\nabstract struct AbstractParent < ASPEC::TestCase\n  @[DataProvider(\"get_values\")]\n  def test_cubes(value : Int32, expected : Int32) : Nil\n    value.should eq expected\n  end\n\n  def get_values : Tuple\n    {\n      {1, 1},\n      {2, 2},\n    }\n  end\nend\n\nstruct Child < AbstractParent; end\n\nstruct TestWithTest < ASPEC::TestCase\n  @[TestWith(\n    {4, 64},\n    {5, 125},\n  )]\n  def test_cubes(value : Int32, expected : Int32) : Nil\n    (value ** 3).should eq expected\n  end\n\n  @[TestWith(\n    two: {2, 4},\n    three: {3, 9},\n    \"with spaces\": {4, 16},\n  )]\n  def test_squares(value : Int32, expected : Int32) : Nil\n    (value ** 2).should eq expected\n  end\nend\n\nstruct BeforeAllTest < ASPEC::TestCase\n  @count : Int32 = 0\n\n  def initialize\n    @count.should eq 1\n  end\n\n  def before_all : Nil\n    @count += 1\n  end\n\n  def test_before_all_runs_before_initialize : Nil\n    # no-op\n  end\n\n  def test_before_all_runs_before_initialize2 : Nil\n    # no-op\n  end\nend\n\nabstract struct GenericTestCase(T) < ASPEC::TestCase\nend\n\nstruct GenericIntTest < GenericTestCase(Int32)\n  @@count : Int32 = 0\n\n  def test_runs_once\n    1.should eq 1\n    @@count += 1\n  end\n\n  def after_all : Nil\n    it \"runs generic string inheritance test cases only once\" { @@count.should eq 1 }\n  end\nend\n\nstruct GenericStringTest < GenericTestCase(String)\n  @@count : Int32 = 0\n\n  def test_runs_once\n    1.should eq 1\n    @@count += 1\n  end\n\n  def after_all : Nil\n    it \"runs generic int inheritance test cases only once\" { @@count.should eq 1 }\n  end\nend\n"
  },
  {
    "path": "src/components/spec/spec/compiler_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate def assert_compile_time_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compile_time_error message, code, line: line, preamble: %(require \"./spec_helper.cr\"), postamble: \"TestTestCase.run\"\nend\n\nprivate def assert_runtime_error(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_runtime_error message, <<-CR, line: line\n    require \"./spec_helper.cr\"\n    #{code}\n    TestTestCase.run\n  CR\nend\n\nprivate def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil\n  ASPEC::Methods.assert_compiles code, line: line, preamble: %(require \"./spec_helper.cr\"), postamble: \"ASPEC.run_all\"\nend\n\ndescribe Athena::Spec do\n  describe \"compiler errors\", tags: \"compiled\" do\n    describe ASPEC::TestCase::TestWith do\n      describe \"args\" do\n        it \"non tuple value\" do\n          assert_compile_time_error \"Expected argument #0 of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to be a Tuple, but got 'NumberLiteral'.\", <<-CODE\n            struct TestTestCase < ASPEC::TestCase\n              @[TestWith(\n                125\n              )]\n              def test_case(value : Int32, expected : Int32) : Nil\n              end\n            end\n          CODE\n        end\n\n        it \"argument count mismatch\" do\n          assert_compile_time_error \"Expected argument #0 of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to contain 2 values, but got 1.\", <<-CODE\n            struct TestTestCase < ASPEC::TestCase\n              @[TestWith(\n                {125}\n              )]\n              def test_case(value : Int32, expected : Int32) : Nil\n              end\n            end\n          CODE\n        end\n      end\n\n      describe \"named args\" do\n        it \"non tuple value\" do\n          assert_compile_time_error \" Expected the value of argument 'value' of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to be a Tuple, but got 'NumberLiteral'.\", <<-CODE\n            struct TestTestCase < ASPEC::TestCase\n              @[TestWith(\n                value: 125\n              )]\n              def test_case(value : Int32, expected : Int32) : Nil\n              end\n            end\n          CODE\n        end\n\n        it \"argument count mismatch\" do\n          assert_compile_time_error \"Expected the value of argument 'value' of the 'ASPEC::TestCase::TestWith' annotation applied to 'TestTestCase#test_case' to contain 2 values, but got 1.\", <<-CODE\n            struct TestTestCase < ASPEC::TestCase\n              @[TestWith(\n                value: {125}\n              )]\n              def test_case(value : Int32, expected : Int32) : Nil\n              end\n            end\n          CODE\n        end\n      end\n    end\n\n    describe \"exception during initialize\" do\n      it \"reports the errors once per test case\" do\n        assert_runtime_error \"oh noes\", <<-CODE\n          struct TestTestCase < ASPEC::TestCase\n            def initialize\n              raise \"oh noes\"\n            end\n\n            def test_one\n              1.should eq 1\n            end\n\n            def test_two\n              2.should eq 2\n            end\n          end\n        CODE\n      end\n\n      it \"reports actual failing tests\" do\n        assert_runtime_error \" Expected: 2\\n            got: 1\", <<-CODE\n          struct TestTestCase < ASPEC::TestCase\n            def test_one\n              1.should eq 2\n            end\n          end\n        CODE\n      end\n    end\n\n    it \"errors if defining a non-argless initializer\" do\n      assert_compile_time_error \"`ASPEC::TestCase` initializers must be argless and non-yielding.\", <<-CODE\n        struct TestTestCase < ASPEC::TestCase\n          def initialize(id : Int32); end\n        end\n        CODE\n    end\n\n    it \"errors if defining a yielding initializer\" do\n      assert_compile_time_error \"`ASPEC::TestCase` initializers must be argless and non-yielding.\", <<-CODE\n        struct TestTestCase < ASPEC::TestCase\n          def initialize(&); end\n        end\n        CODE\n    end\n\n    it \"bubbles up exceptions that happen when instantiating another object within initialize\" do\n      assert_runtime_error \"Raise during initialize\", <<-CODE\n          struct TestTestCase < ASPEC::TestCase\n            private class Foo\n              # Needs some ivar that is uninitialized\n              getter id : Int32 = 123\n\n              def initialize\n                # Makes `@target` become not fully initialized\n                raise \"Raise during initialize\"\n              end\n            end\n\n            @target : Foo\n\n            def initialize\n              @target = Foo.new\n            end\n\n            def tear_down : Nil\n              # Segfaults accessing uninitialized ivar\n              @target.id.should eq 123\n            end\n\n            def test_equals : Nil\n              # no-op\n            end\n          end\n        CODE\n    end\n\n    it \"compiles when a TestCase type conflicts with internal ASPEC types\" do\n      assert_compiles <<-CR\n        struct TestCase::Foo < ASPEC::TestCase\n          def test_add\n            (2 + 2).should eq 4\n          end\n        end\n      CR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/spec/spec/methods_spec.cr",
    "content": "require \"./spec_helper\"\nrequire \"file_utils\"\n\ndescribe ASPEC::Methods do\n  describe \".assert_compile_time_error\", tags: \"compiled\" do\n    it \"allows customizing crystal binary via CRYSTAL env var\" do\n      # Do this in its own sub-process to avoid mucking with ENV.\n      message = {% if flag? \"windows\" %}\n                  \"The system cannot find the file specified\"\n                {% else %}\n                  \"No such file or directory\"\n                {% end %}\n\n      assert_runtime_error message, <<-CR\n        require \"./spec_helper\"\n\n        ENV[\"CRYSTAL\"] = \"/path/to/crystal\"\n\n        assert_compile_time_error \"\", \"\"\n      CR\n    end\n\n    it do\n      assert_compile_time_error \"can't instantiate abstract class Foo\", <<-CR\n          abstract class Foo; end\n          Foo.new\n        CR\n    end\n  end\n\n  describe \".assert_runtime_error\", tags: \"compiled\" do\n    it do\n      assert_runtime_error \"Oh no\", <<-CR\n          raise \"Oh no\"\n        CR\n    end\n  end\n\n  describe \".assert_compiles\" do\n    it tags: \"compiled\" do\n      assert_compiles <<-CR\n          raise \"Oh no\"\n        CR\n    end\n\n    # These run in the unit test suite to ensure this integration also works on other OSs.\n    describe \"adjusts macro coverage line numbers for the stdin file\" do\n      it \"without before/after code\" do\n        temp_dir = File.tempname\n        Dir.mkdir_p(temp_dir)\n\n        ENV[\"ATHENA_SPEC_COVERAGE_OUTPUT_DIR\"] = temp_dir\n\n        # We expect the line `{% x = 1 %}` to be called. Using __LINE__ and adding 3 keeps this robust if other tests are added/removed/re-arranged.\n        spec_line = __LINE__ + 2\n        code_line = __LINE__ + 3\n        ASPEC::Methods.assert_compiles <<-'CR'\n          macro finished\n            {% x = 1 %}\n          end\n        CR\n\n        coverage_file = Dir.glob(::Path[temp_dir, \"macro_coverage.*.codecov.json\"]).first\n        coverage_file.should end_with \"macro_coverage.methods_spec#L#{spec_line}.codecov.json\"\n\n        File.open coverage_file do |file|\n          coverage = JSON.parse file\n\n          # Should be 1 coverage file.\n          coverages = coverage.as_h[\"coverage\"].as_h\n          coverages.size.should eq 1\n\n          coverages.each_value do |file_coverage|\n            # The expected line number should be called once\n            file_coverage.as_h.should eq({code_line.to_s => 1})\n          end\n        end\n      ensure\n        ENV.delete(\"ATHENA_SPEC_COVERAGE_OUTPUT_DIR\")\n        FileUtils.rm_rf(temp_dir) if temp_dir\n      end\n\n      it \"with code before\" do\n        temp_dir = File.tempname\n        Dir.mkdir_p(temp_dir)\n\n        ENV[\"ATHENA_SPEC_COVERAGE_OUTPUT_DIR\"] = temp_dir\n\n        # We expect the line `{% x = 1 %}` to be called. Using __LINE__ and adding 3 keeps this robust if other tests are added/removed/re-arranged.\n        spec_line = __LINE__ + 2\n        code_line = __LINE__ + 3\n        ASPEC::Methods.assert_compiles <<-'CR', preamble: %(puts \"hi\")\n          macro finished\n            {% x = 1 %}\n          end\n        CR\n\n        coverage_file = Dir.glob(::Path[temp_dir, \"macro_coverage.*.codecov.json\"]).first\n        coverage_file.should end_with \"macro_coverage.methods_spec#L#{spec_line}.codecov.json\"\n\n        File.open coverage_file do |file|\n          coverage = JSON.parse file\n\n          # Should be 1 coverage file.\n          coverages = coverage.as_h[\"coverage\"].as_h\n          coverages.size.should eq 1\n\n          coverages.each_value do |file_coverage|\n            # The expected line number should be called once\n            file_coverage.as_h.should eq({code_line.to_s => 1})\n          end\n        end\n      ensure\n        ENV.delete(\"ATHENA_SPEC_COVERAGE_OUTPUT_DIR\")\n        FileUtils.rm_rf(temp_dir) if temp_dir\n      end\n    end\n  end\n\n  describe \".assert_executes\", tags: \"compiled\" do\n    it do\n      assert_executes <<-CR\n        puts 1 + 1\n        CR\n    end\n  end\n\n  describe \".run_executable\", tags: \"compiled\" do\n    it \"without input\" do\n      run_executable \"echo\", [\"foo\", \"bar\"] do |output, error, status|\n        output.should eq \"foo bar\\n\"\n        error.should be_empty\n        status.success?.should be_true\n      end\n    end\n\n    it \"with input\" do\n      input = IO::Memory.new \"foo\\nbar\"\n\n      run_executable \"cat\", input, [\"-e\"] do |output, error, status|\n        output.should eq \"foo$\\nbar\"\n        error.should be_empty\n        status.success?.should be_true\n      end\n    end\n\n    it \"with error output\" do\n      run_executable \"cat\", args: [\"missing.txt\"] do |output, error, status|\n        output.should be_empty\n        error.should contain \"No such file or directory\"\n        status.success?.should be_false\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/spec/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"../src/athena-spec\"\n\ninclude ASPEC::Methods\n\nASPEC.run_all\n"
  },
  {
    "path": "src/components/spec/src/athena-spec.cr",
    "content": "# Convenience alias to make referencing `Athena::Spec` types easier.\nalias ASPEC = Athena::Spec\n\nrequire \"json\"\nrequire \"./methods\"\nrequire \"./test_case\"\n\n# A set of common [Spec](https://crystal-lang.org/api/Spec.html) compliant testing utilities/types.\nmodule Athena::Spec\n  VERSION = \"0.4.2\"\n\n  # Asserts a *condition*, raising *message* if it is falsey.\n  # This is primarily intended to be used with `ASPEC::Methods.assert_compiles` to assert state that exists at compile time.\n  # An example of this is how internally Athena's specs do something like this to assert aspects of wired up services are correct:\n  #\n  # ```\n  # ASPEC::Methods.assert_compiles <<-'CR'\n  #   require \"../spec_helper\"\n  #\n  #   @[ADI::Register(public: true)]\n  #   record MyService\n  #\n  #   macro finished\n  #     macro finished\n  #       \\{%\n  #         service = ADI::ServiceContainer::SERVICE_HASH[\"my_service\"]\n  #       %}\n  #       ASPEC.compile_time_assert(\\{{ service[\"public\"] == true }}, \"Expected service to be public\")\n  #     end\n  #   end\n  # CR\n  # ```\n  macro compile_time_assert(condition, message = \"Compile-time assertion failed\")\n    {% condition.raise message unless condition %}\n  end\n\n  # Runs all `ASPEC::TestCase`s.\n  #\n  # Is equivalent to manually calling `.run` on each test case.\n  def self.run_all : Nil\n    # `#uniq` is to work around https://github.com/crystal-lang/crystal/issues/15793.\n    {% for unit_test in ASPEC::TestCase.all_subclasses.reject { |tc| tc.abstract? || tc.annotation(ASPEC::TestCase::Skip) }.uniq %}\n      ::{{unit_test.id}}.run\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/spec/src/methods.cr",
    "content": "# Namespace for common/helpful testing methods.\n#\n# This module can be included into your `spec_helper` in order\n# to allow your specs to use them all.  This module is also\n# included into `ASPEC::TestCase` by default to allow using them\n# within your unit tests as well.\n#\n# May be reopened to add additional application specific helpers.\nmodule Athena::Spec::Methods\n  extend self\n\n  # Executes the provided Crystal *code* and asserts it results in a compile time error with the provided *message*.\n  #\n  # ```\n  # ASPEC::Methods.assert_compile_time_error \"can't instantiate abstract class Foo\", <<-CR\n  #   abstract class Foo; end\n  #   Foo.new\n  # CR\n  # ```\n  #\n  # The *preamble* and *postamble* parameters may be used to add code before/after the actual *code*.\n  # This is primarily useful when wrapping this method in another private method for use within a specific test file to share common before/after code.\n  #\n  # ```\n  # private def assert_compiles(message : String, code : String, *, line : Int32 = __LINE__) : Nil\n  #   ASPEC::Methods.assert_compile_time_error message, code, preamble: %(require \"../spec_helper.cr\"), postamble: \"MyClass.new\", line: line\n  # end\n  # ```\n  #\n  # NOTE: When files are required within the *code*, they are relative to the file calling this method.\n  def assert_compile_time_error(message : String, code : String, *, preamble : String = \"\", postamble : String = \"\", line : Int32 = __LINE__, file : String = __FILE__) : Nil\n    full_code = String.build do |str|\n      str.puts preamble unless preamble.empty?\n      str.puts code\n      str.puts postamble unless postamble.empty?\n    end\n\n    std_out = IO::Memory.new\n    std_err = IO::Memory.new\n\n    result = execute full_code, std_out, std_err, file, codegen: false, macro_code_coverage: true\n\n    fail std_err.to_s, line: line if result.success?\n    std_err.to_s.should contain(message), line: line\n    std_err.close\n\n    # Ignore coverage report output if the output dir is not defined, or if there is no report.\n    # TODO: Maybe default this to something?\n    if !std_out.empty? && (macro_coverage_output_dir = ENV[\"ATHENA_SPEC_COVERAGE_OUTPUT_DIR\"]?.presence)\n      coverage_line_offset = preamble.empty? ? line : line - preamble.count('\\n') - 1\n      File.open ::Path[macro_coverage_output_dir, \"macro_coverage.#{Path[file].stem}#L#{line}.codecov.json\"], \"w\" do |coverage_report|\n        coverage_report.print adjust_coverage_line_numbers(std_out, file, coverage_line_offset)\n      end\n    end\n\n    std_out.close\n  end\n\n  # Executes the provided Crystal *code* and asserts it results in a runtime error with the provided *message*.\n  # This can be helpful in order to test something in isolation, without affecting other test cases.\n  #\n  # ```\n  # ASPEC::Methods.assert_runtime_error \"Oh noes!\", <<-CR\n  #  raise \"Oh noes!\"\n  # CR\n  # ```\n  #\n  # NOTE: When files are required within the *code*, they are relative to the file calling this method.\n  def assert_runtime_error(message : String, code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil\n    buffer = IO::Memory.new\n    result = execute code, buffer, buffer, file, codegen: true\n\n    fail buffer.to_s, line: line if result.success?\n    buffer.to_s.should contain(message), line: line\n    buffer.close\n  end\n\n  # Similar to `.assert_compile_time_error`, but asserts the provided Crystal *code* successfully compiles.\n  #\n  # ```\n  # ASPEC::Methods.assert_compiles <<-CR\n  #   raise \"Still passes\"\n  # CR\n  # ```\n  #\n  # The *preamble* and *postamble* parameters may be used to add code before/after the actual *code*.\n  # This is primarily useful when wrapping this method in another private method for use within a specific test file to share common before/after code.\n  #\n  # ```\n  # private def assert_compiles(code : String, *, line : Int32 = __LINE__) : Nil\n  #   ASPEC::Methods.assert_compiles code, preamble: %(require \"../spec_helper.cr\"), postamble: \"MyClass.new\", line: line\n  # end\n  # ```\n  #\n  # NOTE: When files are required within the *code*, they are relative to the file calling this method.\n  def assert_compiles(code : String, *, preamble : String = \"\", postamble : String = \"\", line : Int32 = __LINE__, file : String = __FILE__) : Nil\n    full_code = String.build do |str|\n      str.puts preamble unless preamble.empty?\n      str.puts code\n      str.puts postamble unless postamble.empty?\n    end\n\n    std_out = IO::Memory.new\n    std_err = IO::Memory.new\n\n    result = execute full_code, std_out, std_err, file, codegen: false, macro_code_coverage: true\n\n    fail std_err.to_s, line: line unless result.success?\n    std_err.close\n\n    # Ignore coverage report output if the output dir is not defined, or if there is no report.\n    # TODO: Maybe default this to something?\n    if !std_out.empty? && (macro_coverage_output_dir = ENV[\"ATHENA_SPEC_COVERAGE_OUTPUT_DIR\"]?.presence)\n      coverage_line_offset = preamble.empty? ? line : line - preamble.count('\\n') - 1\n      File.open ::Path[macro_coverage_output_dir, \"macro_coverage.#{Path[file].stem}#L#{line}.codecov.json\"], \"w\" do |coverage_report|\n        coverage_report.print adjust_coverage_line_numbers(std_out, file, coverage_line_offset)\n      end\n    end\n\n    std_out.close\n  end\n\n  # Similar to `.assert_runtime_error`, but asserts the provided Crystal *code* successfully executes.\n  #\n  # ```\n  # ASPEC::Methods.assert_executes <<-CR\n  #   puts 2 + 2\n  # CR\n  # ```\n  #\n  # NOTE: When files are required within the *code*, they are relative to the file calling this method.\n  def assert_executes(code : String, *, line : Int32 = __LINE__, file : String = __FILE__) : Nil\n    buffer = IO::Memory.new\n    result = execute code, buffer, buffer, file, codegen: true\n\n    fail buffer.to_s, line: line unless result.success?\n    buffer.close\n  end\n\n  # Adjusts line numbers in the coverage JSON for the stdin file entry.\n  # When code is piped via stdin with --stdin-filename, Crystal reports line numbers relative to the stdin content rather than the actual file.\n  private def adjust_coverage_line_numbers(coverage_output : IO, stdin_filename : String, line_offset : Int32) : String\n    coverage = JSON.parse(coverage_output.rewind.gets_to_end)\n\n    coverage.as_h[\"coverage\"].as_h.each do |filename, file_coverage|\n      next unless stdin_filename.ends_with? filename\n      file_coverage.as_h.transform_keys! { |key| (key.to_i + line_offset).to_s }\n    end\n\n    coverage.to_pretty_json\n  end\n\n  private def execute(code : String, std_out : IO, std_err : IO, file : String, codegen : Bool, macro_code_coverage : Bool = false) : Process::Status\n    input = IO::Memory.new <<-CR\n      #{code}\n    CR\n\n    args = [] of String\n\n    if macro_code_coverage\n      args.push \"tool\", \"macro_code_coverage\"\n    else\n      args << \"run\"\n    end\n\n    args << \"--no-color\"\n    args.push \"--stdin-filename\", file\n    args << \"--no-codegen\" if !macro_code_coverage && !codegen\n\n    Process.run(ENV[\"CRYSTAL\"]? || \"crystal\", args, input: input.rewind, output: std_out, error: std_err)\n  end\n\n  # Runs the executable at the given *path*, optionally with the provided *args*.\n  #\n  # The standard output, error output, and status of the execution are yielded.\n  #\n  # ```\n  # require \"athena-spec\"\n  #\n  # ASPEC::Methods.run_executable \"/usr/bin/ls\" do |output, error, status|\n  #   output # => \"docs\\n\" + \"LICENSE\\n\" + \"README.md\\n\" + \"shard.yml\\n\" + \"spec\\n\" + \"src\\n\"\n  #   error  # => \"\"\n  #   status # => #<Process::Status:0x7f7bc9befb70 @exit_status=0>\n  # end\n  # ```\n  def run_executable(path : String, args : Array(String) = [] of String, & : String, String, Process::Status ->) : Nil\n    run_executable path, IO::Memory.new, args do |output_io, error_io, status|\n      yield output_io, error_io, status\n    end\n  end\n\n  # Runs the executable at the given *path*, with the given *input*, optionally with the provided *args*.\n  #\n  # The standard output, error output, and status of the execution are yielded.\n  #\n  # ```\n  # require \"athena-spec\"\n  #\n  # input = IO::Memory.new %({\"id\":1})\n  #\n  # ASPEC::Methods.run_executable \"jq\", input, [\".\", \"-c\"] do |output, error, status|\n  #   output # => \"{\\\"id\\\":1}\\n\"\n  #   error  # => \"\"\n  #   status # => #<Process::Status:0x7f26ec698b70 @exit_status=0>\n  # end\n  #\n  # invalid_input = IO::Memory.new %({\"id\"1})\n  #\n  # ASPEC::Methods.run_executable \"jq\", invalid_input, [\".\", \"-c\"] do |output, error, status|\n  #   output # => \"\"\n  #   error  # => \"parse error: Expected separator between values at line 1, column 7\\n\"\n  #   status # => #<Process::Status:0x7f0217496900 @exit_status=1024>\n  # end\n  # ```\n  def run_executable(path : String, input : IO, args : Array(String) = [] of String, & : String, String, Process::Status ->) : Nil\n    output_io = IO::Memory.new\n    error_io = IO::Memory.new\n    status = Process.run path, args, error: error_io, output: output_io, input: input\n    yield output_io.to_s, error_io.to_s, status\n  end\nend\n"
  },
  {
    "path": "src/components/spec/src/test_case.cr",
    "content": "# `ASPEC::TestCase` provides a [Spec](https://crystal-lang.org/api/Spec.html) compliant\n# alternative DSL for creating unit and integration tests.  It allows structuring tests\n# in a more OOP fashion, with the main benefits of reusability and extendability.\n#\n# This type can be extended to share common testing logic with groups of similar types.\n# Any tests defined within a parent will run for each child test case.\n# `abstract def`, `super`, and other OOP features can be used as well to reduce duplication.\n# Some additional features are also built in, such as the `DataProvider`.\n#\n# NOTE: This is _NOT_ a standalone testing framework.  Everything boils down to standard `describe`, `it`, and/or `pending` blocks.\n#\n# A test case consists of a `struct` inheriting from `self`, optionally with an `#initialize` method in order to\n# initialize the state that should be used for each test.\n#\n# A test is a method that starts with `test_`, where the method name is used as the description.\n# For example, `test_some_method_some_context` becomes `\"some method some context\"`.\n# Internally each test method maps to an `it` block.\n# All of the stdlib's `Spec` assertions methods are available, in addition to\n# [#pending!](https://crystal-lang.org/api/Spec/Methods.html#pending!%28msg=%22Cannotrunexample%22,file=__FILE__,line=__LINE__%29-instance-method) and\n# [#fail](https://crystal-lang.org/api/Spec/Methods.html#fail%28msg,file=__FILE__,line=__LINE__%29-instance-method).\n#\n# A method may be focused by either prefixing the method name with an `f`, or applying the `Focus` annotation.\n#\n# A method may be marked pending by either prefixing the method name with a `p`, or applying the `Pending` annotation.\n# Internally this maps to a `pending` block.\n#\n# Tags may be applied to a method via the `Tags` annotation.\n#\n# The `Tags`, `Focus`, and `Pending` annotations may also be applied to the test case type as well, with a similar affect.\n#\n# ### Example\n#\n# ```\n# # Require the stdlib's spec module.\n# require \"spec\"\n#\n# # Define a class to test.\n# class Calculator\n#   def add(v1, v2)\n#     v1 + v2\n#   end\n#\n#   def subtract(v1, v2)\n#     raise NotImplementedError.new \"TODO\"\n#   end\n# end\n#\n# # An example test case.\n# struct ExampleSpec < ASPEC::TestCase\n#   @target : Calculator\n#\n#   # Initialize the test target along with any dependencies.\n#   def initialize : Nil\n#     @target = Calculator.new\n#   end\n#\n#   # All of the stdlib's `Spec` methods can be used,\n#   # plus any custom methods defined in `ASPEC::Methods`.\n#   def test_add : Nil\n#     @target.add(1, 2).should eq 3\n#   end\n#\n#   # A pending test.\n#   def ptest_subtract : Nil\n#     @target.subtract(10, 5).should eq 5\n#   end\n#\n#   # Private/protected methods can be used to reduce duplication within the context of single test case.\n#   private def helper_method\n#     # ...\n#   end\n# end\n# ```\n#\n# ## Inheritance\n#\n# Inheritance can be used to build reusable test cases for groups of similar objects\n#\n# ```\n# abstract struct SomeTypeTestCase < ASPEC::TestCase\n#   # Require children to define a method to get the object.\n#   protected abstract def get_object : Calculator\n#\n#   # Test cases can use the abstract method for tests common to all test cases of this type.\n#   def test_common : Nil\n#     obj = self.get_object\n#\n#     # ...\n#   end\n# end\n#\n# struct CalculatorTest < SomeTypeTestCase\n#   protected def get_object : Calculator\n#     Calculator.new\n#   end\n#\n#   # Additional tests specific to this type.\n#   def test_specific : Nil\n#     # ...\n#   end\n# end\n# ```\n#\n# ## Data Providers\n#\n# A `DataProvider` can be used to reduce duplication, see the corresponding annotation or more information.\n#\n# ```\n# struct DataProviderTest < ASPEC::TestCase\n#   # Data Providers allow reusing a test's multiple times with different input.\n#   @[DataProvider(\"get_values\")]\n#   def test_squares(value : Int32, expected : Int32) : Nil\n#     (value ** 2).should eq expected\n#   end\n#\n#   # Returns a hash where the key represents the name of the test,\n#   # and the value is a Tuple of data that should be provided to the test.\n#   def get_values : Hash\n#     {\n#       \"two\"   => {2, 4},\n#       \"three\" => {3, 9},\n#     }\n#   end\n# end\n# ```\n#\n# ```\n# # Run all the test cases\n# ASPEC.run_all # =>\n# # ExampleSpec\n# #   add\n# #   subtract\n# #   a custom method name\n# # CalculatorTest\n# #   common\n# #   specific\n# # DataProviderTest\n# #   squares two\n# #   squares three\n# #\n# # Pending:\n# # ExampleSpec subtract\n# #\n# # Finished in 172 microseconds\n# # 7 examples, 0 failures, 0 errors, 1 pending\n# ```\n#\n# ### TestWith\n#\n# The `TestWith` annotation is similar to `DataProvider`, but can be a bit simpler to use if the data doesn't need shared between multiple test methods.\n#\n# ```\n# struct TestWithTest < ASPEC::TestCase\n#   @[TestWith(\n#     two: {2, 4},\n#     three: {3, 9},\n#     four: {4, 16},\n#     five: {5, 25},\n#   )]\n#   def test_squares(value : Int32, expected : Int32) : Nil\n#     (value ** 2).should eq expected\n#   end\n#\n#   @[TestWith(\n#     {2, 8},\n#     {3, 27},\n#     {4, 64},\n#     {5, 125},\n#   )]\n#   def test_cubes(value : Int32, expected : Int32) : Nil\n#     (value ** 3).should eq expected\n#   end\n# end\n# ```\nabstract struct Athena::Spec::TestCase\n  include Athena::Spec::Methods\n\n  # Defines the tags tied to a specific test case (describe block) or method (it block).\n  #\n  # Maps to [Tagging Specs](https://crystal-lang.org/reference/guides/testing.html#tagging-specs) in the stdlib.\n  annotation Tags; end\n\n  # Focuses a specific test case (describe block) or method (it block).\n  #\n  # Maps to [Focusing Specs](https://crystal-lang.org/reference/guides/testing.html#focusing-on-a-group-of-specs) in the stdlib.\n  annotation Focus; end\n\n  # Marks a specific test case (describe block) or method (it block) as `pending`.\n  #\n  # Maps to the stdlib's [#pending](https://crystal-lang.org/api/master/Spec/Methods.html#pending%28description=%22assert%22,file=__FILE__,line=__LINE__,end_line=__END_LINE__,focus:Bool=false,tags:String%7CEnumerable%28String%29%7CNil=nil,&%29-instance-method) method.\n  annotation Pending; end\n\n  # Can be applied to an `ASPEC::TestCase` type to denote it should be skipped when running tests via `ASPEC.run_all`.\n  # Useful for creating mock types, or to have more control over when it should be ran.\n  annotation Skip; end\n\n  # Tests can be defined with arbitrary arguments.  These arguments are provided by one or more `DataProvider`.\n  #\n  # A data provider is a method that returns either a `Hash`, `NamedTuple`, `Array`, or `Tuple`.\n  #\n  # NOTE: The method's return type must be set to one of those types.\n  #\n  # If the return type is a `Hash` or `NamedTuple` then it is a keyed provider;\n  # the key will be used as part of the description for each test.\n  #\n  # If the return type is an `Array` or `Tuple` it is considered a keyless provider;\n  # the index will be used as part of the description for each test.\n  #\n  # NOTE: In both cases the value must be a `Tuple`; the values should be an ordered list of the arguments you want to provide to the test.\n  #\n  # One or more `DataProvider` annotations can be applied to a test\n  # with a positional argument of the name of the providing methods.\n  # An `it` block will be defined for each \"set\" of data.\n  #\n  # Data providers can be a very powerful tool when combined with inheritance and `abstract def`s.\n  # A parent test case could define all the testing logic, and child implementations only provide the data.\n  #\n  # ### Example\n  #\n  # ```\n  # require \"athena-spec\"\n  #\n  # struct DataProviderTest < ASPEC::TestCase\n  #   @[DataProvider(\"get_values_hash\")]\n  #   @[DataProvider(\"get_values_named_tuple\")]\n  #   def test_squares(value : Int32, expected : Int32) : Nil\n  #     (value ** 2).should eq expected\n  #   end\n  #\n  #   # A keyed provider using a Hash.\n  #   def get_values_hash : Hash\n  #     {\n  #       \"two\"   => {2, 4},\n  #       \"three\" => {3, 9},\n  #     }\n  #   end\n  #\n  #   # A keyed provider using a NamedTuple.\n  #   def get_values_named_tuple : NamedTuple\n  #     {\n  #       four: {4, 16},\n  #       five: {5, 25},\n  #     }\n  #   end\n  #\n  #   @[DataProvider(\"get_values_array\")]\n  #   @[DataProvider(\"get_values_tuple\")]\n  #   def test_cubes(value : Int32, expected : Int32) : Nil\n  #     (value ** 3).should eq expected\n  #   end\n  #\n  #   # A keyless provider using an Array.\n  #   def get_values_array : Array\n  #     [\n  #       {2, 8},\n  #       {3, 27},\n  #     ]\n  #   end\n  #\n  #   # A keyless provider using a Tuple.\n  #   def get_values_tuple : Tuple\n  #     {\n  #       {4, 64},\n  #       {5, 125},\n  #     }\n  #   end\n  # end\n  #\n  # DataProviderTest.run # =>\n  # # DataProviderTest\n  # #   squares two\n  # #   squares three\n  # #   squares four\n  # #   squares five\n  # #   cubes 0\n  # #   cubes 1\n  # #   cubes 2\n  # #   cubes 3\n  # ```\n  annotation DataProvider; end\n\n  # Instead of created a dedicated methods for use with `DataProvider`, you can define a data set using the `TestWith` annotation.\n  # The annotations accepts a variadic amount of `Tuple` positional/named arguments and will create a `it` case for each \"set\" of data.\n  #\n  # ### Example\n  #\n  # ```\n  # require \"athena-spec\"\n  #\n  # struct TestWithTest < ASPEC::TestCase\n  #   @[TestWith(\n  #     two: {2, 4},\n  #     three: {3, 9},\n  #     four: {4, 16},\n  #     five: {5, 25},\n  #   )]\n  #   def test_squares(value : Int32, expected : Int32) : Nil\n  #     (value ** 2).should eq expected\n  #   end\n  #\n  #   @[TestWith(\n  #     {2, 8},\n  #     {3, 27},\n  #     {4, 64},\n  #     {5, 125},\n  #   )]\n  #   def test_cubes(value : Int32, expected : Int32) : Nil\n  #     (value ** 3).should eq expected\n  #   end\n  # end\n  #\n  # TestWithTest.run # =>\n  # # TestWithTest\n  # #   squares two\n  # #   squares three\n  # #   squares four\n  # #   squares five\n  # #   cubes 0\n  # #   cubes 1\n  # #   cubes 2\n  # #   cubes 3\n  # ```\n  annotation TestWith; end\n\n  # :nodoc:\n  def self.construct\n    instance = allocate\n    instance.initialize __init: nil\n    instance\n  end\n\n  # :nodoc:\n  def initialize(__init init : Nil)\n  end\n\n  macro inherited\n    macro finished\n      {% verbatim do %}\n        {%\n          @type.methods.select(&.name.==(\"initialize\")).each do |a_def|\n            if a_def.accepts_block? || a_def.args.size > 0\n              a_def.raise \"`ASPEC::TestCase` initializers must be argless and non-yielding.\"\n            end\n          end\n        %}\n      {% end %}\n    end\n  end\n\n  # Runs the tests contained within `self`.\n  #\n  # See `Athena::Spec.run_all` to run all test cases.\n  def self.run : Nil\n    instance = construct\n\n    {% begin %}\n      {{!!@type.annotation(Pending) ? \"pending\".id : \"describe\".id}} {{@type.name.stringify}}, focus: {{!!@type.annotation Focus}}{% if (tags = @type.annotation(Tags)) %}, tags: {{tags.args}}{% end %} do\n        before_all do\n          instance.before_all\n\n          # Run this here to validate the instance is valid before calling tear_down,\n          # which could possibly lead to segfaults if there was an exception raised during\n          # initialization of an object when assigning an ivar in initialize and some state of that object is interacted with.\n          instance.initialize\n        end\n\n        before_each do\n          instance.initialize\n        end\n\n        after_each do\n          instance.tear_down\n        end\n\n        after_all do\n          instance.after_all\n        end\n\n        {% methods = [] of Nil %}\n\n        {% for parent in @type.ancestors.select &.<(TestCase) %}\n          {% for method in parent.methods.select { |m| m.name =~ /^(?:f|p)?test_/ } %}\n            {% methods << method %}\n          {% end %}\n        {% end %}\n\n        {% for test in methods + @type.methods.select { |m| m.name =~ /^(?:f|p)?test_/ } %}\n          {% focus = test.name.starts_with?(\"ftest_\") || !!test.annotation Focus %}\n          {% tags = (tags = test.annotation(Tags)) ? tags.args : nil %}\n          {% method = (test.name.starts_with?(\"ptest_\") || !!test.annotation Pending) ? \"pending\" : \"it\" %}\n          {% description = test.name.stringify.gsub(/^(?:f|p)?test_/, \"\").underscore.gsub(/_/, \" \") %}\n\n          {% if test_with = test.annotation(TestWith) %}\n            # Treat args as Array/Tuple data providers\n            {% for args, idx in test_with.args %}\n              {% args.raise \"Expected argument ##{idx} of the 'ASPEC::TestCase::TestWith' annotation applied to '#{@type}##{test.name.id}' to be a Tuple, but got '#{args.class_name.id}'.\" unless args.is_a? TupleLiteral %}\n              {% args.raise \"Expected argument ##{idx} of the 'ASPEC::TestCase::TestWith' annotation applied to '#{@type}##{test.name.id}' to contain #{test.args.size} values, but got #{args.size}.\" if test.args.size != args.size %}\n\n              {{method.id}} \"#{{{description}}} #{{{idx}}}\", file: {{test.filename}}, line: {{test.line_number}}, end_line: {{test.end_line_number}}, focus: {{focus}}, tags: {{tags}} do\n                instance.{{test.name.id}} *{{args}}\n              end\n            {% end %}\n\n            # Treat named args as Hash/NamedTuple data providers\n            {% for name, args in test_with.named_args %}\n              {% args.raise \"Expected the value of argument '#{name.id}' of the 'ASPEC::TestCase::TestWith' annotation applied to '#{@type}##{test.name.id}' to be a Tuple, but got '#{args.class_name.id}'.\" unless args.is_a? TupleLiteral %}\n              {% args.raise \"Expected the value of argument '#{name.id}' of the 'ASPEC::TestCase::TestWith' annotation applied to '#{@type}##{test.name.id}' to contain #{test.args.size} values, but got #{args.size}.\" if test.args.size != args.size %}\n\n              {{method.id}} \"#{{{description}}} #{{{name.stringify}}}\", file: {{test.filename}}, line: {{test.line_number}}, end_line: {{test.end_line_number}}, focus: {{focus}}, tags: {{tags}} do\n                instance.{{test.name.id}} *{{args}}\n              end\n            {% end %}\n          {% elsif !test.annotations(DataProvider).empty? %}\n            {% for data_provider in test.annotations DataProvider %}\n              {% data_provider_method_name = data_provider[0] || data_provider.raise \"One or more data provider for test '#{@type}##{test.name.id}' is missing its name.\" %}\n              {% methods = @type.methods %}\n\n              {% for ancestor in @type.ancestors.select &.<=(ASPEC::TestCase) %}\n                {% methods += ancestor.methods %}\n              {% end %}\n\n              {% provider_method_return_type = (methods.find(&.name.stringify.==(data_provider_method_name)).return_type || raise \"Data provider '#{@type}##{data_provider_method_name.id}' must have a return type of Hash, NamedTuple, Array, or Tuple.\").resolve %}\n\n              {% if provider_method_return_type == Hash || provider_method_return_type == NamedTuple %}\n                instance.{{data_provider_method_name.id}}.each do |name, args|\n                  {{method.id}} \"#{{{description}}} #{name}\", file: {{test.filename}}, line: {{test.line_number}}, end_line: {{test.end_line_number}}, focus: {{focus}}, tags: {{tags}} do\n                    instance.{{test.name.id}} *args\n                  end\n                end\n              {% elsif provider_method_return_type == Array || provider_method_return_type == Tuple %}\n                instance.{{data_provider_method_name.id}}.each_with_index do |args, idx|\n                  {{method.id}} \"#{{{description}}} #{idx}\", file: {{test.filename}}, line: {{test.line_number}}, end_line: {{test.end_line_number}}, focus: {{focus}}, tags: {{tags}} do\n                    instance.{{test.name.id}} *args\n                  end\n                end\n              {% else %}\n                {% provider_method.raise \"Unsupported data provider return type: '#{provider_method.return_type}'\" %}\n              {% end %}\n            {% end %}\n          {% else %}\n            {{method.id}} {{description}}, file: {{test.filename}}, line: {{test.line_number}}, end_line: {{test.end_line_number}}, focus: {{focus}}, tags: {{tags}} do\n              instance.{{test.name.id}}\n            end\n          {% end %}\n        {% end %}\n      end\n    {% end %}\n  end\n\n  # Runs once before any tests within `self` have been executed.\n  #\n  # Can be used to initialize objects common to every test,\n  # but that do not need to be reset before running each test.\n  #\n  # ```\n  # require \"spec\"\n  # require \"athena-spec\"\n  #\n  # struct ExampleSpec < ASPEC::TestCase\n  #   def before_all : Nil\n  #     puts \"This prints only once before anything else\"\n  #   end\n  #\n  #   def test_one : Nil\n  #     true.should be_true\n  #   end\n  #\n  #   def test_two : Nil\n  #     1.should eq 1\n  #   end\n  # end\n  #\n  # ExampleSpec.run\n  # ```\n  def before_all : Nil\n  end\n\n  # Runs once after all tests within `self` have been executed.\n  #\n  # ```\n  # require \"spec\"\n  # require \"athena-spec\"\n  #\n  # struct ExampleSpec < ASPEC::TestCase\n  #   def after_all : Nil\n  #     puts \"This prints only once after anything else\"\n  #   end\n  #\n  #   def test_one : Nil\n  #     true.should be_true\n  #   end\n  #\n  #   def test_two : Nil\n  #     1.should eq 1\n  #   end\n  # end\n  #\n  # ExampleSpec.run\n  # ```\n  def after_all : Nil\n  end\n\n  # Runs before each test.\n  #\n  # Used to create the objects that will be used within the tests.\n  #\n  # ```\n  # require \"spec\"\n  # require \"athena-spec\"\n  #\n  # struct ExampleSpec < ASpec::TestCase\n  #   @value : Int32\n  #\n  #   def initialize : Nil\n  #     @value = 1\n  #   end\n  #\n  #   def test_one : Nil\n  #     @value += 1\n  #\n  #     @value # => 2\n  #   end\n  #\n  #   def test_two : Nil\n  #     @value # => 1\n  #   end\n  # end\n  #\n  # ExampleSpec.run\n  # ```\n  def initialize : Nil\n  end\n\n  # Runs after each test.\n  #\n  # Can be used to cleanup data in between tests, such as releasing a connection or closing a file.\n  #\n  # ```\n  # require \"spec\"\n  # require \"athena-spec\"\n  #\n  # struct ExampleSpec < ASPEC::TestCase\n  #   @file : File\n  #\n  #   def initialize : Nil\n  #     @file = File.new \"./foo.txt\", \"w\"\n  #   end\n  #\n  #   def tear_down : Nil\n  #     @file.close\n  #   end\n  #\n  #   def test_one : Nil\n  #     @file.path # => \"./foo.txt\"\n  #   end\n  # end\n  #\n  # ExampleSpec.run\n  # ```\n  def tear_down : Nil\n  end\n\n  # :showdoc:\n  #\n  # Helper macro DSL for defining a test method.\n  #\n  # ```\n  # require \"spec\"\n  # require \"athena-spec\"\n  #\n  # struct ExampleSpec < ASPEC::TestCase\n  #   test \"2 is even\" do\n  #     2.even?.should be_true\n  #   end\n  # end\n  #\n  # ExampleSpec.run\n  # ```\n  private macro test(name, focus = false, *tags)\n    {% if focus %}@[Focus]{% end %}\n    {% unless tags.empty? %}@[Tags({{tags.splat}})]{% end %}\n    def test_{{name.gsub(/[^\\w]/, \"_\").underscore.downcase.id}} : Nil\n      {{yield}}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/.editorconfig",
    "content": "root = true\n\n[*.cr]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "src/components/validator/.gitignore",
    "content": "/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in applications that use them\n/shard.lock\n"
  },
  {
    "path": "src/components/validator/CHANGELOG.md",
    "content": "# Changelog\n\n## [0.5.0] - 2026-04-19\n\n### Changed\n\n- **Breaking:** Split `AVD::Constraints::Size` into `Count` and `Length` constraints ([#611]) (George Dietrich) <!-- blacksmoke16 -->\n- Make identifying constraint violation inequality easier within spec failures ([#610]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Allow picking the unit used for `AVD::Constraints::Length` validations ([#612]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.5.0]: https://github.com/athena-framework/validator/releases/tag/v0.5.0\n[#610]: https://github.com/athena-framework/athena/pull/610\n[#611]: https://github.com/athena-framework/athena/pull/611\n[#612]: https://github.com/athena-framework/athena/pull/612\n\n## [0.4.1] - 2025-09-04\n\n### Changed\n\n- Leverage `mime` component for more robust `AVD::Constraints::File` MIME type validation ([#545]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Added\n\n- Add `AVD::Spec::CompoundConstraintTestCase` to make testing `AVD::Constraints::Compound` easier ([#540]) (George Dietrich) <!-- blacksmoke16 -->\n- Add support for `ATH::UploadedFile` to `AVD::Constraints::File` and `AVD::Constraints::Image` ([#559]) (George Dietrich) <!-- blacksmoke16 -->\n\n### Fixed\n\n- Fix equality between `AVD::Constraint` instances ([#540]) (George Dietrich) <!-- blacksmoke16 -->\n\n[0.4.1]: https://github.com/athena-framework/validator/releases/tag/v0.4.1\n[#545]: https://github.com/athena-framework/athena/pull/545\n[#540]: https://github.com/athena-framework/athena/pull/540\n[#559]: https://github.com/athena-framework/athena/pull/559\n\n## [0.4.0] - 2025-01-26\n\n### Changed\n\n- **Breaking:** Normalize exception types ([#428]) (George Dietrich)\n\n### Added\n\n- **Breaking:** Add and make `require_tld: true` the default for `AVD::Constraints::URL` ([#492]) (George Dietrich)\n- Add example usages to `AVD::Constraints::*` docs ([#483], [#493]) (Zohir Tamda, George Dietrich)\n\n[0.4.0]: https://github.com/athena-framework/validator/releases/tag/v0.4.0\n[#428]: https://github.com/athena-framework/athena/pull/428\n[#483]: https://github.com/athena-framework/athena/pull/483\n[#492]: https://github.com/athena-framework/athena/pull/492\n[#493]: https://github.com/athena-framework/athena/pull/493\n\n## [0.3.4] - 2024-07-31\n\n### Changed\n\n- Update minimum `crystal` version to `~> 1.13.0` ([#433]) (George Dietrich)\n\n[0.3.4]: https://github.com/athena-framework/validator/releases/tag/v0.3.4\n[#433]: https://github.com/athena-framework/athena/pull/433\n\n## [0.3.3] - 2024-04-09\n\n### Changed\n\n- Integrate website into monorepo ([#365]) (George Dietrich)\n\n[0.3.3]: https://github.com/athena-framework/validator/releases/tag/v0.3.3\n[#365]: https://github.com/athena-framework/athena/pull/365\n\n## [0.3.2] - 2023-10-09\n\n### Fixed\n\n- Fix compiler error when using a composite constraint with a single member and no `of AVD::Constraint` ([#292]) (George Dietrich)\n\n[0.3.2]: https://github.com/athena-framework/validator/releases/tag/v0.3.2\n[#292]: https://github.com/athena-framework/athena/pull/292\n\n## [0.3.1] - 2023-02-18\n\n### Changed\n\n- Update some links in preparation for Athena Framework `0.18.0` ([#261]) (George Dietrich)\n\n### Fixed\n\n- Fix issue when using `AVD::Metadata::GetterMetadata` with methods that have parameters ([#252]) (George Dietrich)\n\n[0.3.1]: https://github.com/athena-framework/validator/releases/tag/v0.3.1\n[#252]: https://github.com/athena-framework/athena/pull/252\n[#261]: https://github.com/athena-framework/athena/pull/261\n\n## [0.3.0] - 2023-01-07\n\n### Changed\n\n- **Breaking:** update default `AVD::Constraints::Email::Mode` to be `:html5` ([#230]) (George Dietrich)\n- Refactor `AVD::Constraints::IP` to use new dedicated `Socket::IPAddress` methods ([#205]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.6` ([#205]) (George Dietrich)\n\n### Added\n\n- Add `AVD::Constraints::Collection` ([#229]) (George Dietrich)\n- Add `AVD::Constraints::Existence`, `AVD::Constraints::Required`, and `AVD::Constraints::Optional` for use with the collection constraint ([#229]) (George Dietrich)\n- Add `AVD::Spec::ConstraintValidatorTestCase#expect_validate_value_at` to more easily handle validation of nested constraints ([#229]) (George Dietrich)\n- Add `AVD::Constraints::Email::Mode::HTML5_ALLOW_NO_TLD` that allows matching `HTML` input field validation exactly ([#231]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** remove `AVD::Constraints::Email::Mode::Loose` ([#230]) (George Dietrich)\n\n### Fixed\n\n- **Breaking:** fix spelling of `AVD::Constraints::ISSN#require_hyphen` parameter ([#222]) (George Dietrich)\n- Fix property path display issue with `Enumerable` objects ([#229]) (George Dietrich)\n- Fix `AVD::Constraints::Valid` constraints incorrectly being allowed within `AVD::Constraints::Composite` ([#229]) (George Dietrich)\n\n[0.3.0]: https://github.com/athena-framework/validator/releases/tag/v0.3.0\n[#205]: https://github.com/athena-framework/athena/pull/205\n[#222]: https://github.com/athena-framework/athena/pull/222\n[#229]: https://github.com/athena-framework/athena/pull/229\n[#230]: https://github.com/athena-framework/athena/pull/230\n[#231]: https://github.com/athena-framework/athena/pull/231\n\n## [0.2.1] - 2022-09-05\n\n### Added\n\n- Add support for exclusive end support to `AVD::Constraints::Range` ([#184]) (George Dietrich)\n\n### Changed\n\n- Include allowed MIME types within `AVD::Constraints::Image` if they were customized ([#183]) (George Dietrich)\n- **Breaking:** ensure parameter names defined on interfaces match the implementation ([#188]) (George Dietrich)\n\n### Fixed\n\n- Fix some file size factorization edge cases in `AVD::Constraints::File` ([#182]) (George Dietrich)\n- Fix duplicating constraints due to Crystal generics bug ([#192]) (George Dietrich)\n\n[0.2.1]: https://github.com/athena-framework/validator/releases/tag/v0.2.1\n[#182]: https://github.com/athena-framework/athena/pull/182\n[#183]: https://github.com/athena-framework/athena/pull/183\n[#184]: https://github.com/athena-framework/athena/pull/184\n[#188]: https://github.com/athena-framework/athena/pull/188\n[#192]: https://github.com/athena-framework/athena/pull/192\n\n## [0.2.0] - 2022-05-14\n\n### Added\n\n- Add the [AVD::Constraints::File](https://athenaframework.org/Validator/Constraints/File/) constraint ([#153]) (George Dietrich)\n- Allow `AVD::Spec::MockValidator` to dynamically configure returned violations ([#155], [#157]) (George Dietrich)\n- Add the [AVD::Constraints::Image](https://athenaframework.org/Validator/Constraints/Image/) constraint ([#153]) (George Dietrich)\n- Add getting started documentation to API docs ([#172]) (George Dietrich)\n\n### Changed\n\n- **Breaking:** make `AVD::ConstraintValidator` classes ([#154]) (George Dietrich)\n- **Breaking:** `AVD::ExecutionContext` is no longer a generic type ([#156]) (George Dietrich)\n- Update `assert_violation` to use a clearer failure message if no violations were found ([#153]) (George Dietrich)\n- Update `AVD::Constraints::ISIN` to use the validator off the context versus an ivar ([#155]) (George Dietrich)\n- Update minimum `crystal` version to `~> 1.4.0` ([#169]) (George Dietrich)\n\n### Removed\n\n- **Breaking:** removed `AVD::Spec::MockValidator#violations=` ([#155]) (George Dietrich)\n\n### Fixed\n\n- Fix `AVD::Violation::ConstraintViolation` not comparing correctly ([#153]) (George Dietrich)\n- Ensure only `Indexable` types can be used with `AVD::Constraints::Unique` ([#168]) (George Dietrich)\n\n[0.2.0]: https://github.com/athena-framework/validator/releases/tag/v0.2.0\n[#153]: https://github.com/athena-framework/athena/pull/153\n[#154]: https://github.com/athena-framework/athena/pull/154\n[#155]: https://github.com/athena-framework/athena/pull/155\n[#156]: https://github.com/athena-framework/athena/pull/156\n[#157]: https://github.com/athena-framework/athena/pull/157\n[#168]: https://github.com/athena-framework/athena/pull/168\n[#169]: https://github.com/athena-framework/athena/pull/169\n[#172]: https://github.com/athena-framework/athena/pull/172\n\n## [0.1.7] - 2021-12-27\n\n_First release a part of the monorepo._\n\n### Fixed\n\n- Fix callback constraint methods being incorrectly added as getters ([#132]) (George Dietrich)\n\n[0.1.7]: https://github.com/athena-framework/validator/releases/tag/v0.1.7\n[#132]: https://github.com/athena-framework/athena/pull/132\n\n## [0.1.6] - 2021-12-13\n\n### Fixed\n\n- Fix `AVD::Validatable` not working when included into parent types ([#16]) (George Dietrich)\n\n[0.1.6]: https://github.com/athena-framework/validator/releases/tag/v0.1.6\n[#16]: https://github.com/athena-framework/validator/pull/16\n\n## [0.1.5] - 2021-10-30\n\n### Added\n\n- Add `VERSION` constant to `Athena::Validator` namespace ([#12]) (George Dietrich)\n\n### Fixed\n\n- Fix incorrect type restriction on validator factory ([#12]) (George Dietrich)\n- Fix incorrect link within the docs ([#14]) (George Dietrich)\n\n[0.1.5]: https://github.com/athena-framework/validator/releases/tag/v0.1.5\n[#12]: https://github.com/athena-framework/validator/pull/12\n[#14]: https://github.com/athena-framework/validator/pull/14\n\n## [0.1.4] - 2021-01-30\n\n### Changed\n\n- Finish migration to [MkDocs](https://mkdocstrings.github.io/crystal/) ([#10], [#11]) (George Dietrich)\n\n[0.1.4]: https://github.com/athena-framework/validator/releases/tag/v0.1.4\n[#10]: https://github.com/athena-framework/validator/pull/10\n[#11]: https://github.com/athena-framework/validator/pull/11\n\n## [0.1.3] - 2020-12-07\n\n### Changed\n\n- Update `crystal` version to allow version greater than `1.0.0` ([#9]) (George Dietrich)\n\n[0.1.3]: https://github.com/athena-framework/validator/releases/tag/v0.1.3\n[#9]: https://github.com/athena-framework/validator/pull/9\n\n## [0.1.2] - 2020-11-25\n\n### Added\n\n- Add the [AVD::Constraints::Choice](https://athenaframework.org/Validator/Constraints/Choice/) constraint ([#7]) (George Dietrich)\n\n### Changed\n\n- Allow setting violations directly on mock validators ([#7]) (George Dietrich)\n\n[0.1.2]: https://github.com/athena-framework/validator/releases/tag/v0.1.2\n[#7]: https://github.com/athena-framework/validator/pull/7\n\n## [0.1.1] - 2020-11-08\n\n### Fixed\n\n- Fix compiler error due to less strict `abstract def` implementations ([#6]) (George Dietrich)\n\n[0.1.1]: https://github.com/athena-framework/validator/releases/tag/v0.1.1\n[#6]: https://github.com/athena-framework/validator/pull/6\n\n## [0.1.0] - 2020-10-17\n\n_Initial release._\n\n[0.1.0]: https://github.com/athena-framework/validator/releases/tag/v0.1.0\n\n"
  },
  {
    "path": "src/components/validator/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.\n"
  },
  {
    "path": "src/components/validator/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 George Dietrich\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": "src/components/validator/README.md",
    "content": "# Validator\n\n[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)\n[![CI](https://github.com/athena-framework/athena/actions/workflows/ci.yml/badge.svg?branch=master&event=schedule)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/release/athena-framework/validator.svg)](https://github.com/athena-framework/validator/releases)\n\nObject/value validation library\n\n## Getting Started\n\nCheckout the [Documentation](https://athenaframework.org/Validator).\n\n## Contributing\n\nRead the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.\n"
  },
  {
    "path": "src/components/validator/UPGRADING.md",
    "content": "# Upgrading\n\nDocuments the changes that may be required when upgrading to a newer component version.\n\n## Upgrade to 0.5.0\n\n### Split `AVD::Constraints::Size` constraint\n\n`AVD:Constraints::Size` handled both `Indexable` and `String` values.\nIt has been split into dedicated `Count` and `Length` constraints respectively.\nUsages of `Size` should be updated to use one of the new constraints depending on if the value being validated is a string or a collection.\nThe new constraints have new error names/UUIDs and potentially different error placeholders.\n\n## Upgrade to 0.4.0\n\n### New `AVD::Constraints::URL#require_tld` option\n\n`AVD::Constraints::URL` now requires URLs have a TLD by default; `https://example.com` is valid while `https://example` is not.\nIf your logic requires the latter to be considered valid, you will need to ensure `require_tld` is set to `false` on usages of this constraint.\n\n### Normalization of Exception types\n\nThe namespace exception types live in has changed from `AVD::Exceptions` to `AVD::Exception`.\nAny usages of `validator` exception types will need to be updated.\n\nSome additional types have also been removed/renamed:\n\n* `AVD::Exceptions::ValidatorError` has been removed in favor of using `AVD::Exception` directly\n\nIf using a `rescue` statement with a parent exception type, either from the `validator` component or Crystal stdlib, double check it to ensure it'll still rescue what you are expecting it will.\n"
  },
  {
    "path": "src/components/validator/docs/README.md",
    "content": "The `Athena::Validator` component provides a robust object/value validation framework.\n\n* [AVD::Constraint](/Validator/Constraint/)s describe some assertion; such as a string should be [AVD::Constraints::NotBlank](/Validator/Constraints/NotBlank/)\nor that a value is [AVD::Constraints::GreaterThanOrEqual](/Validator/Constraints/GreaterThanOrEqual/) another value\n* Constraints, along with a value, are then passed to an [AVD::ConstraintValidatorInterface](/Validator/ConstraintValidatorInterface/) that actually performs the validation, using the data defined in the constraint\n* If the validator determines that the value is invalid in some way, it creates and adds an [AVD::Violation::ConstraintViolationInterface](/Validator/Violation/ConstraintViolationInterface/) to this runs' [AVD::ExecutionContextInterface](/Validator/ExecutionContextInterface/)\n* The [AVD::Validator::ValidatorInterface](/Validator/Validator/ValidatorInterface/) then returns an [AVD::Violation::ConstraintViolationListInterface](/Validator/Violation/ConstraintViolationListInterface/) that contains all the violations\n  * The object/value can be considered valid if that list is empty\n\n## Installation\n\nFirst, install the component by adding the following to your `shard.yml`, then running `shards install`:\n\n```yaml\ndependencies:\n  athena-validator:\n    github: athena-framework/validator\n    version: ~> 0.5.0\n```\n\n## Usage\n\n`Athena::Validator` comes with a set of common [AVD::Constraints](/Validator/Constraints/) built-in that any project could find useful.\nWhen used on its own, the [Athena::Validator.validator](/Validator/top_level/#Athena::Validator.validator) method can be used to obtain an [AVD::Validator::ValidatorInterface](/Validator/Validator/ValidatorInterface/) instance to validate a given value/object.\n\n### Basics\n\nA validator accepts a value, and one or more [AVD::Constraint](/Validator/Constraint/) to validate the value against.\nThe validator then returns an [AVD::Violation::ConstraintViolationListInterface](/Validator/Violation/ConstraintViolationListInterface/) that includes all the violations, if any.\n\n```crystal\n# Obtain a validator instance.\nvalidator = AVD.validator\n\n# Use the validator to validate a value.\nviolations = validator.validate \"foo\", AVD::Constraints::NotBlank.new\n\n# The validator returns an empty list of violations, indicating the value is valid.\nviolations.inspect # => Athena::Validator::Violation::ConstraintViolationList(@violations=[])\n```\n\nIn this case it returns an empty list of violations, meaning the value is valid.\n\n```crystal\n# Using the validator instance from the previous example\nviolations = validator.validate \"\", AVD::Constraints::NotBlank.new\n\nviolations.inspect # =>\n# Athena::Validator::Violation::ConstraintViolationList(\n#   @violations=[\n#     Athena::Validator::Violation::ConstraintViolation(\n#       @cause=nil,\n#       @code=\"0d0c3254-3642-4cb0-9882-46ee5918e6e3\",\n#       @constraint=#<Athena::Validator::Constraints::NotBlank:0x7f8a7291fed0\n#         @allow_nil=false,\n#         @groups=[\"default\"],\n#         @message=\"This value should not be blank.\",\n#         @payload=nil>,\n#       @invalid_value_container=Athena::Validator::ValueContainer(String)(@value=\"\"),\n#       @message=\"This value should not be blank.\",\n#       @message_template=\"This value should not be blank.\",\n#       @parameters={\"{{ value }}\" => \"\"},\n#       @plural=nil,\n#       @property_path=\"\",\n#       @root_container=Athena::Validator::ValueContainer(String)(@value=\"\")\n#     )\n#   ]\n)\n\n# Both the ConstraintViolationList and ConstraintViolation implement a `#to_s` method.\nputs violations # =>\n# :\n#   This value should not be blank. (code: 0d0c3254-3642-4cb0-9882-46ee5918e6e3)\n```\n\nHowever in the case of the value _NOT_ being valid, the list includes all of the [AVD::Violation::ConstraintViolationInterface](/Validator/Violation/ConstraintViolationInterface/)s produced during this run.\nEach violation includes some metadata; such as the related constraint that failed, a machine readable code, a human readable message, any parameters\nthat should be used to render that message, etc.  The extra context allows for a lot of flexibility; both in terms of how the error could be rendered or handled.\n\nBy default, in addition to any constraint specific arguments, the majority of the constraints have three optional arguments: `message`, `groups`, and `payload`.\n\n* The `message` argument represents the message that should be used if the value is found to not be valid.\nThe message can also include placeholders, in the form of `{{ key }}`, that will be replaced when the message is rendered.\nMost commonly this includes the invalid value itself, but some constraints have additional placeholders.\n* The `payload` argument can be used to attach any domain specific data to the constraint; such as attaching a severity with each constraint\nto have more serious violations be handled differently.\n* The `groups` argument can be used to run a subset of the defined constraints.  More on this in the [Validation Groups](#validation-groups) section.\n\n```crystal\nvalidator = AVD.validator\n\n# Instantiate a constraint with a custom message, using a placeholder.\nviolations = validator.validate -4, AVD::Constraints::PositiveOrZero.new message: \"{{ value }} is not a valid age.  A user cannot have a negative age.\"\n\nputs violations # =>\n# -4:\n#   -4 is not a valid age.  A user cannot have a negative age. (code: e09e52d0-b549-4ba1-8b4e-420aad76f0de)\n```\nCustomizing the message can be a good way for those consuming the errors to determine _WHY_ a given value is not valid.\n\n### Validating Objects\n\nValidating arbitrary values against a set of arbitrary constraints can be useful in smaller applications and/or for one off use cases.\nHowever to keep in line with our Object Oriented Programming (OOP) principles, we can also validate objects.  The object could be either a struct or a class.\nThe only requirements are that the object includes a specific module, [AVD::Validatable](/Validator/Validatable/), and specifies which properties should be validated and against what constraints.\nThe easiest/most common way to do this is via annotations and the [Assert](/Validator/aliases/#Assert) alias.\n\n```crystal\n# Define a class that can be validated.\nclass User\n  include AVD::Validatable\n\n  def initialize(@name : String, @age : Int32? = nil); end\n\n  # Specify that we want to assert that the user's name is not blank.\n  # Multiple constraints can be defined on a single property.\n  @[Assert::NotBlank]\n  getter name : String\n\n  # Arguments to the constraint can be used normally as well.\n  # The constraint's default argument can also be supplied positionally: `@[Assert::GreaterThan(0)]`.\n  @[Assert::NotNil(message: \"A user's age cannot be null\")]\n  getter age : Int32?\nend\n\n# Obtain a validator instance.\nvalidator = AVD.validator\n\n# Validate a user instance, notice we're not passing in any constraints.\nvalidator.validate(User.new(\"Jim\", 10)).empty? # => true\nvalidator.validate User.new \"\", 10             # =>\n# Object(User).name:\n#   This value should not be blank. (code: 0d0c3254-3642-4cb0-9882-46ee5918e6e3)\n```\n\nNotice that in this case we do not need to supply the constraints to the `#validate` method.\nThis is because the validator is able to extract them from the annotations on the properties.\nAn array of constraints can still be supplied, and will take precedence over the constraints defined within the type.\n\nNOTE: By default if a property's value is another object, the sub object will not be validated.\nuse the [AVD::Constraints::Valid](/Validator/Constraints/Valid/) constraint if you wish to also validate the sub object.\nThis also applies to arrays of objects.\n\nAnother important thing to point out is that no custom DSL is required to define these constraints.\n[Athena::Validator](/Validator/top_level/) is intended to be a generic validation solution that could be used outside of the [Athena](https://github.com/athena-framework) ecosystem.\nHowever, in order to be able to use the annotation based approach, you need to be able to apply the annotations to the underlying properties.\nIf this is not possible due to how a specific type is implemented, or if you just don't like the annotation syntax, the type can also be configured via code.\n\n```crystal\n# Define a class that can be validated.\nclass User\n  include AVD::Validatable\n\n  # This class method is invoked when building the metadata associated with a type,\n  # and can be used to manually wire up the constraints.\n  def self.load_metadata(metadata : AVD::Metadata::ClassMetadata) : Nil\n    metadata.add_property_constraint \"name\", AVD::Constraints::NotBlank.new\n  end\n\n  def initialize(@name : String); end\n\n  getter name : String\nend\n\n# Obtain a validator instance.\nvalidator = AVD.validator\n\n# Validate a user instance, notice we're not passing in any constraints.\nvalidator.validate(User.new(\"Jim\")).empty? # => true\nvalidator.validate User.new \"\"             # =>\n# Object(User).name:\n#   This value should not be blank. (code: 0d0c3254-3642-4cb0-9882-46ee5918e6e3)\n```\n\nThe metadata for each type is lazily loaded when an instance of that type is validated, and is only built once.\nSee [AVD::Metadata::ClassMetadata](/Validator/Metadata/ClassMetadata/) for some additional ways to register property constraints.\n\n#### Getters\n\nConstraints can also be applied to getter methods of an object.\nThis allows for dynamic validations based on the return value of the method.\nFor example, say we wanted to assert that a user's name is not the same as their password.\n\n```crystal\nclass User\n  include AVD::Validatable\n\n  property name : String\n  property password : String\n\n  def initialize(@name : String, @password : String); end\n\n  @[Assert::IsTrue(message: \"Your password cannot be the same as your name.\")]\n  def is_safe_password? : Bool\n    @name != @password\n  end\nend\n\nvalidator = AVD.validator\n\nuser = User.new \"foo\", \"foo\"\n\nvalidator.validate(user).empty? # => false\n\nuser.password = \"bar\"\n\nvalidator.validate(user).empty? # => true\n```\n\n### Custom Constraints\n\nIf the built in [AVD::Constraints](/Validator/Constraints/) are not sufficient to handle validating a given value/object; custom ones can be defined.\nLet's make a new constraint that asserts a string contains only alphanumeric characters.\n\nThis is accomplished by first defining a new class within the [AVD::Constraints](/Validator/Constraints/) namespace that inherits from [AVD::Constraint](/Validator/Constraint/).\nThen define a `Validator` struct within our constraint that inherits from [AVD::ConstraintValidator](/Validator/ConstraintValidator/) that actually implements the validation logic.\n\n```crystal\nclass AVD::Constraints::AlphaNumeric < AVD::Constraint\n  # (Optional) A unique error code can also be defined to provide a machine readable identifier for a specific error.\n  NOT_ALPHANUMERIC_ERROR = \"1a83a8bd-ff79-4d5c-96e7-86d0b25b8a09\"\n\n  # (Optional) Allows using the `.error_message(code : String) : String` method with this constraint.\n  @@error_names = {\n    NOT_ALPHANUMERIC_ERROR => \"NOT_ALPHANUMERIC_ERROR\",\n  }\n\n  # Define an initializer with our default message, and any additional arguments specific to this constraint.\n  def initialize(\n    message : String = \"This value should contain only alphanumeric characters.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil\n  )\n    super message, groups, payload\n  end\n\n  # Define the validator within our constraint that'll contain our validation logic.\n  class Validator < AVD::ConstraintValidator\n    # Define our validate method that accepts the value to be validated, and the constraint.\n    #\n    # Overloads can be used to filter values of specific types.\n    def validate(value : _, constraint : AVD::Constraints::AlphaNumeric) : Nil\n      # Custom constraints should ignore nil and empty values to allow\n      # other constraints (NotBlank, NotNil, etc.) take care of that\n      return if value.nil? || value == \"\"\n\n      # We'll cast the value to a string,\n      # alternatively we could just ignore non `String?` values.\n      value = value.to_s\n\n      # If all the characters of this string are alphanumeric, then it is valid\n      return if value.each_char.all? &.alphanumeric?\n\n      # Otherwise, it is invalid and we need to add a violation,\n      # see `AVD::ExecutionContextInterface` for additional information.\n      self.context.add_violation constraint.message, NOT_ALPHANUMERIC_ERROR, value\n    end\n  end\nend\n\nputs AVD.validator.validate \"$\", AVD::Constraints::AlphaNumeric.new # =>\n# $:\n#   This value should contain only alphanumeric characters. (code: 1a83a8bd-ff79-4d5c-96e7-86d0b25b8a09)\n```\n\nNOTE: The constraint _MUST_ be defined within the [AVD::Constraints](/Validator/Constraints/) namespace for implementation reasons.  This may change in the future.\n\nWe are now able to use this constraint as we would one of the built in ones;\neither by manually instantiating it, or applying an `@[Assert::AlphaNumeric]` annotation to a property.\n\nSee [AVD::ConstraintValidatorInterface](/Validator/ConstraintValidatorInterface/) for more information on custom validators.\n\n### Validation Groups\n\nBy default when validating an object, all constraints defined on that type will be checked.\nHowever, in some cases you may only want to validate the object against _some_ of those constraints.\nThis can be accomplished via assigning each constraint to a validation group, then apply validation against one specific group of constraints.\n\nFor example, using our `User` class from earlier, say we only want to validate certain properties when the user is first created.\nTo do this we can utilize the `groups` argument that all constraints have.\n\n```crystal\nclass User\n  include AVD::Validatable\n\n  def initialize(@email : String, @password : String, @city : String); end\n\n  @[Assert::Email(groups: \"create\")]\n  getter email : String\n\n  @[Assert::NotBlank(groups: \"create\")]\n  @[Assert::Size(7.., groups: \"create\")]\n  getter password : String\n\n  @[Assert::Size(2..)]\n  getter city : String\nend\n\nuser = User.new \"contact@athenaframework.org\", \"monkey123\", \"\"\n\n# Validate the user object, but only for those in the \"create\" group,\n# if no groups are supplied, then all constraints in the \"default\" group will be used.\nviolations = AVD.validator.validate user, groups: \"create\"\n\n# There are no violations since the city's size is not validated since it's not in the \"create\" group.\nviolations.empty? # => true\n```\n\nSee `AVD::Constraint@validation-groups` for some expanded information.\n\n### Sequential Validation\n\nBy default, all constraints are validated in a single \"batch\".  I.e. all constraints within the provided group(s) are validated, without regard\nto if the previous/next constraint is/was (in)valid.  However, an [AVD::Constraints::GroupSequence](/Validator/Constraints/GroupSequence/) can be used to validate batches of constraints in steps.\nI.e. validate the first \"batch\" of constraints, and only advance to the next batch if all constraints in that step are valid.\n\n```crystal\n@[Assert::GroupSequence(\"User\", \"Secondary\")]\nclass User\n  include AVD::Validatable\n\n  @[Assert::NotBlank]\n  getter username : String\n\n  @[Assert::NotBlank(groups: \"Secondary\")]\n  getter password : String\n\n  def initialize(@username : String, @password : String); end\nend\n\n# Instantiate a new `User` object where both properties are invalid.\nuser = User.new \"\", \"\"\n\n# Notice there is only one violation since there was a violation in the `User` group,\n# it did not advance to the `Secondary` group.\nAVD.validator.validate user # =>\n# Object(User).username:\n#   This value should not be blank. (code: 0d0c3254-3642-4cb0-9882-46ee5918e6e3)\n```\n\n#### Group Sequence Providers\n\nThe [AVD::Constraints::GroupSequence](/Validator/Constraints/GroupSequence/) can be a useful tool for creating efficient validations, but it is quite limiting since the sequence is static on the type.\nIf more flexibility is required the [AVD::Constraints::GroupSequence::Provider](/Validator/Constraints/GroupSequence/Provider/) module can be included into a type.\nThe module allows the object to return the sequence it should use dynamically at runtime.\n\n```crystal\nclass User\n  include AVD::Validatable\n  include AVD::Constraints::GroupSequence::Provider\n\n  # ...\n\n  def group_sequence : Array(Array(String) | String) | AVD::Constraints::GroupSequence\n    # Build out and return the sequence `self` should use.\n  end\nend\n```\n\nAlternatively, if you only want to apply constraints sequentially on a single property,\nthe [AVD::Constraints::Sequentially](/Validator/Constraints/Sequentially/) constraint can be used to do this in a simpler way.\n"
  },
  {
    "path": "src/components/validator/mkdocs.yml",
    "content": "INHERIT: ../../../mkdocs-common.yml\n\nsite_name: Validator\nsite_url: https://athenaframework.org/Validator/\nrepo_url: https://github.com/athena-framework/validator\n\nnav:\n  - Introduction: README.md\n  - Back to Manual: project://.\n  - API:\n      - Aliases: aliases.md\n      - Top Level: top_level.md\n      - '*'\n\nplugins:\n  - search\n  - section-index\n  - literate-nav\n  - gen-files:\n      scripts:\n        - ../../../gen_doc_stubs.py\n  - mkdocstrings:\n      default_handler: crystal\n      custom_templates: ../../../docs/templates\n      handlers:\n        crystal:\n          crystal_docs_flags:\n            - ../../../docs/index.cr\n            - ./lib/athena-http/src/athena-http.cr\n            - ./lib/athena-image_size/src/athena-image_size.cr\n            - ./lib/athena-mime/src/athena-mime.cr\n            - ./lib/athena-validator/src/athena-validator.cr\n            - ./lib/athena-validator/src/spec.cr\n          source_locations:\n            lib/athena-validator: https://github.com/athena-framework/validator/blob/v{shard_version}/{file}#L{line}\n"
  },
  {
    "path": "src/components/validator/shard.yml",
    "content": "name: athena-validator\n\nversion: 0.5.0\n\ncrystal: ~> 1.13\n\nlicense: MIT\n\nrepository: https://github.com/athena-framework/validator\n\ndocumentation: https://athenaframework.org/Validator\n\ndescription: |\n  Object/value validation library.\n\nauthors:\n  - George Dietrich <dev@dietrich.pub>\n\ndependencies:\n  athena-http:\n    github: athena-framework/http\n    version: ~> 0.1.0\n  athena-image_size:\n    github: athena-framework/image-size\n    version: ~> 0.1.0\n  athena-mime:\n    github: athena-framework/mime\n    version: ~> 0.2.0\n"
  },
  {
    "path": "src/components/validator/spec/athena-validator_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe Athena::Validator do\n  describe \".validator\" do\n    it \"returns a validator\" do\n      AVD.validator.should be_a AVD::Validator::ValidatorInterface\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraint_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe AVD::Constraint do\n  describe \".error_name\" do\n    it \"exists\" do\n      CustomConstraint.error_name(\"abc123\").should eq \"FAKE_ERROR\"\n    end\n\n    it \"does not add non _ERROR constants\" do\n      expect_raises(AVD::Exception::InvalidArgument, \"The error code 'BLAH' does not exist for constraint of type 'CustomConstraint'.\") do\n        CustomConstraint.error_name \"BLAH\"\n      end\n    end\n\n    it \"does not exist\" do\n      expect_raises(AVD::Exception::InvalidArgument, \"The error code 'foo' does not exist for constraint of type 'CustomConstraint'.\") do\n        CustomConstraint.error_name \"foo\"\n      end\n    end\n  end\n\n  describe \"#add_implicit_group\" do\n    it \"adds group when only group is default\" do\n      constraint = MockConstraint.new \"\"\n      constraint.groups.should eq [\"default\"]\n      constraint.add_implicit_group \"foo\"\n      constraint.groups.should eq [\"default\", \"foo\"]\n    end\n\n    it \"does not add when it's already included\" do\n      constraint = MockConstraint.new \"\"\n      constraint.groups.should eq [\"default\"]\n      constraint.add_implicit_group \"foo\"\n      constraint.groups.should eq [\"default\", \"foo\"]\n      constraint.add_implicit_group \"foo\"\n      constraint.groups.should eq [\"default\", \"foo\"]\n    end\n\n    it \"does not add when there are more than the default group\" do\n      constraint = MockConstraint.new \"\", groups: [\"custom_group\"]\n      constraint.groups.should eq [\"custom_group\"]\n      constraint.add_implicit_group \"foo\"\n      constraint.groups.should eq [\"custom_group\"]\n    end\n  end\n\n  describe \"#initialize\" do\n    it \"allows setting custom values\" do\n      constraint = CustomConstraint.new(\"MESSAGE\", [\"GROUP\"], {\"key\" => \"value\"})\n      constraint.message.should eq \"MESSAGE\"\n      constraint.groups.should eq [\"GROUP\"]\n      constraint.payload.should eq({\"key\" => \"value\"})\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/all_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::All\n\nstruct AllValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint constraints: AVD::Constraints::NotBlank.new\n    self.assert_no_violation\n  end\n\n  def test_raises_if_value_is_not_hash_or_indexable : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Expected argument of type 'Hash | Indexable', 'String' given.\" do\n      self.validator.validate \"FOO\", self.new_constraint constraints: AVD::Constraints::NotBlank.new\n    end\n  end\n\n  def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/at_least_one_of_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::AtLeastOneOf\n\nstruct AtLeastOneOfValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def valid_combinations : Tuple\n    {\n      {\"athena\", [AVD::Constraints::Length.new(range: (10..)), AVD::Constraints::EqualTo.new(value: \"athena\")]},\n      {150, [AVD::Constraints::Range.new(range: (10..20)), AVD::Constraints::GreaterThanOrEqual.new(value: 100)]},\n      {[1, 3, 5], [AVD::Constraints::Count.new(range: (5..)), AVD::Constraints::Unique.new]},\n    }\n  end\n\n  @[DataProvider(\"valid_combinations\")]\n  def test_valid_combinations(value : _, constraints : Array(AVD::Constraint)) : Nil\n    constraints.each_with_index do |constraint, idx|\n      self.expect_violation_at idx, value, constraint\n    end\n\n    self.validator.validate value, self.new_constraint constraints: constraints\n    self.assert_no_violation\n  end\n\n  def invalid_combinations : Tuple\n    {\n      {\"athenaa\", [AVD::Constraints::Length.new(range: (10..)), AVD::Constraints::EqualTo.new(value: \"athena\")]},\n      {50, [AVD::Constraints::Range.new(range: (10..20)), AVD::Constraints::GreaterThanOrEqual.new(value: 100)]},\n      {[1, 3, 3], [AVD::Constraints::Count.new(range: (5..)), AVD::Constraints::Unique.new]},\n    }\n  end\n\n  @[DataProvider(\"invalid_combinations\")]\n  def test_invalid_combinations_default_message(value : _, constraints : Array(AVD::Constraint)) : Nil\n    constraint = self.new_constraint constraints: constraints\n\n    message = [constraint.message]\n\n    constraints.each_with_index do |c, idx|\n      message << \" [#{idx + 1}] #{self.expect_violation_at(idx, value, c).first.message}\"\n    end\n\n    self.validator.validate value, constraint\n\n    self\n      .build_violation(message.join, CONSTRAINT::AT_LEAST_ONE_OF_ERROR)\n      .assert_violation\n  end\n\n  @[DataProvider(\"invalid_combinations\")]\n  def test_invalid_combinations_custom_message(value : _, constraints : Array(AVD::Constraint)) : Nil\n    constraints.each_with_index do |constraint, idx|\n      self.expect_violation_at idx, value, constraint\n    end\n\n    self.validator.validate value, self.new_constraint constraints: constraints, message: \"my_message\", include_internal_messages: false\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::AT_LEAST_ONE_OF_ERROR)\n      .assert_violation\n  end\n\n  def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/blank_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Blank\n\nstruct BlankValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_blank_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_blank_spaces_is_valid : Nil\n    self.validator.validate \"   \", self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"invalid_values\")]\n  def test_invalid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::NOT_BLANK_ERROR, value\n  end\n\n  def invalid_values : Tuple\n    {\n      {\"foobar\"},\n      {0},\n      {false},\n      {1234},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/callback_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Callback\n\nstruct CallbackValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_callback : Nil\n    constraint = CONSTRAINT.with_callback(payload: {\"foo\" => \"bar\"}) do |value, context, payload|\n      value.should eq 123\n      payload.should eq({\"foo\" => \"bar\"})\n\n      context.add_violation(\"my_message\")\n    end\n\n    self.validator.validate 123, constraint\n    self.assert_violation \"my_message\"\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/choice_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Choice\n\nstruct ChoiceValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_requires_enumerable_if_multiple_is_true : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Enumerable\" do\n      self.validator.validate \"foo\", self.new_constraint choices: [\"foo\", \"bar\"], multiple: true\n    end\n  end\n\n  def test_requires_enumerable_if_multiple_is_false : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Enumerable\" do\n      self.validator.validate [1, 2], self.new_constraint choices: [\"foo\", \"bar\"], multiple: false\n    end\n  end\n\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint choices: [\"foo\", \"bar\"]\n    self.assert_no_violation\n  end\n\n  def test_valid_choice : Nil\n    self.validator.validate \"bar\", self.new_constraint choices: [\"foo\", \"bar\"]\n    self.assert_no_violation\n  end\n\n  def test_multiple_choices : Nil\n    self.validator.validate [\"foo\", \"bar\"], self.new_constraint choices: [\"foo\", \"bar\"], multiple: true\n    self.assert_no_violation\n  end\n\n  def test_invalid_choice : Nil\n    self.validator.validate \"baz\", self.new_constraint choices: [\"foo\", \"bar\"], message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NO_SUCH_CHOICE_ERROR, \"baz\")\n      .add_parameter(\"{{ choices }}\", [\"foo\", \"bar\"])\n      .assert_violation\n  end\n\n  def test_invalid_choice_empty_choices_array : Nil\n    self.validator.validate \"baz\", self.new_constraint choices: [] of String, message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NO_SUCH_CHOICE_ERROR, \"baz\")\n      .add_parameter(\"{{ choices }}\", [] of String)\n      .assert_violation\n  end\n\n  def test_invalid_choices_multiple : Nil\n    self.validator.validate [\"foo\", \"baz\"], self.new_constraint choices: [\"foo\", \"bar\"], multiple: true, multiple_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NO_SUCH_CHOICE_ERROR, \"baz\")\n      .add_parameter(\"{{ choices }}\", [\"foo\", \"bar\"])\n      .invalid_value(\"baz\")\n      .assert_violation\n  end\n\n  def test_invalid_choices_too_few : Nil\n    value = [\"foo\"]\n\n    self.value = value\n\n    self.validator.validate value, self.new_constraint choices: [\"foo\", \"bar\", \"moo\", \"maa\"], multiple: true, range: (2..), min_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_FEW_ERROR, value)\n      .add_parameter(\"{{ limit }}\", 2)\n      .add_parameter(\"{{ choices }}\", [\"foo\", \"bar\", \"moo\", \"maa\"])\n      .invalid_value(value)\n      .plural(2)\n      .assert_violation\n  end\n\n  def test_invalid_choices_too_many : Nil\n    value = [\"foo\", \"bar\", \"moo\"]\n\n    self.value = value\n\n    self.validator.validate value, self.new_constraint choices: [\"foo\", \"bar\", \"moo\", \"maa\"], multiple: true, range: (..2), max_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_MANY_ERROR, value)\n      .add_parameter(\"{{ limit }}\", 2)\n      .add_parameter(\"{{ choices }}\", [\"foo\", \"bar\", \"moo\", \"maa\"])\n      .invalid_value(value)\n      .plural(2)\n      .assert_violation\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/collection_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AVD::Constraints::Collection do\n  it \"transforms non optional/required constraints to required\" do\n    constraint = AVD::Constraints::Collection.new({\"name\" => ic = AVD::Constraints::NotBlank.new})\n    constraint.constraints.size.should eq 1\n    c = constraint.constraints[\"name\"]?.should be_a AVD::Constraints::Required\n    c.constraints.should eq({0 => ic})\n  end\n\n  it \"allows explicit required constraints\" do\n    constraint = AVD::Constraints::Collection.new({\"name\" => ic = AVD::Constraints::Required.new(nb = AVD::Constraints::NotBlank.new)})\n    constraint.constraints.size.should eq 1\n    c = constraint.constraints[\"name\"]?.should be_a AVD::Constraints::Required\n    c.should eq ic\n    c.constraints.should eq({0 => nb})\n  end\n\n  it \"allows explicit optional constraints\" do\n    constraint = AVD::Constraints::Collection.new({\"name\" => ic = AVD::Constraints::Optional.new(nb = AVD::Constraints::NotBlank.new)})\n    constraint.constraints.size.should eq 1\n    c = constraint.constraints[\"name\"]?.should be_a AVD::Constraints::Optional\n    c.should eq ic\n    c.constraints.should eq({0 => nb})\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/collection_validator_test_case.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Collection\n\nabstract struct CollectionValidatorTestCase < AVD::Spec::ConstraintValidatorTestCase\n  private abstract def prepare_test_data(contents : Hash)\n\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint fields: {\"foo\" => AVD::Constraints::NotBlank.new}\n    self.assert_no_violation\n  end\n\n  def test_invalid_type : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Expected argument of type 'Enumerable({K, V})', 'String' given.\" do\n      self.validator.validate \"foobar\", self.new_constraint fields: {\"foo\" => AVD::Constraints::NotBlank.new}\n    end\n  end\n\n  def test_walks_single_constraint : Nil\n    constraint = AVD::Constraints::Range.new 4..\n\n    data = {\n      \"foo\" => 3,\n      \"bar\" => 5,\n    }\n\n    idx = 0\n\n    data.each do |k, v|\n      self.expect_validate_value_at idx, \"[#{k}]\", v, [constraint]\n      idx += 1\n    end\n\n    data = self.prepare_test_data data\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => constraint,\n      \"bar\" => constraint,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_walks_multiple_constraints : Nil\n    constraints = [\n      AVD::Constraints::Range.new(4..),\n      AVD::Constraints::NotNil.new,\n    ]\n\n    data = {\n      \"foo\" => 3,\n      \"bar\" => 5,\n    }\n\n    idx = 0\n\n    data.each do |k, v|\n      self.expect_validate_value_at idx, \"[#{k}]\", v, constraints\n      idx += 1\n    end\n\n    data = self.prepare_test_data data\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => constraints,\n      \"bar\" => constraints,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_extra_fields_disallowed : Nil\n    constraint = AVD::Constraints::Range.new(4..)\n\n    data = self.prepare_test_data({\n      \"foo\" => 5,\n      \"baz\" => 6,\n    })\n\n    self.expect_validate_value_at 0, \"[foo]\", data[\"foo\"], [constraint]\n\n    self.validator.validate data, self.new_constraint extra_fields_message: \"my_message\", fields: {\n      \"foo\" => constraint,\n    }\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NO_SUCH_FIELD_ERROR)\n      .invalid_value(6)\n      .add_parameter(\"{{ field }}\", \"baz\")\n      .at_path(\"property.path[baz]\")\n      .assert_violation\n  end\n\n  def test_extra_fields_disallowed_with_optional_values : Nil\n    constraint = AVD::Constraints::Optional.new\n\n    data = self.prepare_test_data({\n      \"baz\" => 6,\n    })\n\n    self.validator.validate data, self.new_constraint extra_fields_message: \"my_message\", fields: {\n      \"foo\" => constraint,\n    }\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NO_SUCH_FIELD_ERROR)\n      .invalid_value(6)\n      .add_parameter(\"{{ field }}\", \"baz\")\n      .at_path(\"property.path[baz]\")\n      .assert_violation\n  end\n\n  def test_nil_not_considered_extra_field : Nil\n    constraint = AVD::Constraints::Range.new(4..)\n\n    data = self.prepare_test_data({\n      \"foo\" => nil,\n    })\n\n    self.expect_validate_value_at 0, \"[foo]\", data[\"foo\"], [constraint]\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => constraint,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_extra_fields_allowed : Nil\n    constraint = AVD::Constraints::Range.new(4..)\n\n    data = self.prepare_test_data({\n      \"foo\" => 5,\n      \"baz\" => 6,\n    })\n\n    self.expect_validate_value_at 0, \"[foo]\", data[\"foo\"], [constraint]\n\n    self.validator.validate data, self.new_constraint allow_extra_fields: true, fields: {\n      \"foo\" => constraint,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_missing_fields_disallowed : Nil\n    constraint = AVD::Constraints::Range.new(4..)\n\n    data = self.prepare_test_data({} of String => Int32)\n\n    self.validator.validate data, self.new_constraint missing_fields_message: \"my_message\", fields: {\n      \"foo\" => constraint,\n    }\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::MISSING_FIELD_ERROR)\n      .at_path(\"property.path[foo]\")\n      .add_parameter(\"{{ field }}\", \"foo\")\n      .invalid_value(nil)\n      .assert_violation\n  end\n\n  def test_missing_fields_allowed : Nil\n    constraint = AVD::Constraints::Range.new(4..)\n\n    data = self.prepare_test_data({} of String => Int32)\n\n    self.validator.validate data, self.new_constraint allow_missing_fields: true, fields: {\n      \"foo\" => constraint,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_optional_field_present_null : Nil\n    data = self.prepare_test_data({\n      \"foo\" => nil,\n    })\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => AVD::Constraints::Optional.new,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_optional_field_not_present : Nil\n    data = self.prepare_test_data({} of String => Int32)\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => AVD::Constraints::Optional.new,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_optional_field_single_constraint : Nil\n    data = {\n      \"foo\" => 5,\n    }\n\n    constraint = AVD::Constraints::Range.new(4..)\n\n    self.expect_validate_value_at 0, \"[foo]\", data[\"foo\"], [constraint]\n\n    data = self.prepare_test_data data\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => AVD::Constraints::Optional.new constraint,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_optional_field_multiple_constraints : Nil\n    data = {\n      \"foo\" => 5,\n    }\n\n    constraints = [\n      AVD::Constraints::NotNil.new,\n      AVD::Constraints::Range.new(4..),\n    ]\n\n    self.expect_validate_value_at 0, \"[foo]\", data[\"foo\"], constraints\n\n    data = self.prepare_test_data data\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => AVD::Constraints::Optional.new constraints,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_required_field_present_null : Nil\n    data = self.prepare_test_data({\n      \"foo\" => nil,\n    })\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => AVD::Constraints::Required.new,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_required_field_not_present : Nil\n    data = self.prepare_test_data({} of String => Int32)\n\n    self.validator.validate data, self.new_constraint missing_fields_message: \"my_message\", fields: {\n      \"foo\" => AVD::Constraints::Required.new,\n    }\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::MISSING_FIELD_ERROR)\n      .at_path(\"property.path[foo]\")\n      .add_parameter(\"{{ field }}\", \"foo\")\n      .invalid_value(nil)\n      .assert_violation\n  end\n\n  def test_required_field_single_constraint : Nil\n    data = {\n      \"foo\" => 5,\n    }\n\n    constraint = AVD::Constraints::Range.new(4..)\n\n    self.expect_validate_value_at 0, \"[foo]\", data[\"foo\"], [constraint]\n\n    data = self.prepare_test_data data\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => AVD::Constraints::Required.new constraint,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_required_field_multiple_constraints : Nil\n    data = {\n      \"foo\" => 5,\n    }\n\n    constraints = [\n      AVD::Constraints::NotNil.new,\n      AVD::Constraints::Range.new(4..),\n    ]\n\n    self.expect_validate_value_at 0, \"[foo]\", data[\"foo\"], constraints\n\n    data = self.prepare_test_data data\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => AVD::Constraints::Required.new constraints,\n    }\n\n    self.assert_no_violation\n  end\n\n  def test_does_not_mutate_object : Nil\n    hash = {\n      \"foo\" => 3,\n    }\n\n    constraint = AVD::Constraints::Range.new(2..)\n\n    self.expect_validate_value_at 0, \"[foo]\", hash[\"foo\"], [constraint]\n\n    data = self.prepare_test_data hash\n\n    self.validator.validate data, self.new_constraint fields: {\n      \"foo\" => constraint,\n    }\n\n    hash.should eq({\"foo\" => 3})\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/composite_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class ConcreteComposite < AVD::Constraints::Composite\n  def initialize(\n    constraints : Array(AVD::Constraint) | AVD::Constraint = [] of AVD::Constraint,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super constraints, \"\", groups, payload\n  end\n\n  # :inherit:\n  def validated_by : NoReturn\n    raise \"BUG: #{self} cannot be validated\"\n  end\nend\n\nstruct CompositeTest < ASPEC::TestCase\n  def test_default_group : Nil\n    constraint = ConcreteComposite.new([\n      AVD::Constraints::NotNil.new,\n      AVD::Constraints::NotBlank.new,\n    ])\n\n    constraint.groups.should eq [\"default\"]\n    constraint.constraints[0].groups.should eq [\"default\"]\n    constraint.constraints[1].groups.should eq [\"default\"]\n  end\n\n  def test_nested_composite_constraint_has_default_group : Nil\n    constraint = ConcreteComposite.new([\n      ConcreteComposite.new,\n      ConcreteComposite.new,\n    ] of AVD::Constraint)\n\n    constraint.groups.should eq [\"default\"]\n    constraint.constraints[0].groups.should eq [\"default\"]\n    constraint.constraints[1].groups.should eq [\"default\"]\n  end\n\n  def test_implicit_nested_groups_if_explicit_parent_group : Nil\n    constraint = ConcreteComposite.new([\n      AVD::Constraints::NotNil.new,\n      AVD::Constraints::NotBlank.new,\n    ], groups: [\"default\", \"strict\"])\n\n    constraint.groups.should eq [\"default\", \"strict\"]\n    constraint.constraints[0].groups.should eq [\"default\", \"strict\"]\n    constraint.constraints[1].groups.should eq [\"default\", \"strict\"]\n  end\n\n  def test_explicit_nested_groups_must_be_subset_of_explicit_parent_groups : Nil\n    constraint = ConcreteComposite.new([\n      AVD::Constraints::NotNil.new(groups: \"default\"),\n      AVD::Constraints::NotBlank.new(groups: \"strict\"),\n    ], groups: [\"default\", \"strict\"])\n\n    constraint.groups.should eq [\"default\", \"strict\"]\n    constraint.constraints[0].groups.should eq [\"default\"]\n    constraint.constraints[1].groups.should eq [\"strict\"]\n  end\n\n  def test_fail_if_explicit_nest_group_not_subset_of_explicit_parent_groups : Nil\n    expect_raises AVD::Exception::Logic, \"The group(s) 'foobar' passed to the constraint 'Athena::Validator::Constraints::NotNil' should also be passed to its containing constraint 'ConcreteComposite'.\" do\n      ConcreteComposite.new([\n        AVD::Constraints::NotNil.new(groups: [\"default\", \"foobar\"]),\n      ] of AVD::Constraint, groups: [\"default\", \"strict\"])\n    end\n  end\n\n  def test_implicit_group_names_are_forwarded : Nil\n    constraint = ConcreteComposite.new([\n      AVD::Constraints::NotNil.new(groups: \"default\"),\n      AVD::Constraints::NotBlank.new(groups: \"strict\"),\n    ])\n\n    constraint.add_implicit_group \"implicit\"\n\n    constraint.groups.should eq [\"default\", \"strict\", \"implicit\"]\n    constraint.constraints[0].groups.should eq [\"default\", \"implicit\"]\n    constraint.constraints[1].groups.should eq [\"strict\"]\n  end\n\n  def test_valid_cannot_be_nested : Nil\n    expect_raises AVD::Exception::Logic, \"The 'Athena::Validator::Constraints::Valid' constraint cannot be nested inside a 'ConcreteComposite' constraint.\" do\n      ConcreteComposite.new([\n        AVD::Constraints::Valid.new,\n      ] of AVD::Constraint)\n    end\n  end\n\n  def test_single_element_inferred_type_array : Nil\n    constraint = ConcreteComposite.new([\n      AVD::Constraints::Positive.new,\n    ])\n\n    constraint.constraints.size.should eq 1\n    constraint.groups.should eq [\"default\"]\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/compound_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class DummyCompoundConstraint < AVD::Constraints::Compound\n  def constraints : Type\n    [\n      AVD::Constraints::NotBlank.new,\n      AVD::Constraints::Length.new(..3),\n    ]\n  end\nend\n\nstruct CompoundValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_valid_value : Nil\n    self.validator.validate \"foo\", DummyCompoundConstraint.new\n    self.assert_no_violation\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    AVD::Constraints::Compound::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    DummyCompoundConstraint\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/count_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Count\n\nstruct CountValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint(range: (1..1), exact_message: \"my_message\")\n\n    self.assert_no_violation\n  end\n\n  def three_or_less : Tuple\n    {\n      { {1} },\n      { {1, 2} },\n      { {1, 2, 3} },\n      {[1]},\n      {[1, 2]},\n      {[1, 2, 3]},\n    }\n  end\n\n  def four : Tuple\n    {\n      { {1, 2, 3, 4} },\n      {[4, 3, 2, 1]},\n    }\n  end\n\n  def five_or_more : Tuple\n    {\n      { {1, 2, 3, 4, 5} },\n      {[5, 4, 3, 2, 1]},\n    }\n  end\n\n  @[DataProvider(\"three_or_less\")]\n  def test_valid_values_max(value : Indexable) : Nil\n    self.validator.validate value, self.new_constraint range: (..3)\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"five_or_more\")]\n  def test_valid_values_min(value : Indexable) : Nil\n    self.validator.validate value, self.new_constraint range: (5..)\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"four\")]\n  def test_values_exact(value : Indexable) : Nil\n    self.validator.validate value, self.new_constraint range: (4..4)\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"five_or_more\")]\n  def test_invalid_values_max(value : Indexable) : Nil\n    self.validator.validate value, self.new_constraint range: (..4), max_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_MANY_ERROR, value)\n      .add_parameter(\"{{ count }}\", value.size)\n      .add_parameter(\"{{ limit }}\", 4)\n      .plural(4)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  @[DataProvider(\"three_or_less\")]\n  def test_invalid_values_min(value : Indexable) : Nil\n    self.validator.validate value, self.new_constraint range: (4..), min_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_FEW_ERROR, value)\n      .add_parameter(\"{{ count }}\", value.size)\n      .add_parameter(\"{{ limit }}\", 4)\n      .plural(4)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  @[DataProvider(\"five_or_more\")]\n  def test_invalid_values_exact_more_than_four(value : Indexable) : Nil\n    self.validator.validate value, self.new_constraint range: (4..4), exact_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_EQUAL_COUNT_ERROR, value)\n      .add_parameter(\"{{ count }}\", value.size)\n      .add_parameter(\"{{ limit }}\", 4)\n      .plural(4)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  @[DataProvider(\"three_or_less\")]\n  def test_invalid_values_exact_less_than_four(value : Indexable) : Nil\n    self.validator.validate value, self.new_constraint range: (4..4), exact_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_EQUAL_COUNT_ERROR, value)\n      .add_parameter(\"{{ count }}\", value.size)\n      .add_parameter(\"{{ limit }}\", 4)\n      .plural(4)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/email_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Email\n\nprivate class EmptyEmailObject\n  def to_s(io : IO) : Nil\n    io << \"\"\n  end\nend\n\nstruct EmailValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_empty_string_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n  end\n\n  def test_empty_string_from_object_is_valid : Nil\n    self.validator.validate EmptyEmailObject.new, self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"valid_emails\")]\n  def test_valid_emails(value : String) : Nil\n    self.validator.validate value, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def valid_emails : Tuple\n    {\n      {\"blacksmoke16@dietrich.app\"},\n      {\"example@example.co.uk\"},\n      {\"blacksmoke_blacksmoke@example.fr\"},\n      {\"{}~!@example.com\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_emails\")]\n  def test_invalid_emails(value : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::INVALID_FORMAT_ERROR, value\n  end\n\n  def invalid_emails : Tuple\n    {\n      {\"example\"},\n      {\"example@\"},\n      {\"example@localhost\"},\n      {\"example@example.co..uk\"},\n      {\"foo@example.com bar\"},\n      {\"example@example.\"},\n      {\"example@.fr\"},\n      {\"@example.com\"},\n      {\"example@example.com;example@example.com\"},\n      {\"example@.\"},\n      {\" example@example.com\"},\n      {\"example@ \"},\n      {\" example@example.com \"},\n      {\" example @example .com \"},\n      {\"example@-example.com\"},\n      {\"example@#{\"a\"*64}.com\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_emails_html5_allow_no_tld\")]\n  def test_invalid_emails_html5_allow_no_tld(value : String) : Nil\n    self.validator.validate value, self.new_constraint mode: CONSTRAINT::Mode::HTML5_ALLOW_NO_TLD, message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::INVALID_FORMAT_ERROR, value\n  end\n\n  def invalid_emails_html5_allow_no_tld : Tuple\n    {\n      {\"example bar\"},\n      {\"example@\"},\n      {\"example@ bar\"},\n      {\"example@localhost bar\"},\n      {\"foo@example.com bar\"},\n    }\n  end\n\n  def test_valid_email_html5_allow_no_tld : Nil\n    self.validator.validate \"example@example\", self.new_constraint mode: CONSTRAINT::Mode::HTML5_ALLOW_NO_TLD\n    self.assert_no_violation\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/equal_to_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::EqualTo\n\nstruct EqualToValidatorTest < AVD::Spec::ComparisonConstraintValidatorTestCase\n  def valid_comparisons : Tuple\n    {\n      {3, 3},\n      {'a', 'a'},\n      {\"a\", \"a\"},\n      {Time.utc(2020, 4, 7), Time.utc(2020, 4, 7)},\n      {nil, false},\n    }\n  end\n\n  def invalid_comparisons : Tuple\n    {\n      {1, 3},\n      {'b', 'a'},\n      {\"b\", \"a\"},\n      {Time.utc(2020, 4, 8), Time.utc(2020, 4, 7)},\n    }\n  end\n\n  def error_code : String\n    CONSTRAINT::NOT_EQUAL_ERROR\n  end\n\n  def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/file_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::File\n\nstruct FileTest < ASPEC::TestCase\n  @[DataProvider(\"valid_sizes\")]\n  def test_max_size(max_size : String | Int, bytes : Int, binary_format : Bool) : Nil\n    constraint = CONSTRAINT.new max_size: max_size\n\n    constraint.max_size.should eq bytes\n    constraint.binary_format?.should eq binary_format\n  end\n\n  @[DataProvider(\"invalid_sizes\")]\n  def test_invalid_max_size(max_size : String | Int) : Nil\n    expect_raises ArgumentError do\n      CONSTRAINT.new max_size: max_size\n    end\n  end\n\n  @[DataProvider(\"formats\")]\n  def test_binary_format(max_size : String | Int, guessed_format : Bool?, binary_format : Bool) : Nil\n    CONSTRAINT.new(max_size: max_size, binary_format: guessed_format).binary_format?.should eq binary_format\n  end\n\n  def valid_sizes : Tuple\n    {\n      {\"500\", 500, false},\n      {12_300, 12_300, false},\n      {\"1ki\", 1_024, true},\n      {\"1KI\", 1_024, true},\n      {\"2k\", 2_000, false},\n      {\"2K\", 2_000, false},\n      {\"1mi\", 1_048_576, true},\n      {\"1MI\", 1_048_576, true},\n      {\"3m\", 3_000_000, false},\n      {\"3M\", 3_000_000, false},\n      {\"1gi\", 1_073_741_824, true},\n      {\"1GI\", 1_073_741_824, true},\n      {\"4g\", 4_000_000_000, false},\n      {\"4G\", 4_000_000_000, false},\n    }\n  end\n\n  def invalid_sizes : Tuple\n    {\n      {\"foo\"},\n      {\"1Ko\"},\n      {\"1kio\"},\n    }\n  end\n\n  def formats : Tuple\n    {\n      {100, nil, false},\n      {100, true, true},\n      {100, false, false},\n      {\"100K\", nil, false},\n      {\"100K\", true, true},\n      {\"100K\", false, false},\n      {\"100Ki\", nil, true},\n      {\"100Ki\", true, true},\n      {\"100Ki\", false, false},\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/file_validator_ath_file_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./file_validator_test_case\"\n\nprivate alias CONSTRAINT = AVD::Constraints::File\n\nstruct FileValidatorATHFileTest < FileValidatorTestCase\n  protected def get_file(file_path : String)\n    Athena::HTTP::File.new file_path\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/file_validator_path_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./file_validator_test_case\"\n\nprivate alias CONSTRAINT = AVD::Constraints::File\n\nstruct FileValidatorPathTest < FileValidatorTestCase\n  def test_not_found : Nil\n    self.validator.validate \"foo\", self.new_constraint not_found_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_FOUND_ERROR)\n      .add_parameter(\"{{ file }}\", \"foo\")\n      .assert_violation\n  end\n\n  protected def get_file(file_path : String)\n    file_path\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/file_validator_std_file_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./file_validator_test_case\"\n\nprivate alias CONSTRAINT = AVD::Constraints::File\n\nstruct FileValidatorStdlibFileTest < FileValidatorTestCase\n  protected def get_file(file_path : String)\n    ::File.new file_path\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/file_validator_test_case.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::File\n\nabstract struct FileValidatorTestCase < AVD::Spec::ConstraintValidatorTestCase\n  @file : File\n\n  def initialize\n    super\n\n    @file = File.open Path[Dir.tempdir, \"file_validator_test\"], \"w\"\n    @file.print \" \"\n    @file.flush\n  end\n\n  def tear_down : Nil\n    super\n\n    @file.delete\n  end\n\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_blank_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_valid_file : Nil\n    self.validator.validate @file.path, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_valid_uploaded_file : Nil\n    File.write @file.path, \"1\"\n    self.validator.validate AHTTP::UploadedFile.new(@file.path, \"original_name\", test: true), self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"max_size_exceeded\")]\n  def test_max_size_exceeded(bytes_written : Int, limit : Int | String, size_as_string : String, limit_as_string : String, suffix : String) : Nil\n    self.write_bytes bytes_written\n    self.validator.validate self.get_file(@file.path), self.new_constraint max_size: limit, max_size_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_LARGE_ERROR)\n      .add_parameter(\"{{ limit }}\", limit_as_string)\n      .add_parameter(\"{{ size }}\", size_as_string)\n      .add_parameter(\"{{ suffix }}\", suffix)\n      .add_parameter(\"{{ file }}\", @file.path)\n      .add_parameter(\"{{ name }}\", File.basename @file.path)\n      .assert_violation\n  end\n\n  def max_size_exceeded : Tuple\n    {\n      # Limit in bytes\n      {1_001, 1_000, \"1001.0\", \"1000.0\", \"bytes\"},\n      {1_004, 1_000, \"1004.0\", \"1000.0\", \"bytes\"},\n      # {1005, 1000, \"1.01\", \"1.0\", \"kB\"},\n      {1_006, 1_000, \"1.01\", \"1.0\", \"kB\"},\n\n      {1_000_001, 1_000_000, \"1000001.0\", \"1000000.0\", \"bytes\"},\n      {1_004_999, 1_000_000, \"1005.0\", \"1000.0\", \"kB\"},\n      # {1_005_000, 1_000_000, \"1.01\", \"1.0\", \"MB\"},\n      {1_006_000, 1_000_000, \"1.01\", \"1.0\", \"MB\"},\n\n      # Limit in kB\n      {1_001, \"1k\", \"1001.0\", \"1000.0\", \"bytes\"},\n      {1_004, \"1k\", \"1004.0\", \"1000.0\", \"bytes\"},\n      # {1005, \"1k\", \"1.01\", \"1.0\", \"kB\"},\n      {1_006, \"1k\", \"1.01\", \"1.0\", \"kB\"},\n\n      {1_000_001, \"1000k\", \"1000001.0\", \"1000000.0\", \"bytes\"},\n      {1_004_999, \"1000k\", \"1005.0\", \"1000.0\", \"kB\"},\n      # {1_005_000, \"1000k\", \"1.01\", \"1.0\", \"MB\"},\n      {1_006_000, \"1000k\", \"1.01\", \"1.0\", \"MB\"},\n\n      # Limit in MB\n      {1_000_001, \"1M\", \"1000001.0\", \"1000000.0\", \"bytes\"},\n      {1_004_999, \"1M\", \"1005.0\", \"1000.0\", \"kB\"},\n      # {1_005_000, \"1M\", \"1.01\", \"1.0\", \"MB\"},\n      {1_006_000, \"1M\", \"1.01\", \"1.0\", \"MB\"},\n\n      # Limit in KiB\n      {1_025, \"1Ki\", \"1025.0\", \"1024.0\", \"bytes\"},\n      {1_029, \"1Ki\", \"1029.0\", \"1024.0\", \"bytes\"},\n      {1_030, \"1Ki\", \"1.01\", \"1.0\", \"KiB\"},\n\n      {1_048_577, \"1024Ki\", \"1048577.0\", \"1048576.0\", \"bytes\"},\n      {1_053_818, \"1024Ki\", \"1029.12\", \"1024.0\", \"KiB\"},\n      {1_053_819, \"1024Ki\", \"1.01\", \"1.0\", \"MiB\"},\n\n      # Limit in MiB\n      {1_048_577, \"1Mi\", \"1048577.0\", \"1048576.0\", \"bytes\"},\n      {1_053_818, \"1Mi\", \"1029.12\", \"1024.0\", \"KiB\"},\n      {1_053_819, \"1Mi\", \"1.01\", \"1.0\", \"MiB\"},\n\n      # limit < coef\n      {169_632, \"100k\", \"169.63\", \"100.0\", \"kB\"},\n      {1_000_001, \"990K\", \"1000.0\", \"990.0\", \"kB\"},\n      {123, \"80\", \"123.0\", \"80.0\", \"bytes\"},\n    }\n  end\n\n  @[DataProvider(\"max_size_not_exceeded\")]\n  def test_max_size_exceeded(bytes_written : Int, limit : Int | String) : Nil\n    self.write_bytes bytes_written\n    self.validator.validate self.get_file(@file.path), self.new_constraint max_size: limit, max_size_message: \"my_message\"\n    self.assert_no_violation\n  end\n\n  def max_size_not_exceeded : Tuple\n    {\n      # Limit in bytes\n      {1_000, 1_000},\n      {1_000_000, 1_000_000},\n\n      # Limit in kB\n      {1_000, \"1k\"},\n      {1_000_000, \"1000k\"},\n\n      # Limit in MB\n      {1_000_000, \"1M\"},\n\n      # Limit in KiB\n      {1_024, \"1Ki\"},\n      {1_048_576, \"1024Ki\"},\n\n      # Limit in MiB\n      {1_048_576, \"1Mi\"},\n    }\n  end\n\n  @[DataProvider(\"max_size_exceeded_binary_format\")]\n  def test_max_size_exceeded(bytes_written : Int, limit : Int | String, binary_format : Bool?, size_as_string : String, limit_as_string : String, suffix : String) : Nil\n    self.write_bytes bytes_written\n    self.validator.validate self.get_file(@file.path), self.new_constraint max_size: limit, binary_format: binary_format, max_size_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_LARGE_ERROR)\n      .add_parameter(\"{{ limit }}\", limit_as_string)\n      .add_parameter(\"{{ size }}\", size_as_string)\n      .add_parameter(\"{{ suffix }}\", suffix)\n      .add_parameter(\"{{ file }}\", @file.path)\n      .add_parameter(\"{{ name }}\", File.basename @file.path)\n      .assert_violation\n  end\n\n  def max_size_exceeded_binary_format : Tuple\n    {\n      {11, 10, nil, \"11.0\", \"10.0\", \"bytes\"},\n      {11, 10, true, \"11.0\", \"10.0\", \"bytes\"},\n      {11, 10, false, \"11.0\", \"10.0\", \"bytes\"},\n\n      {1_010, 1000, nil, \"1.01\", \"1.0\", \"kB\"},\n      {1_010, \"1k\", nil, \"1.01\", \"1.0\", \"kB\"},\n      {1_035, \"1Ki\", nil, \"1.01\", \"1.0\", \"KiB\"},\n\n      {1_035, 1024, true, \"1.01\", \"1.0\", \"KiB\"},\n      {1_034_240, \"1024k\", true, \"1010.0\", \"1000.0\", \"KiB\"},\n      {1_035, \"1Ki\", true, \"1.01\", \"1.0\", \"KiB\"},\n\n      {1_010, 1000, false, \"1.01\", \"1.0\", \"kB\"},\n      {1_010, \"1k\", false, \"1.01\", \"1.0\", \"kB\"},\n      {10_343, \"10Ki\", false, \"10.34\", \"10.24\", \"kB\"},\n    }\n  end\n\n  def test_valid_mime_type : Nil\n    self.validator.validate Path[__DIR__, \"fixtures/foo.png\"], self.new_constraint mime_types: {\"image/png\", \"image/jpeg\"}\n    self.assert_no_violation\n  end\n\n  def test_valid_wildcard_mime_type : Nil\n    self.validator.validate Path[__DIR__, \"fixtures/foo.png\"], self.new_constraint mime_types: {\"image/*\"}\n    self.assert_no_violation\n  end\n\n  def test_invalid_mime_type : Nil\n    File.copy Path[__DIR__, \"fixtures/foo.png\"], dest_path = Path[Dir.tempdir, \"/file_validator_test.png\"]\n\n    self.validator.validate self.get_file(dest_path.to_s), self.new_constraint mime_types: {\"application/pdf\"}, mime_type_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::INVALID_MIME_TYPE_ERROR)\n      .add_parameter(\"{{ type }}\", \"image/png\")\n      .add_parameter(\"{{ types }}\", %(Set{\"application/pdf\"}))\n      .add_parameter(\"{{ file }}\", dest_path)\n      .add_parameter(\"{{ name }}\", \"file_validator_test.png\")\n      .assert_violation\n  end\n\n  def test_invalid_wildcard_mime_type : Nil\n    File.copy Path[__DIR__, \"fixtures/foo.png\"], dest_path = Path[Dir.tempdir, \"/file_validator_test.png\"]\n\n    self.validator.validate self.get_file(dest_path.to_s), self.new_constraint mime_types: {\"application/*\"}, mime_type_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::INVALID_MIME_TYPE_ERROR)\n      .add_parameter(\"{{ type }}\", \"image/png\")\n      .add_parameter(\"{{ types }}\", %(Set{\"application/*\"}))\n      .add_parameter(\"{{ file }}\", dest_path)\n      .add_parameter(\"{{ name }}\", \"file_validator_test.png\")\n      .assert_violation\n  end\n\n  def test_uploaded_file_error : Nil\n    AHTTP::UploadedFile.max_file_size = 100\n\n    uploaded_file = AHTTP::UploadedFile.new \"#{__DIR__}/fixtures/file-big.txt\", \"file-big.txt\", \"text/plain\", :size_limit_exceeded\n\n    self.validator.validate uploaded_file, self.new_constraint max_size: 50, upload_file_size_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::UPLOAD_FILE_SIZE_ERROR)\n      .add_parameter(\"{{ limit }}\", \"50.0\")\n      .add_parameter(\"{{ suffix }}\", \"bytes\")\n      .assert_violation\n  ensure\n    AHTTP::UploadedFile.max_file_size = 0\n  end\n\n  def test_uploaded_file_error_mib : Nil\n    AHTTP::UploadedFile.max_file_size = 1024 * 1024 * 10\n\n    uploaded_file = AHTTP::UploadedFile.new \"#{__DIR__}/fixtures/file-big.txt\", \"file-big.txt\", \"text/plain\", :size_limit_exceeded\n\n    self.validator.validate uploaded_file, self.new_constraint upload_file_size_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::UPLOAD_FILE_SIZE_ERROR)\n      .add_parameter(\"{{ limit }}\", \"10.0\")\n      .add_parameter(\"{{ suffix }}\", \"MiB\")\n      .assert_violation\n  ensure\n    AHTTP::UploadedFile.max_file_size = 0\n  end\n\n  # def test_uploaded_file_extension : Nil\n  # end\n\n  # def test_uploaded_file_name_max_length : Nil\n  # end\n\n  # def test_uploaded_file_charset : Nil\n  # end\n\n  def test_empty_file : Nil\n    @file.truncate\n\n    self.validator.validate self.get_file(@file.path), self.new_constraint empty_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::EMPTY_ERROR)\n      .add_parameter(\"{{ file }}\", @file.path)\n      .add_parameter(\"{{ name }}\", File.basename @file.path)\n      .assert_violation\n  end\n\n  protected abstract def get_file(file_path : String)\n\n  private def write_bytes(bytes : Int) : Nil\n    @file.write Random.new.random_bytes bytes - 1\n    @file.flush\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/fixtures/file-big.txt",
    "content": "I'm not big, but I'm big enough to carry more than 50 bytes inside me.\n"
  },
  {
    "path": "src/components/validator/spec/constraints/greater_than_or_equal_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::GreaterThanOrEqual\n\nstruct GreaterThanOrEqualValidatorTest < AVD::Spec::ComparisonConstraintValidatorTestCase\n  def valid_comparisons : Tuple\n    {\n      {3, 2},\n      {0, 0_u8},\n      {\"333\", \"22\"},\n      {\"22\", \"22\"},\n      {Time.utc(2020, 4, 8), Time.utc(2020, 4, 7)},\n      {nil, false},\n    }\n  end\n\n  def invalid_comparisons : Tuple\n    {\n      {2, 3},\n      {\"a\", \"b\"},\n      {Time.utc(2020, 4, 6), Time.utc(2020, 4, 7)},\n    }\n  end\n\n  def test_invalid_type : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Expected argument of type 'Number | String | Time', 'Bool' given.\" do\n      self.validator.validate false, new_constraint value: 50\n    end\n  end\n\n  def error_code : String\n    CONSTRAINT::TOO_LOW_ERROR\n  end\n\n  def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/greater_than_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::GreaterThan\n\nstruct GreaterThanValidatorTest < AVD::Spec::ComparisonConstraintValidatorTestCase\n  def valid_comparisons : Tuple\n    {\n      {3, 2},\n      {\"333\", \"22\"},\n      {Time.utc(2020, 4, 8), Time.utc(2020, 4, 7)},\n      {nil, false},\n    }\n  end\n\n  def invalid_comparisons : Tuple\n    {\n      {2, 3},\n      {3, 3},\n      {\"a\", \"b\"},\n      {Time.utc(2020, 4, 6), Time.utc(2020, 4, 7)},\n    }\n  end\n\n  def test_invalid_type : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Expected argument of type 'Number | String | Time', 'Bool' given.\" do\n      self.validator.validate false, new_constraint value: 50\n    end\n  end\n\n  def error_code : String\n    CONSTRAINT::TOO_LOW_ERROR\n  end\n\n  def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/hash_collection_validator_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./collection_validator_test_case\"\n\nstruct HashCollectionValidatorTest < CollectionValidatorTestCase\n  private def prepare_test_data(contents : Hash)\n    contents\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/hash_like_object_collection_validator_spec.cr",
    "content": "require \"../spec_helper\"\nrequire \"./collection_validator_test_case\"\n\nprivate struct HashLikeObject\n  include Enumerable({String | Int32, Int32?})\n\n  @data = {} of String => Int32?\n\n  delegate :each, to: @data\n\n  def has_key?(key : String | Int32) : Bool\n    @data.has_key? key\n  end\n\n  def [](key : String | Int32) : Int32?\n    @data[key]\n  end\n\n  def []=(key : String | Int32, value : Int32?)\n    @data[key] = value\n  end\nend\n\nstruct HashLikeObjectCollectionValidatorTest < CollectionValidatorTestCase\n  private def prepare_test_data(contents : Hash)\n    collection = HashLikeObject.new\n\n    contents.each do |k, v|\n      collection[k] = v\n    end\n\n    collection\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/image_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Image\n\nstruct ImageValidatorTestCase < AVD::Spec::ConstraintValidatorTestCase\n  def initialize\n    super\n\n    @image = \"#{__DIR__}/fixtures/2x2.gif\"\n    @image_landscape = \"#{__DIR__}/fixtures/landscape.gif\"\n    @image_portrait = \"#{__DIR__}/fixtures/portrait.gif\"\n    @image_4x3 = \"#{__DIR__}/fixtures/4x3.gif\"\n    @image_16x9 = \"#{__DIR__}/fixtures/16x9.gif\"\n  end\n\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_blank_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_valid_image : Nil\n    self.validator.validate @image, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_image_not_found : Nil\n    self.validator.validate \"foo\", self.new_constraint not_found_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_FOUND_ERROR)\n      .add_parameter(\"{{ file }}\", \"foo\")\n      .assert_violation\n  end\n\n  def test_valid_sizes : Nil\n    self.validator.validate @image, self.new_constraint min_width: 1, max_width: 2, min_height: 1, max_height: 2\n    self.assert_no_violation\n  end\n\n  def test_width_too_small : Nil\n    self.validator.validate @image, self.new_constraint min_width: 3, min_width_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_NARROW_ERROR)\n      .add_parameter(\"{{ width }}\", 2)\n      .add_parameter(\"{{ min_width }}\", 3)\n      .assert_violation\n  end\n\n  def test_width_too_big : Nil\n    self.validator.validate @image, self.new_constraint max_width: 1, max_width_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_WIDE_ERROR)\n      .add_parameter(\"{{ width }}\", 2)\n      .add_parameter(\"{{ max_width }}\", 1)\n      .assert_violation\n  end\n\n  def test_height_too_small : Nil\n    self.validator.validate @image, self.new_constraint min_height: 3, min_height_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_LOW_ERROR)\n      .add_parameter(\"{{ height }}\", 2)\n      .add_parameter(\"{{ min_height }}\", 3)\n      .assert_violation\n  end\n\n  def test_height_too_big : Nil\n    self.validator.validate @image, self.new_constraint max_height: 1, max_height_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_HIGH_ERROR)\n      .add_parameter(\"{{ height }}\", 2)\n      .add_parameter(\"{{ max_height }}\", 1)\n      .assert_violation\n  end\n\n  def test_too_few_pixels : Nil\n    self.validator.validate @image, self.new_constraint min_pixels: 5.0, min_pixels_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_FEW_PIXEL_ERROR)\n      .add_parameter(\"{{ pixels }}\", 4)\n      .add_parameter(\"{{ min_pixels }}\", 5.0)\n      .add_parameter(\"{{ width }}\", 2)\n      .add_parameter(\"{{ height }}\", 2)\n      .assert_violation\n  end\n\n  def test_too_many_pixels : Nil\n    self.validator.validate @image, self.new_constraint max_pixels: 3.0, max_pixels_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_MANY_PIXEL_ERROR)\n      .add_parameter(\"{{ pixels }}\", 4)\n      .add_parameter(\"{{ max_pixels }}\", 3.0)\n      .add_parameter(\"{{ width }}\", 2)\n      .add_parameter(\"{{ height }}\", 2)\n      .assert_violation\n  end\n\n  def test_ratio_too_small : Nil\n    self.validator.validate @image, self.new_constraint min_ratio: 2.0, min_ratio_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::RATIO_TOO_SMALL_ERROR)\n      .add_parameter(\"{{ ratio }}\", 1.0)\n      .add_parameter(\"{{ min_ratio }}\", 2.0)\n      .assert_violation\n  end\n\n  def test_ratio_too_big : Nil\n    self.validator.validate @image, self.new_constraint max_ratio: 0.5, max_ratio_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::RATIO_TOO_BIG_ERROR)\n      .add_parameter(\"{{ ratio }}\", 1.0)\n      .add_parameter(\"{{ max_ratio }}\", 0.5)\n      .assert_violation\n  end\n\n  def test_max_ratio_uses_two_decimals : Nil\n    self.validator.validate @image_4x3, self.new_constraint max_ratio: 1.33\n    self.assert_no_violation\n  end\n\n  def test_min_ratio_uses_input_more_decimals : Nil\n    self.validator.validate @image_4x3, self.new_constraint min_ratio: 4 / 3\n    self.assert_no_violation\n  end\n\n  def test_max_ratio_uses_input_more_decimals : Nil\n    self.validator.validate @image_16x9, self.new_constraint min_ratio: 16 / 9\n    self.assert_no_violation\n  end\n\n  def test_square_not_allowed : Nil\n    self.validator.validate @image, self.new_constraint allow_square: false, allow_square_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::SQUARE_NOT_ALLOWED_ERROR)\n      .add_parameter(\"{{ width }}\", 2)\n      .add_parameter(\"{{ height }}\", 2)\n      .assert_violation\n  end\n\n  def test_landscape_not_allowed : Nil\n    self.validator.validate @image_landscape, self.new_constraint allow_landscape: false, allow_landscape_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::LANDSCAPE_NOT_ALLOWED_ERROR)\n      .add_parameter(\"{{ width }}\", 2)\n      .add_parameter(\"{{ height }}\", 1)\n      .assert_violation\n  end\n\n  def test_portrait_not_allowed : Nil\n    self.validator.validate @image_portrait, self.new_constraint allow_portrait: false, allow_portrait_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::PORTRAIT_NOT_ALLOWED_ERROR)\n      .add_parameter(\"{{ width }}\", 1)\n      .add_parameter(\"{{ height }}\", 2)\n      .assert_violation\n  end\n\n  def ptest_invalid_mime_narrowed_set : Nil\n    self.validator.validate @image, self.new_constraint mime_types: [\"image/jpeg\", \"image/png\"]\n\n    # TODO: Figure out a good way to make it so it doesn't actually translate the message of the actual violation.\n    # Possibly some internal `TranslatorInterface` implementation to support a future `Athena::Translator` component.\n    self\n      .build_violation(\"The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.\", CONSTRAINT::INVALID_MIME_TYPE_ERROR)\n      .add_parameter(\"{{ file }}\", @image)\n      .add_parameter(\"{{ type }}\", \"image/gif\")\n      .add_parameter(\"{{ types }}\", %(Set{\"image/jpeg\", \"image/png\"}))\n      .add_parameter(\"{{ name }}\", \"2x2.gif\")\n      .assert_violation\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/ip_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::IP\n\nstruct IPValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_empty_string_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n  end\n\n  @[DataProvider(\"valid_v4s\")]\n  def test_valid_v4s(value : String) : Nil\n    self.validator.validate value, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def valid_v4s : Tuple\n    {\n      {\"0.0.0.0\"},\n      {\"10.0.0.0\"},\n      {\"123.45.67.178\"},\n      {\"172.16.0.0\"},\n      {\"192.168.1.0\"},\n      {\"224.0.0.1\"},\n      {\"255.255.255.255\"},\n      {\"127.0.0.0\"},\n    }\n  end\n\n  @[DataProvider(\"valid_v6s\")]\n  def test_valid_v6s(value : String) : Nil\n    self.validator.validate value, self.new_constraint version: CONSTRAINT::Version::V6\n    self.assert_no_violation\n  end\n\n  def valid_v6s : Tuple\n    {\n      {\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"},\n      {\"2001:0DB8:85A3:0000:0000:8A2E:0370:7334\"},\n      {\"2001:0Db8:85a3:0000:0000:8A2e:0370:7334\"},\n      {\"fdfe:dcba:9876:ffff:fdc6:c46b:bb8f:7d4c\"},\n      {\"fdc6:c46b:bb8f:7d4c:fdc6:c46b:bb8f:7d4c\"},\n      {\"fdc6:c46b:bb8f:7d4c:0000:8a2e:0370:7334\"},\n      {\"fe80:0000:0000:0000:0202:b3ff:fe1e:8329\"},\n      {\"fe80:0:0:0:202:b3ff:fe1e:8329\"},\n      {\"fe80::202:b3ff:fe1e:8329\"},\n      {\"0:0:0:0:0:0:0:0\"},\n      {\"::\"},\n      {\"0::\"},\n      {\"::0\"},\n      {\"0::0\"},\n      {\"2001:0db8:85a3:0000:0000:8a2e:0.0.0.0\"}, # IPv4 mapped to IP\n      {\"::0.0.0.0\"},\n      {\"::255.255.255.255\"},\n      {\"::123.45.67.178\"},\n    }\n  end\n\n  @[DataProvider(\"valid_v4s_v6s\")]\n  def test_valid_v4s_v6s(value : String) : Nil\n    self.validator.validate value, self.new_constraint version: CONSTRAINT::Version::V4_V6\n    self.assert_no_violation\n  end\n\n  def valid_v4s_v6s : Tuple\n    self.valid_v4s + self.valid_v6s\n  end\n\n  @[DataProvider(\"invalid_v4s\")]\n  def test_invalid_v4s(value : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::INVALID_IP_ERROR, value\n  end\n\n  def invalid_v4s : Tuple\n    {\n      {\"0\"},\n      {\"0.0\"},\n      {\"0.0.0\"},\n      {\"256.0.0.0\"},\n      {\"0.256.0.0\"},\n      {\"0.0.256.0\"},\n      {\"0.0.0.256\"},\n      {\"-1.0.0.0\"},\n      {\"foobar\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_v6s\")]\n  def test_invalid_v6s(value : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\", version: CONSTRAINT::Version::V6\n    self.assert_violation \"my_message\", CONSTRAINT::INVALID_IP_ERROR, value\n  end\n\n  def invalid_v6s : Tuple\n    {\n      {\"z001:0db8:85a3:0000:0000:8a2e:0370:7334\"},\n      {\"fe80\"},\n      {\"fe80:8329\"},\n      {\"fe80:::202:b3ff:fe1e:8329\"},\n      {\"fe80::202:b3ff::fe1e:8329\"},\n      {\"2001:0db8:85a3:0000:0000:8a2e:0370:0.0.0.0\"}, # IPv4 mapped to IPv6\n      {\"::0.0\"},\n      {\"::0.0.0\"},\n      {\"::256.0.0.0\"},\n      {\"::0.256.0.0\"},\n      {\"::0.0.256.0\"},\n      {\"::0.0.0.256\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_v4s_v6s\")]\n  def test_invalid_v4s_v6s(value : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\", version: CONSTRAINT::Version::V4_V6\n    self.assert_violation \"my_message\", CONSTRAINT::INVALID_IP_ERROR, value\n  end\n\n  def invalid_v4s_v6s : Tuple\n    self.invalid_v4s + self.invalid_v6s\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/is_false_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::IsFalse\n\nstruct IsFalseValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_false_is_valid : Nil\n    self.validator.validate false, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_true_is_invalid : Nil\n    self.validator.validate true, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::NOT_FALSE_ERROR, true\n  end\n\n  def test_zero_is_invalid : Nil\n    self.validator.validate 0, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::NOT_FALSE_ERROR, 0\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/is_nil_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::IsNil\n\nstruct IsNilValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"invalid_values\")]\n  def test_invalid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::NOT_NIL_ERROR, value\n  end\n\n  def invalid_values : Tuple\n    {\n      {\"foobar\"},\n      {0},\n      {false},\n      {true},\n      {\"\"},\n      {Time.utc},\n      {[] of Int32},\n      {Pointer(Void).null},\n      {1234},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/is_true_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::IsTrue\n\nstruct IsTrueValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_true_is_valid : Nil\n    self.validator.validate true, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_false_is_invalid : Nil\n    self.validator.validate false, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::NOT_TRUE_ERROR, false\n  end\n\n  def test_one_is_invalid : Nil\n    self.validator.validate 1, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::NOT_TRUE_ERROR, 1\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/isbn_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::ISBN\n\nstruct ISBNValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_empty_string_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n  end\n\n  def test_message_is_used_if_set : Nil\n    self.validator.validate \"asdf\", self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::INVALID_CHARACTERS_ERROR, \"asdf\"\n  end\n\n  @[DataProvider(\"valid_isbn10s\")]\n  def test_valid_isbn10s(value : String) : Nil\n    self.validator.validate value, self.new_constraint type: CONSTRAINT::Type::ISBN10\n    self.assert_no_violation\n  end\n\n  def valid_isbn10s : Tuple\n    {\n      {\"2723442284\"},\n      {\"2723442276\"},\n      {\"2723455041\"},\n      {\"2070546810\"},\n      {\"2711858839\"},\n      {\"2756406767\"},\n      {\"2870971648\"},\n      {\"226623854X\"},\n      {\"2851806424\"},\n      {\"0321812700\"},\n      {\"0-45122-5244\"},\n      {\"0-4712-92311\"},\n      {\"0-9752298-0-X\"},\n    }\n  end\n\n  @[DataProvider(\"valid_isbn13s\")]\n  def test_valid_isbn13s(value : String) : Nil\n    self.validator.validate value, self.new_constraint type: CONSTRAINT::Type::ISBN13\n    self.assert_no_violation\n  end\n\n  def valid_isbn13s : Tuple\n    {\n      {\"978-2723442282\"},\n      {\"978-2723442275\"},\n      {\"978-2723455046\"},\n      {\"978-2070546817\"},\n      {\"978-2711858835\"},\n      {\"978-2756406763\"},\n      {\"978-2870971642\"},\n      {\"978-2266238540\"},\n      {\"978-2851806420\"},\n      {\"978-0321812704\"},\n      {\"978-0451225245\"},\n      {\"978-0471292319\"},\n    }\n  end\n\n  @[DataProvider(\"valid_isbns\")]\n  def test_valid_both(value : String) : Nil\n    self.validator.validate value, self.new_constraint type: CONSTRAINT::Type::Both\n    self.assert_no_violation\n  end\n\n  def valid_isbns : Tuple\n    self.valid_isbn10s + self.valid_isbn13s\n  end\n\n  @[DataProvider(\"invalid_isbn10s\")]\n  def test_invalid_isbn10s(value : String, code : String) : Nil\n    self.validator.validate value, self.new_constraint type: CONSTRAINT::Type::ISBN10, isbn10_message: \"my_message\"\n    self.assert_violation \"my_message\", code, value\n  end\n\n  def invalid_isbn10s : Tuple\n    {\n      {\"27234422841\", CONSTRAINT::TOO_LONG_ERROR},\n      {\"272344228\", CONSTRAINT::TOO_SHORT_ERROR},\n      {\"0-4712-9231\", CONSTRAINT::TOO_SHORT_ERROR},\n      {\"1234567890\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"0987656789\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"7-35622-5444\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"0-4X19-92611\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"0_45122_5244\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"2870#971#648\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"0-9752298-0-x\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"1A34567890\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"2#{1.chr}70546810\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n    }\n  end\n\n  @[DataProvider(\"invalid_isbn13s\")]\n  def test_invalid_isbn13s(value : String, code : String) : Nil\n    self.validator.validate value, self.new_constraint type: CONSTRAINT::Type::ISBN13, isbn13_message: \"my_message\"\n    self.assert_violation \"my_message\", code, value\n  end\n\n  def invalid_isbn13s : Tuple\n    {\n      {\"978-27234422821\", CONSTRAINT::TOO_LONG_ERROR},\n      {\"978-272344228\", CONSTRAINT::TOO_SHORT_ERROR},\n      {\"978-2723442-82\", CONSTRAINT::TOO_SHORT_ERROR},\n      {\"978-2723442281\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"978-0321513774\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"979-0431225385\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"980-0474292319\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"0-4X19-92619812\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"978_2723442282\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"978#2723442282\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"978-272C442282\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"978-2#{1.chr}70546817\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n    }\n  end\n\n  @[DataProvider(\"invalid_isbn10s\")]\n  def test_invalid_both_isbn10s(value : String, code : String) : Nil\n    self.validator.validate value, self.new_constraint type: CONSTRAINT::Type::Both, both_message: \"my_message\"\n\n    # Too long for ISBN-10, but not long enough for ISBN-13\n    if CONSTRAINT::TOO_LONG_ERROR == code\n      code = CONSTRAINT::TYPE_NOT_RECOGNIZED_ERROR\n    end\n\n    self.assert_violation \"my_message\", code, value\n  end\n\n  @[DataProvider(\"invalid_isbn13s\")]\n  def test_invalid_both_isbn13s(value : String, code : String) : Nil\n    self.validator.validate value, self.new_constraint type: CONSTRAINT::Type::Both, both_message: \"my_message\"\n\n    # Too short for an ISBN-13, but not short enough for an ISBN-10\n    if CONSTRAINT::TOO_SHORT_ERROR == code\n      code = CONSTRAINT::TYPE_NOT_RECOGNIZED_ERROR\n    end\n\n    self.assert_violation \"my_message\", code, value\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/isin_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::ISIN\n\nstruct ISINValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_empty_string_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n  end\n\n  @[DataProvider(\"valid_isins\")]\n  def test_valid_isins(value : String) : Nil\n    self.validator.validate value, self.new_constraint\n    self.expect_violation_at 0, value, AVD::Constraints::Luhn.new\n    self.assert_no_violation\n  end\n\n  def valid_isins : Tuple\n    {\n      {\"XS2125535901\"}, # Goldman Sachs International\n      {\"DE0005140008\"}, # Deutsche Bankg AG\n      {\"CH0528261156\"}, # Leonteq Securities AG [Guernsey]\n      {\"US0378331005\"}, # Apple, Inc.\n      {\"AU0000XVGZA3\"}, # TREASURY CORP VICTORIA 5 3/4% 2005-2016\n      {\"GB0002634946\"}, # BAE Systems\n      {\"CH0528261099\"}, # Leonteq Securities AG [Guernsey]\n      {\"XS2155672814\"}, # OP Corporate Bank plc\n      {\"XS2155687259\"}, # Orbian Financial Services III, LLC\n      {\"XS2155696672\"}, # Sheffield Receivables Company LLC\n    }\n  end\n\n  @[DataProvider(\"invalid_length_isins\")]\n  def test_invalid_length_isins(value : String) : Nil\n    self.assert_violation value, CONSTRAINT::INVALID_LENGTH_ERROR\n  end\n\n  def invalid_length_isins : Tuple\n    {\n      {\"X\"},\n      {\"XS\"},\n      {\"XS2\"},\n      {\"XS21\"},\n      {\"XS215\"},\n      {\"XS2155\"},\n      {\"XS21556\"},\n      {\"XS215569\"},\n      {\"XS2155696\"},\n      {\"XS21556966\"},\n      {\"XS215569667\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_pattern_isins\")]\n  def test_invalid_pattern_isins(value : String) : Nil\n    self.assert_violation value, CONSTRAINT::INVALID_PATTERN_ERROR\n  end\n\n  def invalid_pattern_isins : Tuple\n    {\n      {\"X12155696679\"},\n      {\"123456789101\"},\n      {\"XS215569667E\"},\n      {\"XS215E69667A\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_checksum_isins\")]\n  def test_invalid_checksum_isins(value : String) : Nil\n    self.expect_violation_at 0, value, AVD::Constraints::Luhn.new\n    self.assert_violation value, CONSTRAINT::INVALID_CHECKSUM_ERROR\n  end\n\n  def invalid_checksum_isins : Tuple\n    {\n      {\"XS2112212144\"},\n      {\"DE013228VA77\"},\n      {\"CH0512361156\"},\n      {\"XS2125660123\"},\n      {\"XS2012587408\"},\n      {\"XS2012380102\"},\n      {\"XS2012239364\"},\n    }\n  end\n\n  private def assert_violation(isin : String, code : String) : Nil\n    self.validator.validate isin, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", code, isin\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/issn_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::ISSN\n\nstruct ISSNValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_empty_string_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n  end\n\n  @[DataProvider(\"valid_lowercase_issns\")]\n  def test_case_sensitive_issns(value : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\", case_sensitive: true\n    self.assert_violation \"my_message\", CONSTRAINT::INVALID_CASE_ERROR, value\n  end\n\n  def valid_lowercase_issns : Tuple\n    {\n      {\"2162-321x\"},\n      {\"2160-200x\"},\n      {\"1537-453x\"},\n      {\"1937-710x\"},\n      {\"0002-922x\"},\n      {\"1553-345x\"},\n      {\"1553-619x\"},\n    }\n  end\n\n  @[DataProvider(\"valid_non_hyphenated_issns\")]\n  def test_hyphen_required_issns(value : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\", require_hyphen: true\n    self.assert_violation \"my_message\", CONSTRAINT::MISSING_HYPHEN_ERROR, value\n  end\n\n  def valid_non_hyphenated_issns : Tuple\n    {\n      {\"2162321X\"},\n      {\"01896016\"},\n      {\"15744647\"},\n      {\"14350645\"},\n      {\"07174055\"},\n      {\"20905076\"},\n      {\"14401592\"},\n    }\n  end\n\n  def valid_full_issns : Tuple\n    {\n      {\"1550-7416\"},\n      {\"1539-8560\"},\n      {\"2156-5376\"},\n      {\"1119-023X\"},\n      {\"1684-5315\"},\n      {\"1996-0786\"},\n      {\"1684-5374\"},\n      {\"1996-0794\"},\n    }\n  end\n\n  @[DataProvider(\"valid_issns\")]\n  def test_valid_issns(value : String) : Nil\n    self.validator.validate value, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def valid_issns : Tuple\n    self.valid_lowercase_issns + self.valid_non_hyphenated_issns + self.valid_full_issns\n  end\n\n  def invalid_issns : Tuple\n    {\n      {0, CONSTRAINT::TOO_SHORT_ERROR},\n      {\"1539\", CONSTRAINT::TOO_SHORT_ERROR},\n      {\"2156-537A\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"1119-0231\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"1684-5312\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"1996-0783\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"1684-537X\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"1996-0795\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n    }\n  end\n\n  @[DataProvider(\"invalid_issns\")]\n  def test_invalid_issns(value : String | Number, code : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", code, value\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    AVD::Constraints::ISSN::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/length_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Length\n\nstruct LengthValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint(range: (1..1), exact_message: \"my_message\")\n\n    self.assert_no_violation\n  end\n\n  def three_or_less : Tuple\n    {\n      {12, 2},\n      {\"12\", 2},\n      {\"üü\", 2},\n      {\"éé\", 2},\n      {123, 3},\n      {\"123\", 3},\n      {\"üüü\", 3},\n      {\"ééé\", 3},\n    }\n  end\n\n  def four : Tuple\n    {\n      {1234},\n      {\"1234\"},\n      {\"üüüü\"},\n      {\"éééé\"},\n    }\n  end\n\n  def five_or_more : Tuple\n    {\n      {12345, 5},\n      {\"12345\", 5},\n      {\"üüüüü\", 5},\n      {\"ééééé\", 5},\n      {123_456, 6},\n      {\"123456\", 6},\n      {\"üüüüüü\", 6},\n      {\"éééééé\", 6},\n    }\n  end\n\n  @[DataProvider(\"five_or_more\")]\n  def test_valid_values_min(value : Int32 | String, value_length : Int32) : Nil\n    self.validator.validate value, self.new_constraint range: (5..)\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"three_or_less\")]\n  def test_valid_values_max(value : Int32 | String, value_length : Int32) : Nil\n    self.validator.validate value, self.new_constraint range: (..3)\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"four\")]\n  def test_valid_values_exact(value : Int32 | String) : Nil\n    self.validator.validate value, self.new_constraint range: (4..4)\n    self.assert_no_violation\n  end\n\n  def test_valid_graphemes_values : Nil\n    self.validator.validate \"A\\u{0300}\", self.new_constraint range: (1..1), unit: CONSTRAINT::Unit::GRAPHEMES\n    self.assert_no_violation\n  end\n\n  def test_valid_codepoints_values : Nil\n    self.validator.validate \"A\\u{0300}\", self.new_constraint range: (2..2), unit: CONSTRAINT::Unit::CODEPOINTS\n    self.assert_no_violation\n  end\n\n  def test_valid_bytes_values : Nil\n    self.validator.validate \"A\\u{0300}\", self.new_constraint range: (3..3), unit: CONSTRAINT::Unit::BYTES\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"three_or_less\")]\n  def test_invalid_values_min(value : Int32 | String, value_length : Int32) : Nil\n    self.validator.validate value, self.new_constraint range: (4..), min_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_SHORT_ERROR, value)\n      .add_parameter(\"{{ value }}\", value.to_s)\n      .add_parameter(\"{{ limit }}\", 4)\n      .add_parameter(\"{{ min }}\", 4)\n      .add_parameter(\"{{ value_length }}\", value_length)\n      .plural(4)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  @[DataProvider(\"five_or_more\")]\n  def test_invalid_values_max(value : Int32 | String, value_length : Int32) : Nil\n    self.validator.validate value, self.new_constraint range: (..4), max_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_LONG_ERROR, value)\n      .add_parameter(\"{{ value }}\", value.to_s)\n      .add_parameter(\"{{ limit }}\", 4)\n      .add_parameter(\"{{ max }}\", 4)\n      .add_parameter(\"{{ value_length }}\", value_length)\n      .plural(4)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  @[DataProvider(\"three_or_less\")]\n  def test_invalid_values_exact_less_than_four(value : Int32 | String, value_length : Int32) : Nil\n    self.validator.validate value, self.new_constraint range: (4..4), exact_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_EQUAL_LENGTH_ERROR, value)\n      .add_parameter(\"{{ value }}\", value.to_s)\n      .add_parameter(\"{{ limit }}\", 4)\n      .add_parameter(\"{{ min }}\", 4)\n      .add_parameter(\"{{ max }}\", 4)\n      .add_parameter(\"{{ value_length }}\", value_length)\n      .plural(4)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  @[DataProvider(\"five_or_more\")]\n  def test_invalid_values_exact_more_than_four(value : Int32 | String, value_length : Int32) : Nil\n    self.validator.validate value, self.new_constraint range: (4..4), exact_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_EQUAL_LENGTH_ERROR, value)\n      .add_parameter(\"{{ value }}\", value.to_s)\n      .add_parameter(\"{{ limit }}\", 4)\n      .add_parameter(\"{{ min }}\", 4)\n      .add_parameter(\"{{ max }}\", 4)\n      .add_parameter(\"{{ value_length }}\", value_length)\n      .plural(4)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  def test_invalid_values_exact_default_unit_with_grapheme_input : Nil\n    self.validator.validate value = \"A\\u{0300}\", self.new_constraint range: (1..1), exact_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_EQUAL_LENGTH_ERROR, value)\n      .add_parameter(\"{{ value }}\", value)\n      .add_parameter(\"{{ limit }}\", 1)\n      .add_parameter(\"{{ min }}\", 1)\n      .add_parameter(\"{{ max }}\", 1)\n      .add_parameter(\"{{ value_length }}\", 2)\n      .plural(1)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  def test_invalid_values_exact_bytes_unit_with_grapheme_input : Nil\n    self.validator.validate value = \"A\\u{0300}\", self.new_constraint range: (1..1), exact_message: \"my_message\", unit: CONSTRAINT::Unit::BYTES\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_EQUAL_LENGTH_ERROR, value)\n      .add_parameter(\"{{ value }}\", value)\n      .add_parameter(\"{{ limit }}\", 1)\n      .add_parameter(\"{{ min }}\", 1)\n      .add_parameter(\"{{ max }}\", 1)\n      .add_parameter(\"{{ value_length }}\", 3)\n      .plural(1)\n      .invalid_value(value)\n      .assert_violation\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/less_than_or_equal_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::LessThanOrEqual\n\nstruct LessThanOrEqualValidatorTest < AVD::Spec::ComparisonConstraintValidatorTestCase\n  def valid_comparisons : Tuple\n    {\n      {2, 3},\n      {0, 0_u8},\n      {\"a\", \"b\"},\n      {\"22\", \"22\"},\n      {Time.utc(2020, 4, 6), Time.utc(2020, 4, 7)},\n      {nil, false},\n    }\n  end\n\n  def invalid_comparisons : Tuple\n    {\n      {3, 2},\n      {\"333\", \"22\"},\n      {Time.utc(2020, 4, 8), Time.utc(2020, 4, 7)},\n    }\n  end\n\n  def test_invalid_type : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Expected argument of type 'Number | String | Time', 'Bool' given.\" do\n      self.validator.validate false, new_constraint value: 50\n    end\n  end\n\n  def error_code : String\n    CONSTRAINT::TOO_HIGH_ERROR\n  end\n\n  def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/less_than_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::LessThan\n\nstruct LessThanValidatorTest < AVD::Spec::ComparisonConstraintValidatorTestCase\n  def valid_comparisons : Tuple\n    {\n      {2, 3},\n      {\"a\", \"b\"},\n      {Time.utc(2020, 4, 6), Time.utc(2020, 4, 7)},\n      {nil, false},\n    }\n  end\n\n  def invalid_comparisons : Tuple\n    {\n      {3, 2},\n      {3, 3},\n      {\"333\", \"22\"},\n      {Time.utc(2020, 4, 8), Time.utc(2020, 4, 7)},\n    }\n  end\n\n  def test_invalid_type : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Expected argument of type 'Number | String | Time', 'Bool' given.\" do\n      self.validator.validate false, new_constraint value: 50\n    end\n  end\n\n  def error_code : String\n    CONSTRAINT::TOO_HIGH_ERROR\n  end\n\n  def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/luhn_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Luhn\n\nstruct LuhnValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_empty_string_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n  end\n\n  @[DataProvider(\"valid_numbers\")]\n  def test_valid_numbers(value : String) : Nil\n    self.validator.validate value, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def valid_numbers : Tuple\n    {\n      {\"42424242424242424242\"},\n      {\"378282246310005\"},\n      {\"371449635398431\"},\n      {\"378734493671000\"},\n      {\"5610591081018250\"},\n      {\"30569309025904\"},\n      {\"38520000023237\"},\n      {\"6011111111111117\"},\n      {\"6011000990139424\"},\n      {\"3530111333300000\"},\n      {\"3566002020360505\"},\n      {\"5555555555554444\"},\n      {\"5105105105105100\"},\n      {\"4111111111111111\"},\n      {\"4012888888881881\"},\n      {\"4222222222222\"},\n      {\"5019717010103742\"},\n      {\"6331101999990016\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_numbers\")]\n  def test_invalid_numbers(value : String, code : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", code, value\n  end\n\n  def invalid_numbers : Tuple\n    {\n      {\"1234567812345678\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"4222222222222222\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"0000000000000000\", CONSTRAINT::CHECKSUM_FAILED_ERROR},\n      {\"000000!000000000\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n      {\"42-22222222222222\", CONSTRAINT::INVALID_CHARACTERS_ERROR},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/negative_or_zero_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::NegativeOrZero\n\nstruct NegativeOrZeroValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_zero_is_valid : Nil\n    self.validator.validate 0, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_valid_value : Nil\n    self.validator.validate -1, self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"invalid_values\")]\n  def test_invalid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_HIGH_ERROR, value)\n      .add_parameter(\"{{ compared_value }}\", \"0\")\n      .add_parameter(\"{{ compared_value_type }}\", \"Int32\")\n      .assert_violation\n  end\n\n  def invalid_values : Tuple\n    {\n      {1},\n      {1234},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/negative_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Negative\n\nstruct NegativeValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_valid_value : Nil\n    self.validator.validate -1, self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"invalid_values\")]\n  def test_invalid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_HIGH_ERROR, value)\n      .add_parameter(\"{{ compared_value }}\", \"0\")\n      .add_parameter(\"{{ compared_value_type }}\", \"Int32\")\n      .assert_violation\n  end\n\n  def invalid_values : Tuple\n    {\n      {0},\n      {1},\n      {1234},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/not_blank_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::NotBlank\n\nstruct NotBlankValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  @[DataProvider(\"valid_values\")]\n  def test_valid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def valid_values : NamedTuple\n    {\n      string: {\"foo\"},\n      array:  {[1, 2, 3]},\n      bool:   {true},\n    }\n  end\n\n  def test_blank_is_invalid\n    self.validator.validate \"\", self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::IS_BLANK_ERROR, \"\"\n  end\n\n  def test_false_is_invalid\n    self.validator.validate false, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::IS_BLANK_ERROR, false\n  end\n\n  def test_empty_array_is_invalid\n    self.validator.validate [] of String, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::IS_BLANK_ERROR, [] of String\n  end\n\n  def test_allow_nil_true\n    self.validator.validate nil, self.new_constraint message: \"my_message\", allow_nil: true\n    self.assert_no_violation\n  end\n\n  def test_allow_nil_false\n    self.validator.validate nil, self.new_constraint message: \"my_message\", allow_nil: false\n    self.assert_violation \"my_message\", CONSTRAINT::IS_BLANK_ERROR, nil\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/not_equal_to_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::NotEqualTo\n\nstruct NotEqualToValidatorTest < AVD::Spec::ComparisonConstraintValidatorTestCase\n  def valid_comparisons : Tuple\n    {\n      {1, 2},\n      {'b', 'a'},\n      {\"b\", \"a\"},\n      {Time.utc(2020, 4, 8), Time.utc(2020, 4, 7)},\n      {nil, false},\n\n    }\n  end\n\n  def invalid_comparisons : Tuple\n    {\n      {3, 3},\n      {'a', 'a'},\n      {\"a\", \"a\"},\n      {Time.utc(2020, 4, 7), Time.utc(2020, 4, 7)},\n    }\n  end\n\n  def error_code : String\n    CONSTRAINT::IS_EQUAL_ERROR\n  end\n\n  def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/not_nil_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::NotNil\n\nstruct NotNilValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  @[DataProvider(\"valid_values\")]\n  def test_valid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def valid_values : Tuple\n    {\n      {\"\"},\n      {false},\n      {true},\n      {0},\n      {Pointer(Void).null},\n    }\n  end\n\n  def test_nil_is_invalid\n    self.validator.validate nil, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::IS_NIL_ERROR, nil\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/positive_or_zero_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::PositiveOrZero\n\nstruct PositiveOrZeroValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_zero_is_valid : Nil\n    self.validator.validate 0, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_valid_value : Nil\n    self.validator.validate 1, self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"invalid_values\")]\n  def test_invalid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_LOW_ERROR, value)\n      .add_parameter(\"{{ compared_value }}\", \"0\")\n      .add_parameter(\"{{ compared_value_type }}\", \"Int32\")\n      .assert_violation\n  end\n\n  def invalid_values : Tuple\n    {\n      {-1},\n      {-1234},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/positive_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Positive\n\nstruct PositiveValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_valid_value : Nil\n    self.validator.validate 1, self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"invalid_values\")]\n  def test_invalid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_LOW_ERROR, value)\n      .add_parameter(\"{{ compared_value }}\", \"0\")\n      .add_parameter(\"{{ compared_value_type }}\", \"Int32\")\n      .assert_violation\n  end\n\n  def invalid_values : Tuple\n    {\n      {0},\n      {-1},\n      {-1234},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/range_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Range\n\nstruct RangeValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint range: 0..10\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"ten_to_twenty\")]\n  def test_valid_values_min(value : Number?) : Nil\n    self.validator.validate value, self.new_constraint range: (10..)\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"ten_to_twenty\")]\n  def test_valid_values_max(value : Number?) : Nil\n    self.validator.validate value, self.new_constraint range: (..20)\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"ten_to_twenty\")]\n  def test_valid_values_minmax(value : Number?) : Nil\n    self.validator.validate value, self.new_constraint range: (10..20)\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"less_than_ten\")]\n  def test_invalid_values_min(value : Number?) : Nil\n    self.validator.validate value, self.new_constraint range: (10..), min_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_LOW_ERROR, value)\n      .add_parameter(\"{{ limit }}\", 10)\n      .assert_violation\n  end\n\n  @[DataProvider(\"more_than_twenty\")]\n  def test_invalid_values_max(value : Number?) : Nil\n    self.validator.validate value, self.new_constraint range: (..20), max_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_HIGH_ERROR, value)\n      .add_parameter(\"{{ limit }}\", 20)\n      .assert_violation\n  end\n\n  @[DataProvider(\"more_than_twenty\")]\n  def test_invalid_values_minmax_max(value : Number?) : Nil\n    self.validator.validate value, self.new_constraint range: (10..20), not_in_range_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_IN_RANGE_ERROR, value)\n      .add_parameter(\"{{ min }}\", 10)\n      .add_parameter(\"{{ max }}\", 20)\n      .assert_violation\n  end\n\n  @[DataProvider(\"less_than_ten\")]\n  def test_invalid_values_minmax_min(value : Number?) : Nil\n    self.validator.validate value, self.new_constraint range: (10..20), not_in_range_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_IN_RANGE_ERROR, value)\n      .add_parameter(\"{{ min }}\", 10)\n      .add_parameter(\"{{ max }}\", 20)\n      .assert_violation\n  end\n\n  def test_exclusive_range_included : Nil\n    self.validator.validate 15, self.new_constraint range: (10...20)\n    self.assert_no_violation\n  end\n\n  def test_exclusive_range_excluded : Nil\n    self.validator.validate 20, self.new_constraint range: (10...20), not_in_range_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_IN_RANGE_ERROR, 20)\n      .add_parameter(\"{{ min }}\", 10)\n      .add_parameter(\"{{ max }}\", 19)\n      .assert_violation\n  end\n\n  @[DataProvider(\"ten_to_twnentieth_april_2020\")]\n  def test_valid_datetimes_min(value : Time) : Nil\n    self.validator.validate value, self.new_constraint range: (Time.utc(2020, 4, 10)..)\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"ten_to_twnentieth_april_2020\")]\n  def test_valid_datetimes_max(value : Time) : Nil\n    self.validator.validate value, self.new_constraint range: (..Time.utc(2020, 4, 20))\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"ten_to_twnentieth_april_2020\")]\n  def test_valid_datetimes_range(value : Time) : Nil\n    self.validator.validate value, self.new_constraint range: (Time.utc(2020, 4, 10)..Time.utc(2020, 4, 20))\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"before_tenth_april_2020\")]\n  def test_invalid_datetimes_min(value : Time) : Nil\n    expected_start_date = Time.utc(2020, 4, 10)\n\n    self.validator.validate value, self.new_constraint range: (expected_start_date..), min_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_LOW_ERROR, value)\n      .add_parameter(\"{{ limit }}\", expected_start_date)\n      .assert_violation\n  end\n\n  @[DataProvider(\"after_twentieth_april_2020\")]\n  def test_invalid_datetimes_max(value : Time) : Nil\n    expected_end_date = Time.utc(2020, 4, 20)\n\n    self.validator.validate value, self.new_constraint range: (..expected_end_date), max_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::TOO_HIGH_ERROR, value)\n      .add_parameter(\"{{ limit }}\", expected_end_date)\n      .assert_violation\n  end\n\n  @[DataProvider(\"after_twentieth_april_2020\")]\n  def test_invalid_datetimes_minmax_max(value : Time) : Nil\n    expected_begin_date = Time.utc(2020, 4, 10)\n    expected_end_date = Time.utc(2020, 4, 20)\n\n    self.validator.validate value, self.new_constraint range: (expected_begin_date..expected_end_date), not_in_range_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_IN_RANGE_ERROR, value)\n      .add_parameter(\"{{ min }}\", expected_begin_date)\n      .add_parameter(\"{{ max }}\", expected_end_date)\n      .assert_violation\n  end\n\n  @[DataProvider(\"before_tenth_april_2020\")]\n  def test_invalid_datetimes_minmax_min(value : Time) : Nil\n    expected_begin_date = Time.utc(2020, 4, 10)\n    expected_end_date = Time.utc(2020, 4, 20)\n\n    self.validator.validate value, self.new_constraint range: (expected_begin_date..expected_end_date), not_in_range_message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::NOT_IN_RANGE_ERROR, value)\n      .add_parameter(\"{{ min }}\", expected_begin_date)\n      .add_parameter(\"{{ max }}\", expected_end_date)\n      .assert_violation\n  end\n\n  def test_invalid_type : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Expected argument of type 'Number | Time', 'Bool' given.\" do\n      self.validator.validate false, self.new_constraint range: (10..20)\n    end\n  end\n\n  def ten_to_twenty : Tuple\n    {\n      {10.000_01},\n      {19.999_99},\n      {10},\n      {20},\n      {20_i64},\n      {10.0},\n      {10.0_f32},\n      {20.0},\n      {nil},\n    }\n  end\n\n  def less_than_ten : Tuple\n    {\n      {9.999_99},\n      {5},\n      {1.0},\n    }\n  end\n\n  def more_than_twenty : Tuple\n    {\n      {20.000_001},\n      {21},\n      {30.0},\n    }\n  end\n\n  def ten_to_twnentieth_april_2020 : Tuple\n    {\n      {Time.utc(2020, 4, 10)},\n      {Time.utc(2020, 4, 15)},\n      {Time.utc(2020, 4, 20)},\n    }\n  end\n\n  def before_tenth_april_2020 : Tuple\n    {\n      {Time.utc(2019, 4, 20)},\n      {Time.utc(2020, 4, 9)},\n    }\n  end\n\n  def after_twentieth_april_2020 : Tuple\n    {\n      {Time.utc(2020, 4, 21)},\n      {Time.utc(2021, 4, 9)},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/regex_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Regex\n\nprivate class Stringifiable\n  def initialize(@value : String); end\n\n  def to_s(io : IO) : Nil\n    io << @value\n  end\nend\n\nstruct RegexValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint pattern: /^[0-9]+$/\n    self.assert_no_violation\n  end\n\n  def test_empty_string_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint pattern: /^[0-9]+$/\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"valid_values\")]\n  def test_valid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint pattern: /^[0-9]+$/\n    self.assert_no_violation\n  end\n\n  def valid_values : Tuple\n    {\n      {0},\n      {\"0\"},\n      {909_090},\n      {\"0909090\"},\n      {Stringifiable.new(\"909090\")},\n    }\n  end\n\n  @[DataProvider(\"invalid_values\")]\n  def test_invalid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint pattern: /^[0-9]+$/, message: \"my_message\"\n\n    self\n      .build_violation(\"my_message\", CONSTRAINT::REGEX_FAILED_ERROR, value)\n      .add_parameter(\"{{ pattern }}\", /^[0-9]+$/)\n      .assert_violation\n  end\n\n  def invalid_values : Tuple\n    {\n      {\"abcd\"},\n      {\"090foo\"},\n      {Stringifiable.new(\"abcd\")},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/sequentially_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Sequentially\n\nstruct SequentiallyValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_walk_though_constraints : Nil\n    self.validator.validate 6, self.new_constraint constraints: [AVD::Constraints::Range.new(4..), AVD::Constraints::Positive.new]\n    self.assert_no_violation\n  end\n\n  def ptest_stop_at_first_constraint_with_violation : Nil\n    self.validator.validate nil, self.new_constraint constraints: [AVD::Constraints::NotBlank.new, AVD::Constraints::NotNil.new]\n\n    # TODO: Determine how to test this given it depends on an actual validator instance\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/unique_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::Unique\n\nprivate record Foo\n\nstruct UniqueValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  @[DataProvider(\"valid_values\")]\n  def test_valid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"invalid_values\")]\n  def test_invalid_values(value : _) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\"\n    self.assert_violation \"my_message\", CONSTRAINT::IS_NOT_UNIQUE_ERROR, value\n  end\n\n  def test_invalid_type : Nil\n    expect_raises AVD::Exception::UnexpectedValueError, \"Expected argument of type 'Indexable', 'Int32' given.\" do\n      self.validator.validate 123, new_constraint message: \"my_message\"\n    end\n  end\n\n  def valid_values : NamedTuple\n    {\n      nil:             {nil},\n      empty_array:     {[] of Int32},\n      single_nil:      {[nil]},\n      single_integer:  {[1]},\n      single_string:   {[\"foo\"]},\n      single_object:   {[Foo.new]},\n      single_tuple:    { {1} },\n      unique_booleans: {[true, false]},\n      unique_integers: {[1, 2, 3, 4, 5, 6]},\n      unique_floats:   {[1.0, 2.0, 3.0]},\n      unique_strings:  {[\"a\", \"b\", \"c\"]},\n      unique_arrays:   {[[1, 2], [2, 4], [4, 6]]},\n      unique_tuples:   { { {1, 2}, {2, 4}, {4, 6} } },\n      unique_mixed:    {[\"a\", true, 10.0, 7_u8]},\n      unique_dequeue:  {Deque{1, 4, 9}},\n    }\n  end\n\n  def invalid_values : NamedTuple\n    object = Foo.new\n\n    {\n      not_unique_nil:      {[nil, nil]},\n      not_unique_booleans: {[true, true]},\n      not_unique_integers: {[1, 2, 2, 3]},\n      not_unique_floats:   {[0.1, 0.2, 0.1]},\n      not_unique_strings:  {[\"a\", \"a\"]},\n      not_unique_arrays:   {[[1, 1], [2, 3], [1, 1]]},\n      not_unique_objects:  {[object, object]},\n      not_unique_tuples:   { { {1, 1}, {2, 3}, {1, 1} } },\n      not_unique_mixed:    {[\"a\", true, 10.0, 7_u8, \"a\"]},\n      not_unique_dequeue:  {Deque{1, 5, 1}},\n    }\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/url_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate alias CONSTRAINT = AVD::Constraints::URL\n\nprivate class EmptyURLObject\n  def to_s(io : IO) : Nil\n    io << \"\"\n  end\nend\n\nstruct URLValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n  def test_nil_is_valid : Nil\n    self.validator.validate nil, self.new_constraint\n    self.assert_no_violation\n  end\n\n  def test_empty_string_is_valid : Nil\n    self.validator.validate \"\", self.new_constraint\n  end\n\n  def test_empty_string_from_object_is_valid : Nil\n    self.validator.validate EmptyURLObject.new, self.new_constraint\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"valid_urls\")]\n  def test_valid_urls(value : String) : Nil\n    self.validator.validate value, self.new_constraint require_tld: false\n    self.assert_no_violation\n  end\n\n  @[DataProvider(\"valid_urls\")]\n  @[DataProvider(\"valid_relative_urls\")]\n  def test_valid_relative_urls(value : String) : Nil\n    self.validator.validate value, self.new_constraint relative_protocol: true, require_tld: false\n    self.assert_no_violation\n  end\n\n  def valid_urls : Tuple\n    {\n      {\"http://a.pl\"},\n      {\"http://www.example.com\"},\n      {\"http://www.example.com.\"},\n      {\"http://www.example.museum\"},\n      {\"https://example.com/\"},\n      {\"https://example.com:80/\"},\n      {\"http://examp_le.com\"},\n      {\"http://www.sub_domain.examp_le.com\"},\n      {\"http://www.example.coop/\"},\n      {\"http://www.test-example.com/\"},\n      {\"http://www.crystal-lang.org/\"},\n      {\"http://crystal.fake/blog/\"},\n      {\"http://crystal-lang.org/?\"},\n      {\"http://crystal-lang.org/search?type=&q=url+validator\"},\n      {\"http://crystal-lang.org/#\"},\n      {\"http://crystal-lang.org/#?\"},\n      {\"http://crystal-lang.org/reference/getting_started/http_server.html#http-server\"},\n      {\"http://very.long.domain.name.com/\"},\n      {\"http://localhost/\"},\n      {\"http://myhost123/\"},\n      {\"http://127.0.0.1/\"},\n      {\"http://127.0.0.1:80/\"},\n      {\"http://[::1]/\"},\n      {\"http://[::1]:80/\"},\n      {\"http://[1:2:3::4:5:6:7]/\"},\n      {\"http://sãopaulo.com/\"},\n      {\"http://xn--sopaulo-xwa.com/\"},\n      {\"http://sãopaulo.com.br/\"},\n      {\"http://xn--sopaulo-xwa.com.br/\"},\n      {\"http://пример.испытание/\"},\n      {\"http://xn--e1afmkfd.xn--80akhbyknj4f/\"},\n      {\"http://مثال.إختبار/\"},\n      {\"http://xn--mgbh0fb.xn--kgbechtv/\"},\n      {\"http://例子.测试/\"},\n      {\"http://xn--fsqu00a.xn--0zwm56d/\"},\n      {\"http://例子.測試/\"},\n      {\"http://xn--fsqu00a.xn--g6w251d/\"},\n      {\"http://例え.テスト/\"},\n      {\"http://xn--r8jz45g.xn--zckzah/\"},\n      {\"http://مثال.آزمایشی/\"},\n      {\"http://xn--mgbh0fb.xn--hgbk6aj7f53bba/\"},\n      {\"http://실례.테스트/\"},\n      {\"http://xn--9n2bp8q.xn--9t4b11yi5a/\"},\n      {\"http://العربية.idn.icann.org/\"},\n      {\"http://xn--ogb.idn.icann.org/\"},\n      {\"http://xn--e1afmkfd.xn--80akhbyknj4f.xn--e1afmkfd/\"},\n      {\"http://xn--espaa-rta.xn--ca-ol-fsay5a/\"},\n      {\"http://xn--d1abbgf6aiiy.xn--p1ai/\"},\n      {\"http://☎.com/\"},\n      {\"http://username:password@crystal-lang.org\"},\n      {\"http://user.name:password@crystal-lang.org\"},\n      {\"http://user_name:pass_word@crystal-lang.org\"},\n      {\"http://username:pass.word@crystal-lang.org\"},\n      {\"http://user.name:pass.word@crystal-lang.org\"},\n      {\"http://user-name@crystal-lang.org\"},\n      {\"http://user_name@crystal-lang.org\"},\n      {\"http://u%24er:password@crystal-lang.org\"},\n      {\"http://user:pa%24%24word@crystal-lang.org\"},\n      {\"http://crystal-lang.org?\"},\n      {\"http://crystal-lang.org?query=1\"},\n      {\"http://crystal-lang.org/?query=1\"},\n      {\"http://crystal-lang.org#\"},\n      {\"http://crystal-lang.org#fragment\"},\n      {\"http://crystal-lang.org/#fragment\"},\n      {\"http://crystal-lang.org/#one_more%20test\"},\n      {\"http://example.com/exploit.html?hello[0]=test\"},\n    }\n  end\n\n  def valid_relative_urls : Tuple\n    {\n      {\"//example.com\"},\n      {\"//examp_le.com\"},\n      {\"//example.fake/blog/\"},\n      {\"//example.com/search?type=&q=url+validator\"},\n    }\n  end\n\n  @[DataProvider(\"invalid_urls\")]\n  def test_invalid_urls(value : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\", require_tld: false\n    self.assert_violation \"my_message\", CONSTRAINT::INVALID_URL_ERROR, value\n  end\n\n  @[DataProvider(\"invalid_urls\")]\n  @[DataProvider(\"invalid_relative_urls\")]\n  def test_invalid_relative_urls(value : String) : Nil\n    self.validator.validate value, self.new_constraint message: \"my_message\", relative_protocol: true, require_tld: false\n    self.assert_violation \"my_message\", CONSTRAINT::INVALID_URL_ERROR, value\n  end\n\n  def invalid_urls : Tuple\n    {\n      {\"google.com\"},\n      {\"://google.com\"},\n      {\"http ://google.com\"},\n      {\"http:/google.com\"},\n      {\"http://google.com::aa\"},\n      {\"http://google.com:aa\"},\n      {\"ftp://google.fr\"},\n      {\"faked://google.fr\"},\n      {\"http://127.0.0.1:aa/\"},\n      {\"ftp://[::1]/\"},\n      {\"http://[::1\"},\n      {\"http://hello.☎/\"},\n      {\"http://:password@example.com\"},\n      {\"http://:password@@example.com\"},\n      {\"http://username:passwordexample.com\"},\n      {\"http://usern@me:password@example.com\"},\n      {\"http://nota%hex:password@example.com\"},\n      {\"http://example.com/exploit.html?<script>alert(1);</script>\"},\n      {\"http://example.com/exploit.html?hel lo\"},\n      {\"http://example.com/exploit.html?not_a%hex\"},\n      {\"http://\"},\n    }\n  end\n\n  def invalid_relative_urls : Tuple\n    {\n      {\"/google.com\"},\n      {\"//google.com::aa\"},\n      {\"//google.com:aa\"},\n      {\"//127.0.0.1:aa/\"},\n      {\"//[::1\"},\n      {\"//hello.☎/\"},\n      {\"//:password@example.com\"},\n      {\"//:password@@example.com\"},\n      {\"//username:passwordexample.com\"},\n      {\"//usern@me:password@example.com\"},\n      {\"//example.com/exploit.html?<script>alert(1);</script>\"},\n      {\"//example.com/exploit.html?hel lo\"},\n      {\"//example.com/exploit.html?not_a%hex\"},\n      {\"//\"},\n    }\n  end\n\n  @[DataProvider(\"valid_custom_urls\")]\n  def test_custom_protocols_are_valid(value : String) : Nil\n    self.validator.validate value, self.new_constraint protocols: [\"ftp\", \"file\", \"git\"], require_tld: false\n    self.assert_no_violation\n  end\n\n  def valid_custom_urls : Tuple\n    {\n      {\"ftp://example.com\"},\n      {\"file://127.0.0.1\"},\n      {\"git://[::1]/\"},\n    }\n  end\n\n  @[TestWith(\n    {\"https://aaa\", true, false},\n    {\"https://aaa\", false, true},\n    {\"https://localhost\", true, false},\n    {\"https://localhost\", false, true},\n    {\"http://127.0.0.1\", false, true},\n    {\"http://127.0.0.1\", true, false},\n    {\"http://user.pass@local\", false, true},\n    {\"http://user.pass@local\", true, false},\n    {\"https://example.com\", true, true},\n    {\"https://example.com\", false, true},\n    {\"http://foo/bar.png\", false, true},\n    {\"http://foo/bar.png\", true, false},\n    {\"https://example.com.org\", true, true},\n    {\"https://example.com.org\", false, true},\n  )]\n  def test_require_tld(value : String, require_tld : Bool, is_valid : Bool) : Nil\n    self.validator.validate value, self.new_constraint require_tld: require_tld, tld_message: \"my_message\"\n\n    if is_valid\n      self.assert_no_violation\n    else\n      self\n        .build_violation(\"my_message\", CONSTRAINT::MISSING_TLD_ERROR, value)\n        .assert_violation\n    end\n  end\n\n  private def create_validator : AVD::ConstraintValidatorInterface\n    CONSTRAINT::Validator.new\n  end\n\n  private def constraint_class : AVD::Constraint.class\n    CONSTRAINT\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/constraints/valid_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nclass FooBarBaz\n  include AVD::Validatable\n\n  @[Assert::NotBlank(groups: [\"nested\"])]\n  @foo : String? = nil\nend\n\nclass FooBar\n  include AVD::Validatable\n\n  @[Assert::Valid(groups: [\"nested\"])]\n  @foo_bar_baz : FooBarBaz = FooBarBaz.new\nend\n\nclass Foo\n  include AVD::Validatable\n\n  @[Assert::Valid(groups: [\"nested\"])]\n  setter foo_bar : FooBar? = FooBar.new\nend\n\ndescribe AVD::Constraints::Valid::Validator do\n  it \"should pass property paths to nested contexts\" do\n    violations = AVD.validator.validate Foo.new, groups: \"nested\"\n\n    violations.size.should eq 1\n    violations[0].property_path.should eq \"foo_bar.foo_bar_baz.foo\"\n  end\n\n  it \"should pass with null value\" do\n    foo = Foo.new\n    foo.foo_bar = nil\n\n    violations = AVD.validator.validate foo, groups: \"nested\"\n\n    violations.should be_empty\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/metadata/class_metadata_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate record Entity do\n  include AVD::Validatable\nend\n\nstruct ClassMetadataTest < ASPEC::TestCase\n  @metadata : AVD::Metadata::ClassMetadata(Entity)\n\n  def initialize\n    @metadata = AVD::Metadata::ClassMetadata(Entity).new\n  end\n\n  def test_add_constraint_array : Nil\n    constraints = [CustomConstraint.new(\"\"), CustomConstraint.new(\"\")]\n\n    @metadata.add_constraint constraints\n\n    @metadata.constraints.should eq constraints\n\n    constraints.each do |constraint|\n      constraint.groups.should eq [\"default\", \"Entity\"]\n    end\n  end\n\n  def test_add_property_constraints : Nil\n    @metadata.add_property_constraints({\n      \"id\"  => AVD::Constraints::NotBlank.new,\n      \"foo\" => [CustomConstraint.new(\"\"), AVD::Constraints::Valid.new],\n    })\n\n    @metadata.constrained_properties.should eq [\"id\", \"foo\"]\n  end\n\n  def test_add_property_constraint_name_and_array : Nil\n    @metadata.add_property_constraint(\n      \"name\",\n      [CustomConstraint.new(\"\"), CustomConstraint.new(\"\")] of AVD::Constraint\n    )\n\n    @metadata.constrained_properties.should eq [\"name\"]\n  end\n\n  def test_add_property_constraint_name_and_single : Nil\n    @metadata.add_property_constraint(\n      \"name\",\n      CustomConstraint.new \"\"\n    )\n\n    @metadata.constrained_properties.should eq [\"name\"]\n  end\n\n  def test_add_property_constraint_property_metadata_and_single : Nil\n    @metadata.add_property_constraint(\n      AVD::Metadata::PropertyMetadata(Entity, Nil).new(\"name\"),\n      CustomConstraint.new \"\"\n    )\n\n    @metadata.constrained_properties.should eq [\"name\"]\n  end\n\n  def test_add_constraint_single : Nil\n    constraint = CustomConstraint.new \"\"\n\n    @metadata.add_constraint constraint\n\n    @metadata.constraints.should eq [constraint]\n    constraint.groups.should eq [\"default\", \"Entity\"]\n  end\n\n  def test_group_sequence_default_group : Nil\n    @metadata.group_sequence = [\"Foo\", @metadata.default_group]\n    @metadata.group_sequence.should be_a AVD::Constraints::GroupSequence\n  end\n\n  def test_group_sequence_fails_if_missing_default_group : Nil\n    expect_raises ArgumentError, \"The group 'Entity' is missing from the group sequence.\" do\n      @metadata.group_sequence = [\"Foo\", \"Bar\"]\n    end\n  end\n\n  def test_group_sequence_fails_if_contains_default_group : Nil\n    expect_raises ArgumentError, \"The group 'default' is not allowed in group sequences.\" do\n      @metadata.group_sequence = [\"Foo\", AVD::Constraint::DEFAULT_GROUP]\n    end\n  end\n\n  def test_group_sequence_fails_if_is_provider : Nil\n    metadata = AVD::Metadata::ClassMetadata(AVD::Spec::EntitySequenceProvider).new\n    metadata.group_sequence_provider = true\n\n    expect_raises ArgumentError, \"Defining a static group sequence is not allowed with a group sequence provider.\" do\n      metadata.group_sequence = [\"Athena::Validator::Spec::EntitySequenceProvider\", \"Bar\"]\n    end\n  end\n\n  def test_group_sequence_provider_fails_if_is_provider : Nil\n    metadata = AVD::Metadata::ClassMetadata(AVD::Spec::EntitySequenceProvider).new\n    metadata.group_sequence = [\"Athena::Validator::Spec::EntitySequenceProvider\", \"Bar\"]\n\n    expect_raises ArgumentError, \"Defining a group sequence provider is not allowed with a static group sequence.\" do\n      metadata.group_sequence_provider = true\n    end\n  end\n\n  def test_has_property_metadata : Nil\n    @metadata.add_property_constraint(\n      AVD::Metadata::PropertyMetadata(Entity, Nil).new(\"name\"),\n      CustomConstraint.new \"\"\n    )\n\n    @metadata.has_property_metadata?(\"name\").should be_true\n    @metadata.has_property_metadata?(\"age\").should be_false\n  end\n\n  def test_property_metadata : Nil\n    name_metadata = AVD::Metadata::PropertyMetadata(Entity, Nil).new \"name\"\n\n    @metadata.add_property_constraint name_metadata, CustomConstraint.new \"\"\n\n    @metadata.property_metadata(\"name\").should eq [name_metadata]\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/property_path_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate PATHS = [\n  {\"foo\", \"\", \"foo\"},           # It returns the basePath if subPath is empty\n  {\"\", \"bar\", \"bar\"},           # It returns the subPath if basePath is empty\n  {\"foo\", \"bar\", \"foo.bar\"},    # It append the subPath to the basePath\n  {\"foo\", \"[bar]\", \"foo[bar]\"}, # It does not include the dot separator if subPath uses the array notation\n  {\"0\", \"bar\", \"0.bar\"},        # Leading zeros are kept\n]\n\ndescribe AVD::PropertyPath do\n  describe \".append\" do\n    PATHS.each do |(base, sub, expected)|\n      it \"generates the correct strings\" do\n        AVD::PropertyPath.append(base, sub).should eq expected\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/spec/compound_constraint_test_case_spec.cr",
    "content": "require \"../spec_helper\"\n\nprivate class DummyCompoundConstraint < AVD::Constraints::Compound\n  def constraints : Type\n    [\n      AVD::Constraints::NotBlank.new,\n      AVD::Constraints::Length.new(...3),\n      AVD::Constraints::Regex.new(/[a-z]+/),\n      AVD::Constraints::Regex.new(/[0-9]+/),\n    ]\n  end\nend\n\nstruct CompoundConstraintTestCaseTest < AVD::Spec::CompoundConstraintTestCase(String)\n  protected def create_compound : AVD::Constraints::Compound\n    DummyCompoundConstraint.new\n  end\n\n  def test_assert_no_violation : Nil\n    self.validate_value \"ab1\"\n\n    self.assert_no_violation\n    self.assert_violation_count 0\n  end\n\n  def test_assert_is_raised_by_component : Nil\n    self.validate_value \"\"\n\n    self.assert_violations_raised_by_compound AVD::Constraints::NotBlank.new\n    self.assert_violation_count 1\n  end\n\n  def test_multiple_assert_are_raised_by_compound : Nil\n    self.validate_value \"1234\"\n\n    self.assert_violations_raised_by_compound(\n      AVD::Constraints::Length.new(...3),\n      AVD::Constraints::Regex.new(/[a-z]+/),\n    )\n    self.assert_violation_count 2\n  end\n\n  def test_no_assert_raised_but_expected : Nil\n    self.validate_value \"azert\"\n\n    expect_raises ::Spec::AssertionFailed, \"Expected violation(s) for constraint(s) 'Athena::Validator::Constraints::Length, Athena::Validator::Constraints::Regex' to be raised by compound.\" do\n      self.assert_violations_raised_by_compound(\n        AVD::Constraints::Length.new(..5),\n        AVD::Constraints::Regex.new(/^[A-Z]+$/),\n      )\n    end\n  end\n\n  def test_assert_raised_by_compound_is_not_exactly_the_same : Nil\n    self.validate_value \"123\"\n\n    expect_raises ::Spec::AssertionFailed, \"Expected violation(s) for constraint(s) 'Athena::Validator::Constraints::Regex' to be raised by compound.\" do\n      self.assert_violations_raised_by_compound(\n        AVD::Constraints::Regex.new(/^[A-Z]+$/),\n      )\n    end\n  end\n\n  def test_assert_raised_by_compound_but_got_none : Nil\n    self.validate_value \"123\"\n\n    expect_raises ::Spec::AssertionFailed, \"Expected at least one violation for constraint(s): 'Athena::Validator::Constraints::Length', got none.\" do\n      self.assert_violations_raised_by_compound(\n        AVD::Constraints::Length.new(..5),\n      )\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"../src/athena-validator\"\nrequire \"../src/spec\"\n\nASPEC.run_all\n\nclass MockConstraint < AVD::Constraint\n  def validated_by : AVD::ConstraintValidator.class\n    MockConstraintValidator\n  end\nend\n\nclass MockConstraintValidator < AVD::ConstraintValidator; end\n\nclass MockServiceConstraintValidator < AVD::ServiceConstraintValidator; end\n\nclass CustomConstraint < AVD::Constraint\n  @@error_names = {\n    \"abc123\" => \"FAKE_ERROR\",\n  }\n\n  class Validator < Athena::Validator::ConstraintValidator\n    def validate(value : _, constraint : CustomConstraint) : Nil\n    end\n  end\nend\n\ndef get_violation(message : String, *, invalid_value : _ = nil, root : _ = nil, property_path : String = \"property_path\", code : String? = nil) : AVD::Violation::ConstraintViolation\n  AVD::Violation::ConstraintViolation.new message, message, Hash(String, String).new, root, property_path, AVD::ValueContainer.new(invalid_value), code: code\nend\n"
  },
  {
    "path": "src/components/validator/spec/validatable_spec.cr",
    "content": "require \"./spec_helper\"\n\nprivate class ManualConstraints\n  include AVD::Validatable\n\n  def self.load_metadata(class_metadata : AVD::Metadata::ClassMetadata) : Nil\n    class_metadata.add_property_constraint \"name\", AVD::Constraints::EqualTo.new(\"foo\")\n  end\n\n  def initialize(@name : String); end\nend\n\nprivate abstract class Parent\n  include AVD::Validatable\nend\n\nprivate class Child < Parent\n  @[Assert::NotBlank]\n  property name : String = \"\"\nend\n\nprivate class Obj\n  include AVD::Validatable\n\n  @[Assert::NotBlank]\n  property name : String = \"\"\nend\n\nprivate class InstanceCallbackClass\n  include AVD::Validatable\n\n  @[Assert::Callback]\n  def validate(context : AVD::ExecutionContextInterface, payload : Hash(String, String)?) : Nil\n  end\nend\n\nprivate class ClassCallbackClass\n  include AVD::Validatable\n\n  @[Assert::Callback]\n  def self.validate(value : AVD::Constraints::Callback::ValueContainer, context : AVD::ExecutionContextInterface, payload : Hash(String, String)?) : Nil\n  end\nend\n\nprivate class ComparisonConstrained\n  include AVD::Validatable\n\n  @[Assert::LessThan(10)]\n  getter age : Int32 = 0\nend\n\ndescribe AVD::Validatable do\n  describe \".load_metadata\" do\n    it \"should manually add constraints to the metadata object\" do\n      ManualConstraints.validation_class_metadata.constrained_properties.should eq [\"name\"]\n    end\n  end\n\n  describe \".validation_class_metadata\" do\n    it \"is inherited when included in parent type\" do\n      Child.validation_class_metadata.constrained_properties.should eq [\"name\"]\n    end\n\n    it \"is not defined for abstract types\" do\n      Parent.responds_to?(:validation_class_metadata).should be_false\n    end\n\n    it \"is defined when included directly into non-abstract types\" do\n      Obj.validation_class_metadata.constrained_properties.should eq [\"name\"]\n    end\n\n    it \"properly registers instance method callback constraints\" do\n      constraints = InstanceCallbackClass.validation_class_metadata.constraints\n      constraints.size.should eq 1\n      constraints.first.should be_a AVD::Constraints::Callback\n    end\n\n    it \"properly registers class method callback constraints\" do\n      constraints = ClassCallbackClass.validation_class_metadata.constraints\n      constraints.size.should eq 1\n      constraints.first.should be_a AVD::Constraints::Callback\n    end\n\n    it \"does not duplicate property metadata for generic module constraints\" do\n      ComparisonConstrained.validation_class_metadata.property_metadata(\"age\").first.constraints.size.should eq 1\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/validator/recursive_validator_spec.cr",
    "content": "require \"../spec_helper\"\n\nstruct RecursiveValidatorTest < AVD::Spec::ValidatorTestCase\n  def create_validator(metadata_factory : AVD::Metadata::MetadataFactoryInterface) : AVD::Validator::ValidatorInterface\n    AVD::Validator::RecursiveValidator.new metadata_factory: metadata_factory\n  end\n\n  def test_validate_valid_constraint_on_getter_returning_null : Nil\n    metadata = AVD::Metadata::ClassMetadata(EntityParent).new\n    metadata.add_getter_constraint \"child\", AVD::Constraints::Valid.new\n\n    @metadata_factory.add_metadata EntityParent, metadata\n\n    self.validate(EntityParent.new).should be_empty\n  end\n\n  def test_validate_not_nil_constraint_on_getter_returning_null : Nil\n    metadata = AVD::Metadata::ClassMetadata(EntityParent).new\n    metadata.add_getter_constraint \"child\", AVD::Constraints::NotNil.new\n\n    @metadata_factory.add_metadata EntityParent, metadata\n\n    self.validate(EntityParent.new).size.should eq 1\n  end\n\n  def test_validate_all_constraint_validate_all_groups_for_nested_constraints : Nil\n    @metadata.add_property_constraint \"data_hash\", AVD::Constraints::All.new([\n      AVD::Constraints::NotBlank.new(groups: \"group1\"),\n      AVD::Constraints::Length.new(2.., groups: \"group2\"),\n    ])\n\n    object = Entity.new\n    object.data_hash = {\"one\" => \"t\", \"two\" => \"\"}\n\n    violations = self.validate object, nil, [\"group1\", \"group2\"]\n\n    violations.size.should eq 3\n\n    violations[0].constraint.should be_a AVD::Constraints::NotBlank\n    violations[1].constraint.should be_a AVD::Constraints::Length\n    violations[2].constraint.should be_a AVD::Constraints::Length\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/violation/constraint_violation_list_spec.cr",
    "content": "require \"../spec_helper\"\n\ndescribe AVD::Violation::ConstraintViolationList do\n  it \"without any violations\" do\n    AVD::Violation::ConstraintViolationList.new.size.should eq 0\n  end\n\n  it \"with violations\" do\n    violation = get_violation \"error\"\n\n    list = AVD::Violation::ConstraintViolationList.new [violation]\n\n    list.size.should eq 1\n    list.first.should eq violation\n  end\n\n  it \"#find_by_code\" do\n    list = AVD::Violation::ConstraintViolationList.new [get_violation(\"one\", code: \"CODE\"), get_violation(\"two\", code: \"CODE\"), get_violation(\"three\", code: \"CODE2\")]\n\n    new_list = list.find_by_code \"CODE\"\n\n    new_list.should be_a AVD::Violation::ConstraintViolationList\n    new_list.size.should eq 2\n  end\n\n  describe \"#add\" do\n    it \"adds the given violation\" do\n      violation = get_violation \"error\"\n\n      list = AVD::Violation::ConstraintViolationList.new\n      list.add violation\n\n      list.size.should eq 1\n      list.first.should eq violation\n    end\n\n    it \"adds another list\" do\n      other_list = AVD::Violation::ConstraintViolationList.new [get_violation(\"one\"), get_violation(\"two\"), get_violation(\"three\")]\n\n      list = AVD::Violation::ConstraintViolationList.new\n      list.add other_list\n\n      list.size.should eq 3\n      list[0].should eq other_list[0]\n      list[1].should eq other_list[1]\n      list[2].should eq other_list[2]\n    end\n  end\n\n  it \"#has?\" do\n    violation = get_violation \"error\"\n\n    list = AVD::Violation::ConstraintViolationList.new\n\n    list.has?(0).should be_false\n    list.add violation\n    list.has?(0).should be_true\n    list.has?(1).should be_false\n  end\n\n  it \"#set\" do\n    violation = get_violation \"error\"\n    other_error = get_violation \"other error\"\n\n    list = AVD::Violation::ConstraintViolationList.new [violation]\n\n    list.first.should eq violation\n    list.set 0, other_error\n    list.first.should eq other_error\n  end\n\n  it \"#remove\" do\n    violation = get_violation \"error\"\n\n    list = AVD::Violation::ConstraintViolationList.new\n    list.add violation\n\n    list.size.should eq 1\n    list.remove 0\n    list.should be_empty\n  end\n\n  it \"#to_s\" do\n    AVD::Violation::ConstraintViolationList.new([get_violation(\"Error 1\", root: \"Root\", property_path: \"\"), get_violation(\"Error 2\", root: \"Root\", property_path: \"\")]).to_s.should eq \"Root:\\n\\tError 1\\nRoot:\\n\\tError 2\\n\"\n  end\n\n  describe \"#to_json\" do\n    it \"serializes to an array of objects\" do\n      violations = AVD::Violation::ConstraintViolationList.new([get_violation(\"Error 1\"), get_violation(\"Error 2\", root: \"Root\")])\n      violations.to_json.should eq %([{\"property\":\"property_path\",\"message\":\"Error 1\"},{\"property\":\"property_path\",\"message\":\"Error 2\"}])\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/spec/violation/constraint_violation_spec.cr",
    "content": "require \"../spec_helper\"\n\nrecord TestObj do\n  include AVD::Validatable\nend\n\ndescribe AVD::Violation::ConstraintViolation do\n  describe \"#invalid_value\" do\n    it \"returns the value\" do\n      get_violation(\"Message\", invalid_value: 12.8).invalid_value.should eq 12.8\n    end\n  end\n\n  describe \"#to_s\" do\n    it Indexable do\n      get_violation(\"Array\", root: \"Root\", property_path: \"property.path\").to_s.should eq \"Root.property.path:\\n\\tArray\\n\"\n      get_violation(\"Array\", root: \"Root\", property_path: \"[2].value\").to_s.should eq \"Root[2].value:\\n\\tArray\\n\"\n    end\n\n    it Enumerable do\n      get_violation(\"Array\", root: [1, 2, 3], property_path: \"[1]\").to_s.should eq \"Object(Array(Int32))[1]:\\n\\tArray\\n\"\n    end\n\n    it Hash do\n      get_violation(\"Some message\", root: {\"key\" => \"value\"}, property_path: \"key\").to_s.should eq \"Hash.key:\\n\\tSome message\\n\"\n      get_violation(\"Some message\", root: {\"key\" => \"value\"}, property_path: \"[key]\").to_s.should eq \"Hash[key]:\\n\\tSome message\\n\"\n    end\n\n    it \"code\" do\n      get_violation(\"Some message\", property_path: \"key\", code: \"CODE\").to_s.should eq \"key:\\n\\tSome message (code: CODE)\\n\"\n    end\n\n    it AVD::Validatable do\n      get_violation(\"Some message\", root: TestObj.new, property_path: \"\").to_s.should eq \"Object(TestObj):\\n\\tSome message\\n\"\n    end\n  end\n\n  describe \"#to_json\" do\n    it \"without a code\" do\n      get_violation(\"Message\", invalid_value: 12.8).to_json.should eq %({\"property\":\"property_path\",\"message\":\"Message\"})\n    end\n\n    it \"with a code\" do\n      get_violation(\"Message\", invalid_value: 12.8, code: \"CODE\").to_json.should eq %({\"property\":\"property_path\",\"message\":\"Message\",\"code\":\"CODE\"})\n    end\n\n    it \"with a root value\" do\n      get_violation(\"Message\", invalid_value: 12.8, code: \"CODE\", root: \"Root\").to_json.should eq %({\"property\":\"property_path\",\"message\":\"Message\",\"code\":\"CODE\"})\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/athena-validator.cr",
    "content": "require \"json\"\n\nrequire \"./constraint\"\nrequire \"./constraint_validator\"\nrequire \"./constraint_validator_factory\"\nrequire \"./constraint_validator_factory_interface\"\nrequire \"./constraint_validator_interface\"\nrequire \"./execution_context\"\nrequire \"./execution_context_interface\"\nrequire \"./property_path\"\nrequire \"./validatable\"\n\nrequire \"./constraints/abstract_comparison\"\nrequire \"./constraints/abstract_comparison_validator\"\nrequire \"./constraints/*\"\nrequire \"./exception/*\"\nrequire \"./metadata/*\"\nrequire \"./validator/*\"\nrequire \"./violation/*\"\n\n# Convenience alias to make referencing `Athena::Validator` types easier.\nalias AVD = Athena::Validator\n\n# Used to apply constraints to instance variables and types via annotations.\n#\n# ```\n# @[Assert::NotBlank]\n# property name : String\n# ```\n# NOTE: Constraints, including custom ones, are automatically added to this namespace.\nalias Assert = AVD::Annotations\n\nmodule Athena; end\n\n# Provides a robust object/value validation framework.\nmodule Athena::Validator\n  VERSION = \"0.5.0\"\n\n  # :nodoc:\n  #\n  # Default namespace for constraint annotations.\n  #\n  # NOTE: Constraints, including custom ones, are automatically added to this namespace.\n  module Annotations; end\n\n  # Contains all of the built in `AVD::Constraint`s.\n  # See each individual constraint for more information.\n  # The `Assert` alias is used to apply these constraints via annotations.\n  module Constraints; end\n\n  # Both acts as a namespace for exceptions related to the `Athena::Validator` component, as well as a way to check for exceptions from the component.\n  module Exception; end\n\n  # Contains types used to store metadata associated with a given `AVD::Validatable` instance.\n  #\n  # Most likely you won't have to work any of these directly.\n  # However if you are adding constraints manually to properties using the `self.load_metadata` method,\n  # you should be familiar with `AVD::Metadata::ClassMetadata`.\n  module Metadata; end\n\n  # Contains types related to the validator itself.\n  module Validator; end\n\n  # Contains types related to constraint violations.\n  module Violation; end\n\n  # :nodoc:\n  abstract struct Container\n    abstract def type_name : String\n\n    def inspect(io : IO) : Nil\n      io << \"#<AVD::Container(\" << self.type_name << \")>\"\n    end\n  end\n\n  # :nodoc:\n  record ValueContainer(T) < Container, value : T do\n    def value_type : T.class\n      T\n    end\n\n    def type_name : String\n      {{ T.stringify }}\n    end\n\n    def ==(other : AVD::Container) : Bool\n      @value == other.value\n    end\n  end\n\n  # Returns a new `AVD::Validator::ValidatorInterface`.\n  #\n  # ```\n  # validator = AVD.validator\n  #\n  # validator.validate \"foo\", AVD::Constraints::NotBlank.new\n  # ```\n  def self.validator : AVD::Validator::ValidatorInterface\n    AVD::Validator::RecursiveValidator.new\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraint.cr",
    "content": "# `Athena::Validator` validates values/objects against a set of constraints, i.e. rules.\n# Each constraint makes an assertive statement that some condition is true.\n# Given a value, a constraint will tell you if that value adheres to the rules of the constraint.\n# An example of this could be asserting a value is not blank, or greater than or equal to another value.\n#\n# It's important to note a constraint does not implement the validation logic itself.\n# Instead, this is handled via an `AVD::ConstraintValidator` as defined via `#validated_by`.\n# Having this abstraction allows for better reusability and testability.\n#\n# `Athena::Validator` comes with a set of common constraints built in.\n# See the individual types within `AVD::Constraints` for more information.\n#\n# ## Usage\n#\n# A constraint can be instantiated and passed to a validator directly:\n#\n# ```\n# # An array of constraints can also be passed.\n# AVD.validator.validate \"\", AVD::Constraints::NotBlank.new\n# ```\n#\n# Constraint annotation(s) can also be applied to instance variables to assert the value of that property adheres to the constraint.\n#\n# ```\n# class Example\n#   include AVD::Validatable\n#\n#   def initialize(@name : String); end\n#\n#   # More than one constraint can be applied to a property.\n#   @[Assert::NotBlank]\n#   property name : String\n# end\n#\n# # Constraints are extracted from the annotations.\n# # An array can also be passed to validate against that list instead.\n# AVD.validator.validate Example.new(\"Jim\")\n# ```\n#\n# Constraints can also be added manually via code by defining an `self.load_metadata(metadata : AVD::Metadata::ClassMetadata) : Nil`\n# method and adding the constraints directly to the `AVD::Metadata::ClassMetadata` instance.\n#\n# ```\n# # This class method is invoked when building the metadata associated with a type,\n# # and can be used to manually wire up the constraints.\n# def self.load_metadata(metadata : AVD::Metadata::ClassMetadata) : Nil\n#   metadata.add_property_constraint \"name\", AVD::Constraints::NotBlank.new\n# end\n# ```\n#\n# The metadata for each type is lazily loaded when an instance of that type is validated, and is only built once.\n#\n# ## Arguments\n#\n# While most constraints can be instantiated with an argless constructor,they do have a set of optional arguments.\n# * The `message` argument represents the message that should be used if the value is found to not be valid.\n# The message can also include placeholders, in the form of `{{ key }}`, that will be replaced when the message is rendered.\n# Most commonly this includes the invalid value itself, but some constraints have additional placeholders.\n# * The `payload` argument can be used to attach any domain specific data to the constraint; such as attaching a severity with each constraint\n# to have more serious violations be handled differently.  See the [Payload][Athena::Validator::Constraint--payload] section.\n# * The `groups` argument can be used to run a subset of the defined constraints.  More on this in the [Validation Groups][Athena::Validator::Constraint--validation-groups] section.\n#\n# For example:\n#\n# ```\n# validator = AVD.validator\n#\n# # Instantiate a constraint with a custom message, using a placeholder.\n# violations = validator.validate -4, AVD::Constraints::PositiveOrZero.new message: \"{{ value }} is not a valid age.  A user cannot have a negative age.\"\n#\n# puts violations # =>\n# # -4:\n# #   -4 is not a valid age.  A user cannot have a negative age. (code: e09e52d0-b549-4ba1-8b4e-420aad76f0de)\n# ```\n# Customizing the message can be a good way for those consuming the errors to determine _WHY_ a given value is not valid.\n#\n# ### Default Argument\n#\n# The first argument of the constructor is known as the default argument.\n# This argument is special when using the annotation based approach in that it can be supplied as a positional argument within the annotation.\n#\n# For example the default argument for `AVD::Constraints::GreaterThan` is the value that the value being validated should be compared against.\n#\n# Thus:\n#\n# ```\n# @[Assert::GreaterThan(0)]\n# property age : Int32\n# ```\n#\n# Is equivalent to:\n#\n# ```\n# @[Assert::GreaterThan(value: 0)]\n# property age : Int32\n# ```\n#\n# NOTE: Only the first argument can be supplied positionally, all other arguments must be provided as named arguments within the annotation.\n#\n# ### Message Plurality\n#\n# `Athena::Validator` has very basic support for pluralizing constraint `#message`s via `AVD::Violation::ConstraintViolationInterface#plural`.\n#\n# For example the `#message` could have different versions based on the plurality of the violation.\n# Currently this only supports two contexts: singular (1/nil) and plural (2+).\n#\n# Multiple messages, separated by a `|`, can be included as part of an `AVD::Constraint` message.\n# For example from `AVD::Constraints::Size`:\n#\n# `min_message : String = \"This value is too short. It should have {{ limit }} {{ type }} or more.|This value is too short. It should have {{ limit }} {{ type }}s or more.\"`\n#\n# If violations' `#plural` method returns `1` (or `nil`) the first message will be used.  If `#plural` is `2` or more, the latter message will be used.\n#\n# TODO: Support more robust translations; like language or multiple pluralities.\n#\n# ### Payload\n#\n# The `payload` argument defined on every `AVD::Constraint` type can be used to store custom domain specific information with a constraint.\n# This data can later be retrieved off of an `AVD::Violation::ConstraintViolationInterface`.\n# An example use case for this could be mapping a \"severity\" to a CSS class based on how important each specific constraint is.\n#\n# ```\n# class User\n#   include AVD::Validatable\n#\n#   def initialize(@email : String, @password : String); end\n#\n#   @[Assert::NotBlank(payload: {\"severity\" => \"error\"})]\n#   getter email : String\n#\n#   @[Assert::NotBlank(payload: {\"severity\" => \"warning\"})]\n#   getter password : String\n# end\n#\n# violations = AVD.validator.validate User.new \"\", \"\"\n#\n# # Use this when rendering HTML, or JSON to allow dynamically customizing the response object.\n# violations[0].constraint.payload # => {\"severity\" => \"error\"}\n# violations[1].constraint.payload # => {\"severity\" => \"warning\"}\n# ```\n#\n# ## Validation Groups\n#\n# The `groups` argument defined on every `AVD::Constraint` type can be used to run a subset of validations.\n#\n# For example, say we only want to validate certain properties when the user is first created:\n#\n# ```\n# class User\n#   include AVD::Validatable\n#\n#   def initialize(@email : String, @password : String, @city : String); end\n#\n#   @[Assert::Email(groups: \"create\")]\n#   getter email : String\n#\n#   @[Assert::NotBlank(groups: \"create\")]\n#   @[Assert::Size(7.., groups: \"create\")]\n#   getter password : String\n#\n#   @[Assert::Size(2..)]\n#   getter city : String\n# end\n#\n# user = User.new \"contact@athenaframework.org\", \"monkey123\", \"\"\n#\n# # Validate the user object, but only for those in the \"create\" group,\n# # if no groups are supplied, then all constraints in the \"default\" group will be used.\n# violations = AVD.validator.validate user, groups: \"create\"\n#\n# # There are no violations since the city's size is not validated since it's not in the \"create\" group.\n# violations.empty? # => true\n# ```\n#\n# Using this configuration, there are three groups at play within the `User` class:\n# 1. `default` - Contains constraints in the current type, and subtypes, that belong to no other group.  I.e. `city`.\n# 1. `User` - In this example, equivalent to all constraints in the `default` group.  See `AVD::Constraints::GroupSequence`, and the note below.\n# 1. `create` - A custom group that only contains the constraints explicitly associated with it.  I.e. `email`, and `password`.\n#\n# NOTE: When validating _just_ the `User` object, the `default` group is equivalent to the `User` group.\n# However, if the `User` object has other embedded types using the `AVD::Constraints::Valid` constraint, then validating the `User` object with the `User`\n# group would only validate constraints that are explicitly in the `User` group within the embedded types.\n#\n# By default, all constraints are validated in a single \"batch\".  I.e. all constraints within the provided group(s) are validated, without regard\n# to if the previous/next constraint is/was (in)valid.  However, an `AVD::Constraints::GroupSequence` can be used to validate batches of constraints in steps.\n# I.e. validate the first \"batch\" of constraints, and only advance to the next batch if all constraints in that step are valid.\n#\n# NOTE: The payload is not used with the framework itself.\n#\n# ## Custom Constraints\n#\n# If the built in `AVD::Constraints` are not sufficient to handle validating a given value/object; custom ones can be defined.\n# Let's make a new constraint that asserts a string contains only alphanumeric characters.\n#\n# This is accomplished by first defining a new class within the `AVD::Constraints` namespace that inherits from `AVD::Constraint`.\n# Then define a `Validator` struct within our constraint that inherits from `AVD::ConstraintValidator` that actually implements the validation logic.\n#\n# ```\n# class AVD::Constraints::AlphaNumeric < AVD::Constraint\n#   # (Optional) A unique error code can also be defined to provide a machine readable identifier for a specific error.\n#   NOT_ALPHANUMERIC_ERROR = \"1a83a8bd-ff79-4d5c-96e7-86d0b25b8a09\"\n#\n#   # (Optional) Allows using the `.error_message(code : String) : String` method with this constraint.\n#   @@error_names = {\n#     NOT_ALPHANUMERIC_ERROR => \"NOT_ALPHANUMERIC_ERROR\",\n#   }\n#\n#   # Define an initializer with our default message, and any additional arguments specific to this constraint.\n#   def initialize(\n#     message : String = \"This value should contain only alphanumeric characters.\",\n#     groups : Array(String) | String | Nil = nil,\n#     payload : Hash(String, String)? = nil,\n#   )\n#     super message, groups, payload\n#   end\n#\n#   # Define the validator within our constraint that'll contain our validation logic.\n#   class Validator < AVD::ConstraintValidator\n#     # Define our validate method that accepts the value to be validated, and the constraint.\n#     #\n#     # Overloads can be used to filter values of specific types.\n#     def validate(value : _, constraint : AVD::Constraints::AlphaNumeric) : Nil\n#       # Custom constraints should ignore nil and empty values to allow\n#       # other constraints (NotBlank, NotNil, etc.) take care of that\n#       return if value.nil? || value == \"\"\n#\n#       # We'll cast the value to a string,\n#       # alternatively we could just ignore non `String?` values.\n#       value = value.to_s\n#\n#       # If all the characters of this string are alphanumeric, then it is valid\n#       return if value.each_char.all? &.alphanumeric?\n#\n#       # Otherwise, it is invalid and we need to add a violation,\n#       # see `AVD::ExecutionContextInterface` for additional information.\n#       self.context.add_violation constraint.message, NOT_ALPHANUMERIC_ERROR, value\n#     end\n#   end\n# end\n#\n# puts AVD.validator.validate \"$\", AVD::Constraints::AlphaNumeric.new # =>\n# # $:\n# #   This value should contain only alphanumeric characters. (code: 1a83a8bd-ff79-4d5c-96e7-86d0b25b8a09)\n# ```\n#\n# NOTE: The constraint _MUST_ be defined within the `AVD::Constraints` namespace for implementation reasons.  This may change in the future.\n#\n# We are now able to use this constraint as we would one of the built in ones;\n# either by manually instantiating it, or applying an `@[Assert::AlphaNumeric]` annotation to a property.\n#\n# See `AVD::ConstraintValidatorInterface` for more information on custom validators.\n#\n# NOTE:  The `AVD::Constraints::Compound` constraint can be used to create a constraint that consists of one or more other constraints.\n#\nabstract class Athena::Validator::Constraint\n  # The group that `self` is a part of if no other group(s) are explicitly defined.\n  DEFAULT_GROUP = \"default\"\n\n  @@error_names = Hash(String, String).new\n\n  # Returns the name of the provided *error_code*.\n  def self.error_name(error_code : String) : String\n    @@error_names[error_code]? || raise AVD::Exception::InvalidArgument.new \"The error code '#{error_code}' does not exist for constraint of type '#{self}'.\"\n  end\n\n  # Returns the message that should be rendered if `self` is found to be invalid.\n  #\n  # NOTE: Some subtypes do not use this and instead define multiple message\n  # properties in order to support more specific error messages.\n  getter message : String\n\n  # Returns any domain specific data associated with `self`.\n  getter payload : Hash(String, String)?\n\n  # This isn't set directly as a property such that we can somewhat tell if it's been customized or not.\n  # E.g. so that the composite constraint knows if it needs to apply its groups to it or not\n  @groups : Array(String)? = nil\n\n  def initialize(@message : String, groups : Array(String) | String | Nil = nil, @payload : Hash(String, String)? = nil)\n    unless groups.nil?\n      @groups = case groups\n                when Array  then groups\n                when String then [groups]\n                end\n    end\n  end\n\n  # Sets the validation groups `self` is a part of.\n  def groups=(@groups : Array(String))\n  end\n\n  # Returns the validation groups `self` is a part of.\n  def groups : Array(String)\n    @groups ||= [DEFAULT_GROUP]\n  end\n\n  # Adds the provided *group* to `#groups` if `self` is in the `AVD::Constraint::DEFAULT_GROUP`.\n  def add_implicit_group(group : String) : Nil\n    if self.groups.includes?(DEFAULT_GROUP) && !self.groups.includes?(group)\n      self.groups << group\n    end\n  end\n\n  # Returns the `AVD::ConstraintValidator.class` that should handle validating `self`.\n  abstract def validated_by : AVD::ConstraintValidator.class\n\n  macro inherited\n    {% unless @type.abstract? %}\n      # See `{{@type.id}}`.\n      annotation ::Athena::Validator::Annotations::{{@type.name(generic_args: false).split(\"::\").last.id}}; end\n\n      # :inherit:\n      def validated_by : AVD::ConstraintValidator.class\n        Validator\n      end\n\n      # :nodoc:\n      def ==(other : self) : Bool\n        \\{% if @type.class? %}\n          return true if same?(other)\n        \\{% end %}\n        \\{% for field in @type.instance_vars %}\n          return false unless @\\{{field.id}} == other.@\\{{field.id}}\n        \\{% end %}\n        true\n      end\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraint_validator.cr",
    "content": "require \"./constraint_validator_interface\"\n\n# Basic implementation of `AVD::ConstraintValidatorInterface`.\nabstract class Athena::Validator::ConstraintValidator\n  include Athena::Validator::ConstraintValidatorInterface\n\n  # :inherit:\n  def context : AVD::ExecutionContextInterface\n    @context.not_nil!\n  end\n\n  # :nodoc:\n  def context=(@context : AVD::ExecutionContextInterface); end\n\n  # :inherit:\n  def validate(value : _, constraint : AVD::Constraint) : Nil\n    # Noop if a given validator doesn't support a given type of value\n  end\n\n  # Can be used to raise an `AVD::Exception::UnexpectedValueError`\n  # in case `self` is only able to validate values of the *supported_types*.\n  #\n  # ```\n  # # Define a validate method to catch values of other types.\n  # # Overloads above would handle the valid types.\n  # def validate(value : _, constraint : AVD::Constraints::MyConstraint) : Nil\n  #   self.raise_invalid_type value, \"Int | Float\"\n  # end\n  # ```\n  #\n  # This would result in a violation with the message `This value should be a valid: Int | Float`\n  # being added to the current `#context`.\n  def raise_invalid_type(value : _, supported_types : String) : NoReturn\n    raise AVD::Exception::UnexpectedValueError.new value, supported_types\n  end\nend\n\n# Extension of `AVD::ConstraintValidator` used to denote a service validator\n# that can be used with [Athena Dependency Injection](https://github.com/athena-framework/dependency-injection).\nabstract class Athena::Validator::ServiceConstraintValidator < Athena::Validator::ConstraintValidator\n  macro inherited\n    def self.new : NoReturn\n      # Validators of this type will be injected via DI and not directly instantiated within the factory.\n      raise \"\"\n    end\n  end\nend\n\n# Compiler doesn't like there not being any instances of this\nprivate class FakeConstraintValidator < Athena::Validator::ServiceConstraintValidator; end\n"
  },
  {
    "path": "src/components/validator/src/constraint_validator_factory.cr",
    "content": "require \"./constraint_validator_factory_interface\"\n\n# Basic implementation of `AVD::ConstraintValidatorFactoryInterface`.\nstruct Athena::Validator::ConstraintValidatorFactory\n  include Athena::Validator::ConstraintValidatorFactoryInterface\n\n  @validators : Hash(AVD::ConstraintValidator.class, AVD::ConstraintValidator) = Hash(AVD::ConstraintValidator.class, AVD::ConstraintValidator).new\n\n  # :nodoc:\n  #\n  # Overload to support DI.\n  def initialize(constraint_validators : Array(AVD::ServiceConstraintValidator) = [] of AVD::ServiceConstraintValidator)\n    constraint_validators.each do |validator|\n      @validators[validator.class] = validator\n    end\n  end\n\n  # Returns an `AVD::ConstraintValidator` based on the provided *validator_class*.\n  #\n  # NOTE: This overloaded is intended to be used for service based validators that are already\n  # instantiated and were provided via DI.\n  def validator(for validator_class : AVD::ServiceConstraintValidator.class) : AVD::ConstraintValidator\n    @validators[validator_class]\n  end\n\n  # Returns an `AVD::ConstraintValidator` based on the provided *validator_class*.\n  def validator(for validator_class : AVD::ConstraintValidator.class) : AVD::ConstraintValidator\n    if validator = @validators[validator_class]?\n      return validator\n    end\n\n    @validators[validator_class] = validator_class.new\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraint_validator_factory_interface.cr",
    "content": "# Provides validator instances based on a validator class, caching the instance.\n#\n# `AVD::ServiceConstraintValidator`s are instantiated externally and injected into the factory.\nmodule Athena::Validator::ConstraintValidatorFactoryInterface\n  # Returns an `AVD::ConstraintValidatorInterface` instance based on the provided *validator_class*.\n  abstract def validator(for validator_class : AVD::ConstraintValidator.class) : AVD::ConstraintValidatorInterface\nend\n"
  },
  {
    "path": "src/components/validator/src/constraint_validator_interface.cr",
    "content": "# A constraint validator is responsible for implementing the actual validation logic for a given `AVD::Constraint`.\n#\n# Constraint validators should inherit from this type and implement a `#validate` method.\n# Most commonly the validator type will be defined within the namespace of the related `AVD::Constraint` itself.\n#\n# The `#validate` method itself does not return anything.\n# Violations are added to the current `#context`, either as a single error message, or augmented with additional metadata about the failure.\n# See `AVD::ExecutionContextInterface` for more information on how violations can be added.\n#\n# ### Example\n#\n# ```\n# class AVD::Constraints::MyConstraint < AVD::Constraint\n#   # Initializer/etc for the constraint\n#\n#   class Validator < AVD::ConstraintValidator\n#     # Define a validate method that handles values of any type, and our `MyConstraint` constraint.\n#     def validate(value : _, constraint : AVD::Constraints::MyConstraint) : Nil\n#       # Implement logic to determine if the value is valid.\n#       # Violations should be added to the current `#context`,\n#       # See `AVD::ExecutionContextInterface` for more information.\n#     end\n#   end\n# end\n# ```\n#\n# Overloads of the `#validate` method can also be used to handle validating values of different types independently.\n# If the value cannot be handled by any of `self`'s validators, it is handled via `AVD::ConstraintValidator#validate`\n# and is essentially a noop.\n#\n# If a `AVD::Constraint` can only support values of certain types, `AVD::ConstraintValidator#raise_invalid_type`\n# in a catchall overload can be used to add an invalid type `AVD::Violation::ConstraintViolationInterface`.\n#\n# ```\n# class Validator < AVD::ConstraintValidator\n#   def validate(value : Number, constraint : AVD::Constraints::MyConstraint) : Nil\n#     # Handle validating `Number` values\n#   end\n#\n#   def validate(value : Time, constraint : AVD::Constraints::MyConstraint) : Nil\n#     # Handle validating `Time` values\n#   end\n#\n#   def validate(value : _, constraint : AVD::Constraints::MyConstraint) : Nil\n#     # Add an invalid type violation for values of all other types.\n#     self.raise_invalid_type value, \"Number | Time\"\n#   end\n# end\n# ```\n#\n# NOTE:  Normally custom validators should not handle `nil` or `blank` values as they are handled via other constraints.\nmodule Athena::Validator::ConstraintValidatorInterface\n  # Validate the provided *value* against the provided *constraint*.\n  #\n  # Violations should be added to the current `#context`.\n  abstract def validate(value : _, constraint : AVD::Constraint) : Nil\n\n  # Returns the a reference to the `AVD::ExecutionContextInterface`\n  # to which violations within `self` should be added.\n  #\n  # See the type for more information.\n  abstract def context : AVD::ExecutionContextInterface\n\n  # Internal\n\n  # :nodoc:\n  abstract def context=(context : AVD::ExecutionContextInterface)\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/abstract_comparison.cr",
    "content": "# Defines common logic for comparison based constraints, such as `AVD::Constraints::GreaterThan`, or `AVD::Constraints::EqualTo`.\nmodule Athena::Validator::Constraints::AbstractComparison(ValueType)\n  # Returns the expected value.\n  getter value : ValueType\n\n  # Returns the type of the expected value.\n  getter value_type : ValueType.class = ValueType\n\n  def initialize(\n    @value : ValueType,\n    message : String = default_error_message,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  # Returns the `AVD::Constraint#message` for this constraint.\n  abstract def default_error_message : String\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/abstract_comparison_validator.cr",
    "content": "# Defines common logic for comparison based constraint validators.\nabstract class Athena::Validator::Constraints::ComparisonValidator < Athena::Validator::ConstraintValidator\n  # Returns `true` if the provided *actual* and *expected* values are compatible, otherwise `false`.\n  abstract def compare_values(actual : _, expected : _) : Bool\n\n  # Returns the expected error code for `self`.\n  abstract def error_code : String\n\n  # :inherit:\n  def validate(value : _, constraint : AVD::Constraints::AbstractComparison) : Nil\n    return if value.nil?\n\n    compared_value = constraint.value\n\n    return if self.compare_values value, compared_value\n\n    self\n      .context\n      .build_violation(constraint.message, self.error_code)\n      .set_parameters({\"{{ value }}\" => value.to_s, \"{{ compared_value }}\" => compared_value.to_s, \"{{ compared_value_type }}\" => constraint.value_type.to_s})\n      .add\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/all.cr",
    "content": "require \"./composite\"\n\n# Validates each element of an `Iterable` is valid based on a collection of constraints.\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### constraints\n#\n# **Type:** `Array(AVD::Constraint) | AVD::Constraint`\n#\n# The `AVD::Constraint`(s) that you want to apply to each element of the underlying iterable.\n#\n# ## Optional Arguments\n#\n# NOTE: This constraint does not support a `message` argument.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\n#\n# # Usage\n#\n# ```\n# class Example\n#   include AVD::Validatable\n#\n#   def initialize(@strings : Array(String)); end\n#\n#   # Assert each string is not blank and is at least 5 characters long.\n#   @[Assert::All([\n#     @[Assert::NotBlank],\n#     @[Assert::Size(5..)],\n#   ])]\n#   getter strings : Array(String)\n# end\n# ```\n#\n# NOTE: The annotation approach only supports two levels of nested annotations.\n# Manually wire up the constraint via code if you require more than that.\nclass Athena::Validator::Constraints::All < Athena::Validator::Constraints::Composite\n  def initialize(\n    constraints : AVD::Constraints::Composite::Type,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super constraints, \"\", groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : Hash?, constraint : AVD::Constraints::All) : Nil\n      return if value.nil?\n\n      self.with_validator do |validator|\n        value.each do |k, v|\n          validator.at_path(\"[#{k}]\").validate(v, constraint.constraints.values)\n        end\n      end\n    end\n\n    # :inherit:\n    def validate(value : Indexable?, constraint : AVD::Constraints::All) : Nil\n      return if value.nil?\n\n      self.with_validator do |validator|\n        value.each_with_index do |item, idx|\n          validator.at_path(\"[#{idx}]\").validate(item, constraint.constraints.values)\n        end\n      end\n    end\n\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::All) : NoReturn\n      self.raise_invalid_type value, \"Hash | Indexable\"\n    end\n\n    private def with_validator(& : AVD::Validator::ContextualValidatorInterface ->) : Nil\n      yield self.context.validator.in_context self.context\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/at_least_one_of.cr",
    "content": "require \"./composite\"\n\n# Validates that a value satisfies at least one of the provided constraints.\n# Validation stops as soon as one constraint is satisfied.\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### constraints\n#\n# **Type:** `Array(AVD::Constraint) | AVD::Constraint`\n#\n# The `AVD::Constraint`(s) from which at least one of has to be satisfied in order for the validation to succeed.\n#\n# ## Optional Arguments\n#\n# ### include_internal_messages\n#\n# **Type:** `Bool` **Default:** `true`\n#\n# If the validation failed message should include the list of messages for the internal constraints.\n# See the [message](#message) argument for an example.\n#\n# ### message_collection\n#\n# **Type:** `String` **Default:** `Each element of this collection should satisfy its own set of constraints.`\n#\n# The message that will be shown if validation fails and the internal constraint is an `AVD::Constraints::All`.\n# See the [message](#message) argument for an example.\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should satisfy at least one of the following constraints:`\n#\n# The intro that will be shown if validation fails.\n# By default, it'll be followed by the list of messages from the internal [constraints](#constraints);\n# configurable via the [include_internal_messages](#include_internal_messages) argument.\n#\n# For example, if the `grades` property in the example below fails to validate, the message will be:\n#\n# > This value should satisfy at least one of the following constraints: [1] This value is too short. It should have 3 items or more. [2] Each element of this collection should satisfy its own set of constraints.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\n#\n# # Usage\n#\n# ```\n# class Example\n#   include AVD::Validatable\n#\n#   def initialize(@password : String, @grades : Array(Int32)); end\n#\n#   # Asserts the password contains an `#` or is at least 10 characters long.\n#   @[Assert::AtLeastOneOf([\n#     @[Assert::Regex(/#/)],\n#     @[Assert::Size(10..)],\n#   ])]\n#   getter password : String\n#\n#   # Asserts the `grades` array contains at least 3 elements or\n#   # that each element is greater than or equal to 5.\n#   @[Assert::AtLeastOneOf([\n#     @[Assert::Size(3..)],\n#     @[Assert::All([\n#       @[Assert::GreaterThanOrEqual(5)],\n#     ])],\n#   ])]\n#   getter grades : Array(Int32)\n# end\n# ```\n#\n# NOTE: The annotation approach only supports two levels of nested annotations.\n# Manually wire up the constraint via code if you require more than that.\nclass Athena::Validator::Constraints::AtLeastOneOf < Athena::Validator::Constraints::Composite\n  DEFAULT_ERROR_MESSAGE = \"This value should satisfy at least one of the following constraints:\"\n  AT_LEAST_ONE_OF_ERROR = \"811994eb-b634-42f5-ae98-13eec66481b6\"\n\n  @@error_names = {\n    AT_LEAST_ONE_OF_ERROR => \"AT_LEAST_ONE_OF_ERROR\",\n  }\n\n  getter? include_internal_messages : Bool\n  getter message_collection : String\n\n  def initialize(\n    constraints : AVD::Constraints::Composite::Type,\n    @include_internal_messages : Bool = true,\n    @message_collection : String = \"Each element of this collection should satisfy its own set of constraints.\",\n    message : String = \"This value should satisfy at least one of the following constraints:\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super constraints, message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::AtLeastOneOf) : Nil\n      messages = [constraint.message]\n\n      validator = self.context.validator\n\n      constraint.constraints.each do |idx, item|\n        violations = validator.validate value, [item]\n\n        return if violations.empty?\n\n        if constraint.include_internal_messages?\n          messages << String.build do |str|\n            str << \" [#{idx.to_i + 1}] \"\n\n            str << if item.is_a? AVD::Constraints::All\n              constraint.message_collection\n            else\n              violations.first.message\n            end\n          end\n        end\n      end\n\n      self.context.add_violation messages.join, AT_LEAST_ONE_OF_ERROR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/blank.cr",
    "content": "# Validates that a value is blank; meaning equal to an empty string or `nil`.\n#\n# ```\n# class Profile\n#   include AVD::Validatable\n#\n#   def initialize(@username : String); end\n#\n#   @[Assert::Blank]\n#   property username : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be blank.`\n#\n# The message that will be shown if the value is not blank.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Blank < Athena::Validator::Constraint\n  NOT_BLANK_ERROR = \"c815f901-c581-4fb7-a85d-b8c5bc757959\"\n\n  @@error_names = {\n    NOT_BLANK_ERROR => \"NOT_BLANK_ERROR\",\n  }\n\n  def initialize(\n    message : String = \"This value should be blank.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Blank) : Nil\n      return if value.nil?\n      return if value.responds_to?(:blank?) && value.blank?\n\n      self.context.add_violation constraint.message, NOT_BLANK_ERROR, value\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/callback.cr",
    "content": "# Allows creating totally custom validation rules, assigning any violations to specific fields on your object.\n# This process is achieved via using one or more _callback_ methods which will be invoked during the validation process.\n#\n# NOTE: The callback method itself does _fail_ or return any value.\n# Instead it should directly add violations to the `AVD::ExecutionContextInterface` argument.\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### callback\n#\n# **Type:** `AVD::Constraints::Callback::CallbackProc?` **Default:** `nil`\n#\n# The proc that should be invoked as the callback for this constraint.\n#\n# NOTE: If this argument is not supplied, the [callback_name](#callback_name) argument must be.\n#\n# ### callback_name\n#\n# **Type:** `String?` **Default:** `nil`\n#\n# The name of the method that should be invoked as the callback for this constraint.\n#\n# NOTE: If this argument is not supplied, the [callback](#callback) argument must be.\n#\n# ## Optional Arguments\n#\n# NOTE: This constraint does not support a `message` argument.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\n#\n# # Usage\n#\n# The callback constraint supports two callback methods when validating objects, and one callback method when using the constraint directly.\n#\n# ## Instance Methods\n#\n# To define an instance callback method, apply the `@[Assert::Callback]` method to a public instance method defined within an object.\n# This method should accept two arguments: the `AVD::ExecutionContextInterface` to which violations should be added,\n# and the `AVD::Constraint@payload` from the related constraint.\n#\n# More than one callback method can exist on a type, and the method name does not have to be `validate`.\n#\n# ```\n# class Example\n#   include AVD::Validatable\n#\n#   SPAM_DOMAINS = [\"fake.com\", \"spam.net\"]\n#\n#   def initialize(@domain_name : String); end\n#\n#   @[Assert::Callback]\n#   def validate(context : AVD::ExecutionContextInterface, payload : Hash(String, String)?) : Nil\n#     # Validate that the `domain_name` is not spammy.\n#     return unless SPAM_DOMAINS.includes? @domain_name\n#\n#     context\n#       .build_violation(\"This domain name is not legit!\")\n#       .at_path(\"domain_name\")\n#       .add\n#   end\n# end\n# ```\n#\n# ## Class Methods\n#\n# The callback method can also be defined as a class method.\n# Since class methods do not have access to the related object instance, it is passed in as an argument.\n#\n# That argument is typed as `AVD::Constraints::Callback::Value` instance which exposes a `AVD::Constraints::Callback::Value#get`\n# method that can be used as an easier syntax than `.as`.\n#\n# ```\n# class Example\n#   include AVD::Validatable\n#\n#   SPAM_DOMAINS = [\"fake.com\", \"spam.net\"]\n#\n#   @[Assert::Callback]\n#   def self.validate(value : AVD::Constraints::Callback::ValueContainer, context : AVD::ExecutionContextInterface, payload : Hash(String, String)?) : Nil\n#     # Get the object from the value, typed as our `Example` class.\n#     object = value.get self\n#\n#     # Validate that the `domain_name` is not spammy.\n#     return unless SPAM_DOMAINS.includes? object.domain_name\n#\n#     context\n#       .build_violation(\"This domain name is not legit!\")\n#       .at_path(\"domain_name\")\n#       .add\n#   end\n#\n#   def initialize(@domain_name : String); end\n#\n#   getter domain_name : String\n# end\n# ```\n#\n# ## Procs/Blocks\n#\n# When working with constraints in a non object context, a callback passed in as a proc/block.\n# `AVD::Constraints::Callback::CallbackProc` alias can be used to more easily create a callback proc.\n# `AVD::Constraints::Callback.with_callback` can be used to create a callback constraint, using the block as the callback proc.\n# See the related types for more information.\n#\n# Proc/block based callbacks operate similarly to [Class Methods][Athena::Validator::Constraints::Callback--class-methods] in that they receive the value as an argument.\nclass Athena::Validator::Constraints::Callback < Athena::Validator::Constraint\n  # :nodoc:\n  abstract struct ValueContainer\n    abstract def type_name : String\n\n    def inspect(io : IO) : Nil\n      io << \"#<AVD::Constraints::Callback::Value(\" << self.type_name << \")>\"\n    end\n  end\n\n  # Wrapper type to allow passing arbitrarily typed values as arguments in the `AVD::Constraints::Callback::CallbackProc`.\n  record Value(T) < ValueContainer, value : T do\n    forward_missing_to @value\n\n    # :inherit:\n    def type_name : String\n      {{ T.stringify }}\n    end\n\n    # Returns the value as `T`.\n    #\n    # If used inside a `AVD::Constraints::Callback@class-method`.\n    #\n    # ```\n    # # Get the wrapped value as the type of the current class.\n    # object = value.get self\n    # ```\n    #\n    # If used inside a `AVD::Constraints::Callback@procsblocks`.\n    # ```\n    # # Get the wrapped value as the expected type.\n    # value = value.get Int32\n    #\n    # # Alternatively, can use normal Crystal semantics for narrowing the type.\n    # value = value.value\n    #\n    # case value\n    # when Int32 then \"value is Int32\"\n    # when String then \"value is String\"\n    # end\n    def get(as _t : T.class) : T forall T\n      @value.as?(T).not_nil!\n    end\n\n    def ==(other) : Bool\n      @value == other\n    end\n  end\n\n  # Convenience method for creating a `AVD::Constraints::Callback` with\n  # the given *&block* as the callback.\n  #\n  # ```\n  # # Instantiate a callback constraint, using the block as the callback\n  # constraint = AVD::Constraints::Callback.with_callback do |value, context, payload|\n  #   next if (value = value.get(Int32)).even?\n  #\n  #   context.add_violation \"This value should be even.\"\n  # end\n  # ```\n  def self.with_callback(**args, &block : AVD::Constraints::Callback::ValueContainer, AVD::ExecutionContextInterface, Hash(String, String)? ->) : AVD::Constraints::Callback\n    new **args, callback: block\n  end\n\n  # Convenience alias to make creating `AVD::Constraints::Callback` procs easier.\n  #\n  # ```\n  # # Create a proc to handle the validation\n  # callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, payload|\n  #   return if (value = value.get(Int32)).even?\n  #\n  #   context.add_violation \"This value should be even.\"\n  # end\n  #\n  # # Instantiate a callback constraint with this proc\n  # constraint = AVD::Constraints::Callback.new callback: callback\n  # ```\n  alias CallbackProc = Proc(AVD::Constraints::Callback::ValueContainer, AVD::ExecutionContextInterface, Hash(String, String)?, Nil)\n\n  # Returns the name of the callback method this constraint should invoke.\n  getter callback_name : String?\n\n  # Returns the proc that this constraint should invoke.\n  getter callback : AVD::Constraints::Callback::CallbackProc?\n\n  def initialize(\n    @callback : AVD::Constraints::Callback::CallbackProc? = nil,\n    @callback_name : String? = nil,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    raise AVD::Exception::Logic.new \"either `callback` or `callback_name` must be provided.\" if @callback.nil? && @callback_name.nil?\n\n    super \"\", groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Callback) : Nil\n      if value.is_a?(AVD::Validatable) && (name = constraint.callback_name) && (metadata = self.context.metadata) && (metadata.is_a?(AVD::Metadata::ClassMetadata))\n        metadata.invoke_callback name, value, self.context, constraint.payload\n      elsif callback = constraint.callback\n        callback.call Value.new(value), self.context, constraint.payload\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/choice.cr",
    "content": "# Validates that a value is one of a given set of valid choices;\n# can also be used to validate that each item in a collection is one of those valid values.\n#\n# ```\n# class User\n#   include AVD::Validatable\n#\n#   def initialize(@role : String); end\n#\n#   @[Assert::Choice([\"member\", \"moderator\", \"admin\"])]\n#   property role : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### choices\n#\n# **Type:** `Array(String | Number::Primitive | Symbol)`\n#\n# The choices that are considered valid.\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value is not a valid choice.`\n#\n# The message that will be shown if the value is not a valid choice and [multiple](#multiple) is `false`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ choices }}` - The available choices.\n#\n# ### multiple_message\n#\n# **Type:** `String` **Default:** `One or more of the given values is invalid.`\n#\n# The message that will be shown if one of the values is not a valid choice and [multiple](#multiple) is `true`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ choices }}` - The available choices.\n#\n# ### min_message\n#\n# **Type:** `String` **Default:** `You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.`\n#\n# The message that will be shown if too few choices are chosen as per the [range](#range) option.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ choices }}` - The available choices.\n# * `{{ limit }}` - If [multiple](#multiple) is true, enforces that at most this many values may be selected in order to be valid.\n#\n# ### max_message\n#\n# **Type:** `String` **Default:** `You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.`\n#\n# The message that will be shown if too many choices are chosen as per the [range](#range) option.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ choices }}` - The available choices.\n# * `{{ limit }}` - If [multiple](#multiple) is true, enforces that no more than this many values may be selected in order to be valid.\n#\n# ### range\n#\n# **Type:** `::Range?` **Default:** `nil`\n#\n# If [multiple](#multiple) is true, is used to define the \"range\" of how many choices must be valid for the value to be considered valid.\n# For example, if set to `(3..)`, but there are only 2 valid items in the input enumerable then validation will fail.\n#\n# Beginless/endless ranges can be used to define only a lower/upper bound.\n#\n# ### multiple\n#\n# **Type:** `Bool` **Default:** `false`\n#\n# If `true`, the input value is expected to be an `Enumerable` instead of a single scalar value.\n# The constraint will check each item in the enumerable is valid choice.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Choice < Athena::Validator::Constraint\n  NO_SUCH_CHOICE_ERROR = \"c7398ea5-e787-4ee9-9fca-5f2c130614d6\"\n  TOO_FEW_ERROR        = \"3573357d-c9a8-4633-a742-c001086fd5aa\"\n  TOO_MANY_ERROR       = \"91d0d22b-a693-4b9c-8b41-bc6392cf89f4\"\n\n  @@error_names = {\n    NO_SUCH_CHOICE_ERROR => \"NO_SUCH_CHOICE_ERROR\",\n    TOO_FEW_ERROR        => \"TOO_FEW_ERROR\",\n    TOO_MANY_ERROR       => \"TOO_MANY_ERROR\",\n  }\n\n  getter choices : Array(String | Number::Primitive | Symbol)\n\n  getter multiple_message : String\n  getter min_message : String\n  getter max_message : String\n\n  getter min : Number::Primitive?\n  getter max : Number::Primitive?\n\n  getter? multiple : Bool\n\n  def self.new(\n    choices : Array(String | Number::Primitive | Symbol),\n    message : String = \"This value is not a valid choice.\",\n    multiple_message : String = \"One or more of the given values is invalid.\",\n    min_message : String = \"You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.\",\n    max_message : String = \"You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.\",\n    multiple : Bool = false,\n    range : ::Range? = nil,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    new choices.map(&.as(String | Number::Primitive | Symbol)), message, multiple_message, min_message, max_message, multiple, range.try(&.begin), range.try(&.end), groups, payload\n  end\n\n  private def initialize(\n    @choices : Array(String | Number::Primitive | Symbol),\n    message : String,\n    @multiple_message : String,\n    @min_message : String,\n    @max_message : String,\n    @multiple : Bool,\n    @min : Number::Primitive?,\n    @max : Number::Primitive?,\n    groups : Array(String) | String | Nil,\n    payload : Hash(String, String)?,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : Enumerable?, constraint : AVD::Constraints::Choice) : Nil\n      return if value.nil?\n\n      self.raise_invalid_type(value, \"Enumerable\") unless constraint.multiple?\n\n      choices = constraint.choices\n\n      value.each do |v|\n        unless choices.includes? v\n          self\n            .context\n            .build_violation(constraint.multiple_message, NO_SUCH_CHOICE_ERROR, v)\n            .add_parameter(\"{{ choices }}\", choices)\n            .invalid_value(v)\n            .add\n\n          return\n        end\n      end\n\n      size = value.size\n\n      if (limit = constraint.min) && (size < limit)\n        self\n          .context\n          .build_violation(constraint.min_message, TOO_FEW_ERROR, value)\n          .add_parameter(\"{{ limit }}\", limit)\n          .add_parameter(\"{{ choices }}\", choices)\n          .plural(limit.to_i)\n          .invalid_value(value)\n          .add\n\n        return\n      end\n\n      if (limit = constraint.max) && (size > limit)\n        self\n          .context\n          .build_violation(constraint.max_message, TOO_MANY_ERROR, value)\n          .add_parameter(\"{{ limit }}\", limit)\n          .add_parameter(\"{{ choices }}\", choices)\n          .plural(limit.to_i)\n          .invalid_value(value)\n          .add\n\n        return\n      end\n    end\n\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Choice) : Nil\n      return if value.nil?\n\n      self.raise_invalid_type(value, \"Enumerable\") if constraint.multiple? && !value.is_a?(Enumerable)\n\n      return if constraint.choices.includes? value\n\n      self\n        .context\n        .build_violation(constraint.message, NO_SUCH_CHOICE_ERROR, value)\n        .add_parameter(\"{{ choices }}\", constraint.choices)\n        .add\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/collection.cr",
    "content": "# Can be used with any `Enumerable({K, V})` to validate each key in a different way.\n# For example validating the `email` key via `AVD::Constraints::Email`, and the `inventory` key with the `AVD::Constraints::Range` constraint.\n# The collection constraint can also ensure that certain collection keys are present and that extra keys are not present.\n#\n# TODO: Update it to be `Mappable` when/if https://github.com/crystal-lang/crystal/issues/10886 is implemented.\n#\n# # Usage\n#\n# ```\n# data = {\n#   \"email\"           => \"...\",\n#   \"email_signature\" => \"...\",\n# }\n# ```\n#\n# For example, say you want to ensure the *email* field is a valid email,\n# and that their *email_signature* is not blank nor over 100 characters long;\n# without creating a dedicated class to represent the hash.\n#\n# ```\n# constraint = AVD::Constraints::Collection.new({\n#   \"email\"           => AVD::Constraints::Email.new,\n#   \"email_signature\" => [\n#     AVD::Constraints::NotBlank.new,\n#     AVD::Constraints::Size.new(..100, max_message: \"Your signature is too long\"),\n#   ],\n# })\n#\n# validator.validate data, constraint\n# ```\n#\n# The collection constraint expects a hash representing the keys in the collection, with the value being which constraint(s) should be executed against its value.\n# From there we can go ahead and validate our data hash against the constraint.\n#\n# ## Presence and Absence of Fields\n#\n# This constraint also will return validation errors if any keys of a collection are missing, or if there are any unrecognized keys in the collection.\n# This can be customized via the [allow_extra_fields](#allow_extra_fields) and [allow_missing_fields](#allow_missing_fields) configuration options respectively.\n#\n# If the latter was set to `true`, then either *email* or *email_signature* could be missing from the data hash, and no validation errors would occur.\n#\n# ## Required and Optional Constraints\n#\n# Each field in the collection is assumed to be required by default.\n# While you could make everything optional via the setting [allow_missing_fields](#allow_missing_fields) to `true`,\n# this is less than ideal in some cases when you only want to affect a single key, or a subset of keys.\n#\n# In this case, a single constraint, or array of constraints, can be wrapped via the `AVD::Constraints::Optional` or `AVD::Constraints::Required` constraints.\n# For example, if you wanted to require that the *personal_email* field is not blank and is a valid email,\n# but also have an optional *alternate_email* field that must be a valid email if supplied, you could set things up like:\n#\n# ```\n# constraint = AVD::Constraints::Collection.new({\n#   \"personal_email\" => AVD::Constraints::Required.new([\n#     AVD::Constraints::NotBlank.new,\n#     AVD::Constraints::Email.new,\n#   ]),\n#   \"alternate_email\" => AVD::Constraints::Optional.new([\n#     AVD::Constraints::Email.new,\n#   ] of AVD::Constraint),\n# })\n# ```\n#\n# In this way, even if [allow_missing_fields](#allow_missing_fields) is `true`, you would be able to omit *alternate_email* since it is optional.\n# However, since *personal_email* is required, the not blank assertion will still be applied and a violation will occur if it is missing.\n#\n# ## Groups\n#\n# Any groups defined in nested constraints are automatically added to the collection constraint itself such that it can be traversed for all nested groups.\n#\n# ```\n# constraint = AVD::Constraints::Collection.new({\n#   \"name\"  => AVD::Constraints::NotBlank.new(groups: \"basic\"),\n#   \"email\" => AVD::Constraints::NotBlank.new(groups: \"contact\"),\n# })\n#\n# constraint.groups # => [\"basic\", \"contact\"]\n# ```\n#\n# TIP: The collection constraint can be used to validate form data via a [URI::Param](https://crystal-lang.org/api/URI/Params.html) instance.\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### fields\n#\n# **Type:** `Hash(String, AVD::Constraint | Array(AVD::Constraint))`\n#\n# A hash defining the keys in the collection, and for which constraint(s) should be executed against them.\n#\n# ## Optional Arguments\n#\n# ### allow_extra_fields\n#\n# **Type:** `Bool` **Default:** `false`\n#\n# If extra fields in the collection other than those defined within [fields](#fields) are allowed. By default extra fields will result in a validation error.\n#\n# ### allow_missing_fields\n#\n# **Type:** `Bool` **Default:** `false`\n#\n# If the fields defined within [fields](#fields) are allowed to be missing. By default a validation error will be returned if one or more field is missing.\n#\n# ### extra_fields_message\n#\n# **Type:** `String` **Default:** `This field was not expected.`\n#\n# The message that will be shown if [allow_extra_fields](#allow_extra_fields) is `false` and a field in the collection was not defined within `#fields`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ field }}` - The name of the extra field.\n#\n# ### missing_fields_message\n#\n# **Type:** `String` **Default:** `This field is missing.`\n#\n# The message that will be shown if [allow_missing_fields](#allow_missing_fields) is `false` and a field defined within `#fields` is missing from the collection.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ field }}` - The name of the missing field.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Collection < Athena::Validator::Constraints::Composite\n  MISSING_FIELD_ERROR = \"af103ee5-3bcb-448e-98ad-b4ef76c05060\"\n  NO_SUCH_FIELD_ERROR = \"70e60467-4078-4f92-acf9-d1e6683d0922\"\n\n  @@error_names = {\n    MISSING_FIELD_ERROR => \"MISSING_FIELD_ERROR\",\n    NO_SUCH_FIELD_ERROR => \"NO_SUCH_FIELD_ERROR\",\n  }\n\n  getter? allow_extra_fields : Bool\n  getter? allow_missing_fields : Bool\n\n  getter extra_fields_message : String\n  getter missing_fields_message : String\n\n  def initialize(\n    fields : Hash(String, AVD::Constraint | Array(AVD::Constraint)),\n    @allow_extra_fields : Bool = false,\n    @allow_missing_fields : Bool = false,\n    @extra_fields_message : String = \"This field was not expected.\",\n    @missing_fields_message : String = \"This field is missing.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    constraints = Hash(String, AVD::Constraint).new\n\n    fields.each do |key, value|\n      constraints[key] = !value.is_a?(AVD::Constraints::Optional) && !value.is_a?(AVD::Constraints::Required) ? AVD::Constraints::Required.new(value) : value\n    end\n\n    super constraints, \"\", groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    #\n    # TODO: Support https://github.com/crystal-lang/crystal/issues/10886 when/if implemented.\n    def validate(value : Enumerable({K, V})?, constraint : AVD::Constraints::Collection) : Nil forall K, V\n      return if value.nil?\n\n      context = self.context\n\n      constraint.constraints.each do |field, field_constraint|\n        field_constraint = field_constraint.as AVD::Constraints::Existence\n\n        if value.has_key? field\n          if field_constraint.constraints.size > 0\n            context\n              .validator\n              .in_context(context)\n              .at_path(\"[#{field}]\")\n              .validate(value[field], field_constraint.constraints.values)\n          end\n        elsif !field_constraint.is_a?(AVD::Constraints::Optional) && !constraint.allow_missing_fields?\n          context\n            .build_violation(constraint.missing_fields_message, MISSING_FIELD_ERROR)\n            .at_path(\"[#{field}]\")\n            .add_parameter(\"{{ field }}\", field)\n            .invalid_value(nil)\n            .add\n        end\n      end\n\n      unless constraint.allow_extra_fields?\n        value.each do |field, field_value|\n          unless constraint.constraints.has_key? field\n            context\n              .build_violation(constraint.extra_fields_message, NO_SUCH_FIELD_ERROR)\n              .at_path(\"[#{field}]\")\n              .add_parameter(\"{{ field }}\", field)\n              .invalid_value(field_value)\n              .add\n          end\n        end\n      end\n    end\n\n    # :inherit:\n    def validate(actual : _, expected : _) : NoReturn\n      self.raise_invalid_type actual, \"Enumerable({K, V})\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/composite.cr",
    "content": "# A constraint composed of other constraints.\n# handles normalizing the groups of the nested constraints, via the following algorithm:\n#\n# * If groups are passed explicitly to the composite constraint, but\n#   not to the nested constraints, the options of the composite\n#   constraint are copied to the nested constraints\n# * If groups are passed explicitly to the nested constraints, but not\n#   to the composite constraint, the groups of all nested constraints\n#   are merged and used as groups for the composite constraint\n# * If groups are passed explicitly to both the composite and its nested\n#   constraints, the groups of the nested constraints must be a subset\n#   of the groups of the composite constraint.\n#\n# NOTE: You most likely want to use `AVD::Constraints::Compound` instead of this type.\nabstract class Athena::Validator::Constraints::Composite < Athena::Validator::Constraint\n  alias Type = Array(AVD::Constraint) | AVD::Constraint | Enumerable({String | Int32, AVD::Constraint})\n\n  getter constraints : Enumerable({String | Int32, AVD::Constraint})\n\n  def initialize(\n    constraints : AVD::Constraints::Composite::Type,\n    message : String,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n\n    constraints = case constraints\n                  when AVD::Constraint then {0 => constraints} of String | Int32 => AVD::Constraint\n                  when Array\n                    hash = Hash(String | Int32, AVD::Constraint).new initial_capacity: constraints.size\n\n                    constraints.each_with_index do |v, k|\n                      hash[k] = v\n                    end\n\n                    hash\n                  else\n                    constraints.transform_keys(&.as(String | Int32))\n                  end\n\n    constraints.each_value do |c|\n      raise AVD::Exception::Logic.new \"The '#{AVD::Constraints::Valid}' constraint cannot be nested inside a '#{self.class}' constraint.\" if c.is_a? AVD::Constraints::Valid\n    end\n\n    if groups.nil?\n      merged_groups = Hash(String, Bool).new\n\n      constraints.each_value do |constraint|\n        constraint.groups.each do |group|\n          merged_groups[group] = true\n        end\n      end\n\n      @groups = merged_groups.empty? ? [AVD::Constraint::DEFAULT_GROUP] : merged_groups.keys\n      @constraints = constraints\n\n      return\n    end\n\n    constraints.each_value do |constraint|\n      if !constraint.@groups.nil?\n        unless (excess_groups = (constraint.groups - self.groups)).empty?\n          raise AVD::Exception::Logic.new \"The group(s) '#{excess_groups.join \", \"}' passed to the constraint '#{constraint.class}' should also be passed to its containing constraint '#{self.class}'.\"\n        end\n      else\n        constraint.groups = self.groups\n      end\n    end\n\n    @constraints = constraints\n  end\n\n  def add_implicit_group(group : String) : Nil\n    super group\n\n    @constraints.each_value &.add_implicit_group(group)\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/compound.cr",
    "content": "# Allows creating a custom set of reusable constraints, representing rules to use consistently across your application.\n#\n# NOTE: See the [custom constraint][Athena::Validator::Constraint--custom-constraints] documentation for information on defining custom constraints.\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# NOTE: This constraint does not support a `message` argument.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\n#\n# # Usage\n#\n# This constraint is not used directly on its own;\n# instead it's used to create another constraint.\n#\n# ```\n# # Define a compound constraint to centralize the logic to validate a password.\n# #\n# # NOTE: The constraint _MUST_ be defined within the `AVD::Constraints` namespace for implementation reasons.  This may change in the future.\n# class AVD::Constraints::ValidPassword < AVD::Constraints::Compound\n#   # Define a method that returns an array of the constraints we want to be a part of `self`.\n#   def constraints : Type\n#     [\n#       AVD::Constraints::NotBlank.new,       # Not empty/null\n#       AVD::Constraints::Size.new(12..),     # At least 12 characters longs\n#       AVD::Constraints::Regex.new(/^\\d.*/), # Must start with a number\n#     ]\n#   end\n# end\n# ```\n#\n# We can then use this constraint as we would any other.\n#\n# Either as an annotation\n#\n# ```\n# @[Assert::ValidPassword]\n# getter password : String\n# ```\n# or directly.\n#\n# ```\n# constraint = AVD::Constraints::ValidPassword.new\n# ```\nabstract class Athena::Validator::Constraints::Compound < Athena::Validator::Constraints::Composite\n  def initialize(\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super self.constraints, \"\", groups, payload\n  end\n\n  def validated_by : AVD::ConstraintValidator.class\n    AVD::Constraints::Compound::Validator\n  end\n\n  abstract def constraints : AVD::Constraints::Composite::Type\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Compound) : Nil\n      context = self.context\n\n      validator = context.validator.in_context context\n\n      validator.validate value, constraint.@constraints.values\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/count.cr",
    "content": "# Validates that the `#size` of an `Indexable` value is between some minimum and maximum.\n#\n# ```\n# class User\n#   include AVD::Validatable\n#\n#   def initialize(@emails : Array(String)); end\n#\n#   @[Assert::Count(1..5)]\n#   property emails : Array(String)\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### range\n#\n# **Type:** `::Range`\n#\n# The `::Range` that defines the minimum and maximum values, if any.\n# An endless range can be used to only have a minimum or maximum.\n#\n# ## Optional Arguments\n#\n# NOTE: This constraint does not support a `message` argument.\n#\n# ### exact_message\n#\n# **Type:** `String` **Default:** `This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.`\n#\n# The message that will be shown if min and max values are equal and the underlying collection’s count is not exactly this value.\n# The message is pluralized depending on how many elements the underlying value has.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ count }}` - The current collection count\n# * `{{ limit }}` - The exact expected collection count\n#\n# ### min_message\n#\n# **Type:** `String` **Default:** `This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.`\n#\n# The message that will be shown if the underlying collection’s count is less than the min.\n# The message is pluralized depending on how many elements the underlying value has.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ count }}` - The current collection count\n# * `{{ limit }}` - The lower limit\n#\n# ### max_message\n#\n# **Type:** `String` **Default:** `This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.`\n#\n# The message that will be shown if the underlying collection’s count is greater than the max.\n# The message is pluralized depending on how many elements the underlying value has.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ count }}` - The current collection count\n# * `{{ limit }}` - The upper limit\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Count < Athena::Validator::Constraint\n  TOO_FEW_ERROR         = \"07f04a04-c346-4983-9868-602c62a5d0c1\"\n  TOO_MANY_ERROR        = \"c35d873a-8095-4710-88d0-de68bce36055\"\n  NOT_EQUAL_COUNT_ERROR = \"ee29a9b5-924b-42dd-a810-044c86803244\"\n\n  @@error_names = {\n    TOO_FEW_ERROR         => \"TOO_FEW_ERROR\",\n    TOO_MANY_ERROR        => \"TOO_MANY_ERROR\",\n    NOT_EQUAL_COUNT_ERROR => \"NOT_EQUAL_COUNT_ERROR\",\n  }\n\n  getter min : Int32?\n  getter max : Int32?\n  getter min_message : String\n  getter max_message : String\n  getter exact_message : String\n\n  def self.new(\n    range : ::Range,\n    min_message : String = \"This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.\",\n    max_message : String = \"This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.\",\n    exact_message : String = \"This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    new range.begin, range.end, min_message, max_message, exact_message, groups, payload\n  end\n\n  private def initialize(\n    @min : Int32?,\n    @max : Int32?,\n    @min_message : String = \"This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.\",\n    @max_message : String = \"This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.\",\n    @exact_message : String = \"This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super \"\", groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : Indexable, constraint : AVD::Constraints::Count) : Nil\n      return if value.nil?\n\n      count = value.size\n\n      min = constraint.min\n      max = constraint.max\n\n      if max && count > max\n        exactly_option_enabled = min == max\n\n        self\n          .context\n          .build_violation(\n            exactly_option_enabled ? constraint.exact_message : constraint.max_message,\n            exactly_option_enabled ? NOT_EQUAL_COUNT_ERROR : TOO_MANY_ERROR,\n            value\n          )\n          .add_parameter(\"{{ count }}\", count)\n          .add_parameter(\"{{ limit }}\", max)\n          .invalid_value(value)\n          .plural(max)\n          .add\n      end\n\n      if min && count < min\n        exactly_option_enabled = min == max\n\n        self\n          .context\n          .build_violation(\n            exactly_option_enabled ? constraint.exact_message : constraint.min_message,\n            exactly_option_enabled ? NOT_EQUAL_COUNT_ERROR : TOO_FEW_ERROR,\n            value\n          )\n          .add_parameter(\"{{ count }}\", count)\n          .add_parameter(\"{{ limit }}\", min)\n          .invalid_value(value)\n          .plural(min)\n          .add\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/email.cr",
    "content": "# Validates that a value is a valid email address.\n# The underlying value is converted to a string via `#to_s` before being validated.\n#\n# NOTE: As with most other constraints, `nil` and empty strings are considered valid values, in order to allow the value to be optional.\n# If the value is required, consider combining this constraint with `AVD::Constraints::NotBlank`.\n#\n# ```\n# class User\n#   include AVD::Validatable\n#\n#   def initialize(@email : String); end\n#\n#   @[Assert::Email]\n#   property email : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### mode\n#\n# **Type:** `AVD::Constraints::Email::Mode` **Default:** `AVD::Constraints::Email::Mode::HTML5`\n#\n# Defines the pattern that should be used to validate the email address.\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value is not a valid email address.`\n#\n# The message that will be shown if the value is not a valid email address.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Email < Athena::Validator::Constraint\n  # Determines _how_ the email address should be validated.\n  enum Mode\n    # Validates the email against the [HTML5 input pattern](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address), but requires a [TLD](https://en.wikipedia.org/wiki/Top-level_domain) to be present.\n    HTML5\n\n    # Same as `HTML5`, but follows the pattern exactly, allowing there to be no [TLD](https://en.wikipedia.org/wiki/Top-level_domain).\n    HTML5_ALLOW_NO_TLD\n\n    # TODO: Implement this mode.\n    # STRICT\n\n    # Returns the `::Regex` pattern for `self`.\n    def pattern : ::Regex\n      case self\n      in .html5?              then /^[a-zA-Z0-9.!\\#$\\%&\\'*+\\\\\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/\n      in .html5_allow_no_tld? then /^[a-zA-Z0-9.!\\#$\\%&\\'*+\\\\\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/\n      end\n    end\n  end\n\n  INVALID_FORMAT_ERROR = \"ad9d877d-9ad1-4dd7-b77b-e419934e5910\"\n\n  @@error_names = {\n    INVALID_FORMAT_ERROR => \"INVALID_FORMAT_ERROR\",\n  }\n\n  getter mode : AVD::Constraints::Email::Mode\n\n  def initialize(\n    @mode : AVD::Constraints::Email::Mode = :html5,\n    message : String = \"This value is not a valid email address.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Email) : Nil\n      value = value.to_s\n\n      return if value.nil? || value.empty?\n      return if value.matches? constraint.mode.pattern\n\n      self.context.add_violation constraint.message, INVALID_FORMAT_ERROR, value\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/equal_to.cr",
    "content": "# Validates that a value is equal to another.\n#\n# ```\n# class Project\n#   include AVD::Validatable\n#\n#   def initialize(@name : String); end\n#\n#   @[Assert::EqualTo(\"Athena\")]\n#   property name : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### value\n#\n# Defines the value that the value being validated should be compared to.\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be equal to {{ compared_value }}.`\n#\n# The message that will be shown if the value is not equal to the comparison value.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::EqualTo(ValueType) < Athena::Validator::Constraint\n  include Athena::Validator::Constraints::AbstractComparison(ValueType)\n\n  NOT_EQUAL_ERROR = \"47d83d11-15d5-4267-b469-1444f80fd169\"\n\n  @@error_names = {\n    NOT_EQUAL_ERROR => \"NOT_EQUAL_ERROR\",\n  }\n\n  # :inherit:\n  def default_error_message : String\n    \"This value should be equal to {{ compared_value }}.\"\n  end\n\n  class Validator < Athena::Validator::Constraints::ComparisonValidator\n    # :inherit:\n    def compare_values(actual : _, expected : _) : Bool\n      actual == expected\n    end\n\n    # :inherit:\n    def error_code : String\n      NOT_EQUAL_ERROR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/existence.cr",
    "content": "# See [AVD::Constraints::Collection][Athena::Validator::Constraints::Collection--required-and-optional-constraints] for more information.\nabstract class Athena::Validator::Constraints::Existence < Athena::Validator::Constraints::Composite\n  def initialize(\n    constraints : Array(AVD::Constraint) | AVD::Constraint = [] of AVD::Constraint,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super constraints, \"\", groups, payload\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/file.cr",
    "content": "require \"athena-mime\"\nrequire \"athena-http\"\n\n# Validates that a value is a valid file.\n# If the underlying value is a [::File](https://crystal-lang.org/api/File.html), then its path is used as the value.\n# Otherwise the value is converted to a string via `#to_s` before being validated, which is assumed to be a path to a file.\n#\n# NOTE: As with most other constraints, `nil` and empty strings are considered valid values, in order to allow the value to be optional.\n# If the value is required, consider combining this constraint with `AVD::Constraints::NotBlank`.\n#\n# ```\n# class Profile\n#   include AVD::Validatable\n#\n#   def initialize(@resume : ::File); end\n#\n#   @[Assert::File]\n#   property resume : ::File\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### max_size\n#\n# **Type:** `Int | String | Nil` **Default:** `nil`\n#\n# Defines that maximum size the file must be in order to be considered valid.\n# The value may be an integer representing the size in bytes, or a format string in one of the following formats:\n#\n# | Suffix | Unit Name | Value           | Example |\n# | :----- | :-------- | :-------------- | :------ |\n# | (none) | byte      | 1 byte          | `4096`  |\n# | `k`    | kilobyte  | 1,000 bytes     | `\"200k\"`  |\n# | `M`    | megabyte  | 1,000,000 bytes | `\"2M\"`    |\n# | `Ki`   | kibibyte  | 1,024 bytes     | `\"32Ki\"`  |\n# | `Mi`   | mebibyte  | 1,048,576 bytes | `\"8Mi\"`   |\n#\n# ### mime_types\n#\n# **Type:** `Enumerable(String)?` **Default:** `nil`\n#\n# If set, allows checking that the MIME type of the file is one of an allowed set of types.\n# This value is ignored if the MIME type of the file could not be determined.\n#\n# ### binary_format\n#\n# **Type:** `Bool?` **Default:** `nil`\n#\n# When `true`, the sizes will be displayed in messages with binary-prefixed units (KiB, MiB).\n# When `false`, the sizes will be displayed with SI-prefixed units (kB, MB).\n# When `nil`, then the binaryFormat will be guessed from the value defined in the [max_size](#max_size) option.\n#\n# ### max_size_message\n#\n# **Type:** `String` **Default:** `The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.`\n#\n# The message that will be shown if the file is greater than the [max_size](#max_size).\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ file }}` - Absolute path to the invalid file.\n# * `{{ limit }}` - Maximum file size allowed.\n# * `{{ name }}` - Basename of the invalid file.\n# * `{{ size }}` - The size of the invalid file.\n# * `{{ suffix }}` - Suffix for the used file size unit.\n#\n# ### not_found_message\n#\n# **Type:** `String` **Default:** `The file could not be found.`\n#\n# The message that will be shown if no file could be found at the given path.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ file }}` - Absolute path to the invalid file.\n#\n# ### empty_message\n#\n# **Type:** `String` **Default:** `An empty file is not allowed.`\n#\n# The message that will be shown if the file is empty.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ file }}` - Absolute path to the invalid file.\n# * `{{ name }}` - Basename of the invalid file.\n#\n# ### not_readable_message\n#\n# **Type:** `String` **Default:** `The file is not readable.`\n#\n# The message that will be shown if the file is not readable.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ file }}` - Absolute path to the invalid file.\n# * `{{ name }}` - Basename of the invalid file.\n#\n# ### mime_type_message\n#\n# **Type:** `String` **Default:** `The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.`\n#\n# The message that will be shown if the MIME type of the file is not one of the valid [mime_types](#mime_types).\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ file }}` - Absolute path to the invalid file.\n# * `{{ name }}` - Basename of the invalid file.\n# * `{{ type }}` - The MIME type of the invalid file.\n# * `{{ types }}` - The list of allowed MIME types.\n#\n# ### upload_file_size_message\n#\n# **Type:** `String` **Default:** `The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.`\n#\n# The message that will be shown if the uploaded file is larger than the configured [max allowed size](/Framework/Bundle/Schema/FileUploads/#Athena::Framework::Bundle::Schema::FileUploads#max_file_size).\n# See the [Getting Started](/getting_started/routing/#file-uploads) docs for more information.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ limit }}` - The maximum file size allowed.\n# * `{{ suffix }}` - Suffix for the used file size unit.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::File < Athena::Validator::Constraint\n  NOT_FOUND_ERROR         = \"b6ae563c-4aec-4dfa-b268-2bb282912ed8\"\n  NOT_READABLE_ERROR      = \"e9f18a3d-f968-469f-868e-2331c8c982c2\"\n  EMPTY_ERROR             = \"de1a4b3c-a69f-46bd-b017-4a60361a1765\"\n  TOO_LARGE_ERROR         = \"4ce61d7c-43a0-44c2-bfe0-a59072b6cd17\"\n  INVALID_MIME_TYPE_ERROR = \"96c8591c-e990-48f6-b82b-75c878ae9fd9\"\n  UPLOAD_FILE_SIZE_ERROR  = \"6b06e7c7-2f21-46ef-b6ec-1dac08a1af7e\"\n\n  private KB_BYTES  =     1_000\n  private MB_BYTES  = 1_000_000\n  private KIB_BYTES =     1_024\n  private MIB_BYTES = 1_048_576\n\n  private SUFFICES = {\n            1 => \"bytes\",\n    KB_BYTES  => \"kB\",\n    MB_BYTES  => \"MB\",\n    KIB_BYTES => \"KiB\",\n    MIB_BYTES => \"MiB\",\n  }\n\n  @@error_names = {\n    NOT_FOUND_ERROR         => \"NOT_FOUND_ERROR\",\n    NOT_READABLE_ERROR      => \"NOT_READABLE_ERROR\",\n    EMPTY_ERROR             => \"EMPTY_ERROR\",\n    TOO_LARGE_ERROR         => \"TOO_LARGE_ERROR\",\n    INVALID_MIME_TYPE_ERROR => \"INVALID_MIME_TYPE_ERROR\",\n    UPLOAD_FILE_SIZE_ERROR  => \"UPLOAD_FILE_SIZE_ERROR\",\n  }\n\n  getter not_found_message : String\n  getter not_readable_message : String\n  getter empty_message : String\n  getter max_size_message : String\n  getter mime_type_message : String\n\n  getter upload_file_size_message : String\n\n  getter max_size : Int64?\n  getter mime_types : Set(String)?\n  getter! binary_format : Bool?\n\n  def initialize(\n    max_size : Int | String | Nil = nil,\n    @binary_format : Bool? = nil,\n    mime_types : Enumerable(String)? = nil,\n\n    @not_found_message : String = \"The file could not be found.\",\n    @not_readable_message : String = \"The file is not readable.\",\n    @empty_message : String = \"An empty file is not allowed.\",\n    @max_size_message : String = \"The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.\",\n    @mime_type_message : String = \"The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.\",\n\n    @upload_file_size_message : String = \"The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.\",\n\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super \"\", groups, payload\n\n    mime_types.try do |types|\n      @mime_types = types.to_set\n    end\n\n    max_size.try do |bytes|\n      @max_size = self.normalize_binary_format bytes\n    end\n  end\n\n  private def normalize_binary_format(max_size : Int) : Int64\n    @binary_format = @binary_format.nil? ? false : @binary_format\n    max_size.to_i64\n  end\n\n  private def normalize_binary_format(max_size : String) : Int64\n    if number = max_size.to_i64?\n      return self.normalize_binary_format number\n    end\n\n    factors = {\n      \"k\"  => 1_000,\n      \"ki\" => 1 << 10,\n      \"m\"  => 1000 * 1000,\n      \"mi\" => 1 << 20,\n      \"g\"  => 1000 * 1000 * 1000,\n      \"gi\" => 1 << 30,\n    }\n\n    if match = max_size.match /^(\\d++)(#{factors.each_key.join('|')})$/i\n      unit = match[2].downcase\n      @binary_format = @binary_format.nil? ? 2 == unit.size : @binary_format\n      return match[1].to_i64 * factors[unit].to_i64\n    end\n\n    raise AVD::Exception::InvalidArgument.new \"'#{max_size}' is not a valid maximum size.\"\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    #\n    # ameba:disable Metrics/CyclomaticComplexity\n    def validate(value : _, constraint : AVD::Constraints::File) : Nil\n      return if value.nil? || value == \"\"\n\n      if value.is_a?(Athena::HTTP::UploadedFile) && !value.valid?\n        case value.status\n        when .size_limit_exceeded?\n          max_allowed_file_size = Athena::HTTP::UploadedFile.max_file_size\n          if (constraint_max_size = constraint.max_size) && (constraint_max_size < max_allowed_file_size)\n            limit_in_bytes = constraint_max_size\n            binary_format = constraint.binary_format\n          else\n            limit_in_bytes = max_allowed_file_size\n            binary_format = (bf = constraint.binary_format?).nil? ? true : bf\n          end\n\n          _, limit_as_string, suffix = self.factorize_sizes 0, limit_in_bytes, binary_format\n\n          self\n            .context\n            .build_violation(constraint.upload_file_size_message, UPLOAD_FILE_SIZE_ERROR)\n            .add_parameter(\"{{ limit }}\", limit_as_string)\n            .add_parameter(\"{{ suffix }}\", suffix)\n            .add\n\n          return\n        end\n      end\n\n      path = case value\n             when Path                               then value\n             when ::File, Athena::HTTP::AbstractFile then value.path\n             else\n               value.to_s\n             end\n\n      unless ::File.file? path\n        self\n          .context\n          .build_violation(constraint.not_found_message, NOT_FOUND_ERROR)\n          .add_parameter(\"{{ file }}\", path)\n          .add\n\n        return\n      end\n\n      unless ::File::Info.readable? path\n        self\n          .context\n          .build_violation(constraint.not_readable_message, NOT_READABLE_ERROR)\n          .add_parameter(\"{{ file }}\", path)\n          .add\n\n        return\n      end\n\n      size_in_bytes = ::File.size path\n      base_name = value.is_a?(Athena::HTTP::UploadedFile) ? value.client_original_name : ::File.basename path\n\n      if size_in_bytes.zero?\n        self\n          .context\n          .build_violation(constraint.empty_message, EMPTY_ERROR)\n          .add_parameter(\"{{ file }}\", path)\n          .add_parameter(\"{{ name }}\", base_name)\n          .add\n\n        return\n      end\n\n      if (max_size_in_bytes = constraint.max_size) && size_in_bytes > max_size_in_bytes\n        size_as_string, limit_as_string, suffix = self.factorize_sizes size_in_bytes, max_size_in_bytes, constraint.binary_format\n\n        self\n          .context\n          .build_violation(constraint.max_size_message, TOO_LARGE_ERROR)\n          .add_parameter(\"{{ file }}\", path)\n          .add_parameter(\"{{ size }}\", size_as_string)\n          .add_parameter(\"{{ limit }}\", limit_as_string)\n          .add_parameter(\"{{ suffix }}\", suffix)\n          .add_parameter(\"{{ name }}\", base_name)\n          .add\n\n        return\n      end\n\n      if mime_types = constraint.mime_types\n        mime = if value.is_a? Athena::HTTP::AbstractFile\n                 value.mime_type\n               else\n                 AMIME::Types.default.guess_mime_type path\n               end\n\n        if mime\n          mime_types.each do |mime_type|\n            return if mime == mime_type\n\n            t, matched, _ = mime_type.partition \"/*\"\n\n            unless matched.blank?\n              t2, _, _ = mime.partition \"/\"\n\n              return if t2 == t\n            end\n          end\n        end\n\n        self\n          .context\n          .build_violation(constraint.mime_type_message, INVALID_MIME_TYPE_ERROR)\n          .add_parameter(\"{{ file }}\", path)\n          .add_parameter(\"{{ type }}\", mime)\n          .add_parameter(\"{{ types }}\", mime_types)\n          .add_parameter(\"{{ name }}\", base_name)\n          .add\n      end\n    end\n\n    private def more_decimals_than(double : String, number_of_decimals : Int) : Bool\n      double.size > double.to_f.round(2).to_s.size\n    end\n\n    # TODO: Can we use `#humaize_bytes` for this?\n    def factorize_sizes(size : Int, limit : Int, binary_format : Bool) : Tuple(String, String, String)\n      coef, coef_factor = binary_format ? {MIB_BYTES, KIB_BYTES} : {MB_BYTES, KB_BYTES}\n\n      # If limit < coef, limit_as_string could be < 1 with less than 3 decimals.\n      # In this case, we would end up displaying an allowed size < 1 (eg: 0.1 MB).\n      # It looks better to keep on factorizing (to display 100 kB for example).\n      while limit < coef\n        coef /= coef_factor\n      end\n\n      limit_as_string = (limit / coef).to_s\n\n      while self.more_decimals_than limit_as_string, 2\n        coef /= coef_factor\n        limit_as_string = (limit / coef).to_s\n      end\n\n      size_as_string = (size / coef).round(2).to_s\n\n      while size_as_string == limit_as_string\n        coef /= coef_factor\n        limit_as_string = (limit / coef).to_s\n        size_as_string = (size / coef).round(2).to_s\n      end\n\n      {size_as_string, limit_as_string, SUFFICES[coef]}\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/greater_than.cr",
    "content": "# Validates that a value is greater than another.\n#\n# ```\n# class Person\n#   include AVD::Validatable\n#\n#   def initialize(@age : Int64); end\n#\n#   @[Assert::GreaterThan(18)]\n#   property age : Int64\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### value\n#\n# **Type:** `Number | String | Time`\n#\n# Defines the value that the value being validated should be compared to.\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be greater than {{ compared_value }}.`\n#\n# The message that will be shown if the value is not greater than the comparison value.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::GreaterThan(ValueType) < Athena::Validator::Constraint\n  include Athena::Validator::Constraints::AbstractComparison(ValueType)\n\n  TOO_LOW_ERROR = \"a221096d-d125-44e8-a865-4270379ac11a\"\n\n  @@error_names = {\n    TOO_LOW_ERROR => \"TOO_LOW_ERROR\",\n  }\n\n  def default_error_message : String\n    \"This value should be greater than {{ compared_value }}.\"\n  end\n\n  class Validator < Athena::Validator::Constraints::ComparisonValidator\n    def compare_values(actual : Number, expected : Number) : Bool\n      actual > expected\n    end\n\n    def compare_values(actual : String, expected : String) : Bool\n      actual > expected\n    end\n\n    def compare_values(actual : Time, expected : Time) : Bool\n      actual > expected\n    end\n\n    # :inherit:\n    def compare_values(actual : _, expected : _) : NoReturn\n      # TODO: Support checking if arbitrarily typed values are actually comparable once `#responds_to?` supports it.\n      self.raise_invalid_type actual, \"Number | String | Time\"\n    end\n\n    # :inherit:\n    def error_code : String\n      TOO_LOW_ERROR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/greater_than_or_equal.cr",
    "content": "# Validates that a value is greater than or equal to another.\n#\n# ```\n# class Person\n#   include AVD::Validatable\n#\n#   def initialize(@age : Int64); end\n#\n#   @[Assert::GreaterThanOrEqual(18)]\n#   property age : Int64\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### value\n#\n# **Type:** `Number | String | Time`\n#\n# Defines the value that the value being validated should be compared to.\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be greater than or equal to {{ compared_value }}.`\n#\n# The message that will be shown if the value is not greater than or equal to the comparison value.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::GreaterThanOrEqual(ValueType) < Athena::Validator::Constraint\n  include Athena::Validator::Constraints::AbstractComparison(ValueType)\n\n  TOO_LOW_ERROR = \"e09e52d0-b549-4ba1-8b4e-420aad76f0de\"\n\n  @@error_names = {\n    TOO_LOW_ERROR => \"TOO_LOW_ERROR\",\n  }\n\n  def default_error_message : String\n    \"This value should be greater than or equal to {{ compared_value }}.\"\n  end\n\n  class Validator < Athena::Validator::Constraints::ComparisonValidator\n    def compare_values(actual : Number, expected : Number) : Bool\n      actual >= expected\n    end\n\n    def compare_values(actual : String, expected : String) : Bool\n      actual >= expected\n    end\n\n    def compare_values(actual : Time, expected : Time) : Bool\n      actual >= expected\n    end\n\n    # :inherit:\n    def compare_values(actual : _, expected : _) : NoReturn\n      # TODO: Support checking if arbitrarily typed values are actually comparable once `#responds_to?` supports it.\n      self.raise_invalid_type actual, \"Number | String | Time\"\n    end\n\n    # :inherit:\n    def error_code : String\n      TOO_LOW_ERROR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/group_sequence.cr",
    "content": "# :nodoc:\nannotation Athena::Validator::Annotations::GroupSequence; end\n\n# Allows validating your `AVD::Constraint@validation-groups` in steps.\n# I.e. only continue to the next group if all constraints in the first group are valid.\n#\n# ```\n# @[Assert::GroupSequence(\"User\", \"strict\")]\n# class User\n#   include AVD::Validatable\n#\n#   @[Assert::NotBlank]\n#   property name : String\n#\n#   @[Assert::NotBlank]\n#   property password : String\n#\n#   def initialize(@name : String, @password : String); end\n#\n#   @[Assert::IsTrue(message: \"Your password cannot be the same as your name.\", groups: \"strict\")]\n#   def is_safe_password? : Bool\n#     @name != @password\n#   end\n# end\n# ```\n#\n# In this case, it'll validate the `name` and `password` properties are not blank before validating they are not the same.\n# If either property is blank, the `is_safe_password?` validation will be skipped.\n#\n# NOTE: The `default` group is not allowed as part of a group sequence.\n#\n# NOTE: Calling `validate` with a group in the sequence, such as `strict`, will\n# cause violations to _ONLY_ use that group and not all groups within the sequence.\n# This is because the group sequence is now referred to as the `default` group.\n#\n# See `AVD::Constraints::GroupSequence::Provider` for a way to dynamically determine the sequence an object should use.\nstruct Athena::Validator::Constraints::GroupSequence\n  getter groups : Array(String | Array(String))\n\n  def self.new(groups : Array(String))\n    new groups.map &.as(String | Array(String))\n  end\n\n  def initialize(@groups : Array(String | Array(String))); end\n\n  # `AVD::Constraints::GroupSequence`s can be a good way to create efficient validations.\n  # However, since the sequence is static, it is not a very flexible solution.\n  #\n  # Group sequence providers allow the sequence to be dynamically determined at runtime.\n  # This allows running specific validations only when the object is in a specific state,\n  # such as validating a \"registered\" user differently than a non-registered user.\n  #\n  # ```\n  # class User\n  #   include AVD::Validatable\n  #\n  #   # Include the interface that informs the validator this object will provide its sequence.\n  #   include AVD::Constraints::GroupSequence::Provider\n  #\n  #   @[Assert::NotBlank]\n  #   property name : String\n  #\n  #   # Only validate the `email` property if the `#group_sequence` method includes \"registered\"\n  #   # Which can be determined using the current state of the object.\n  #   @[Assert::Email(groups: \"registered\")]\n  #   @[Assert::NotBlank(groups: \"registered\")]\n  #   property email : String?\n  #\n  #   def initialize(@name : String, @email : String); end\n  #\n  #   # Define a method that returns the sequence.\n  #   def group_sequence : Array(String | Array(String)) | AVD::Constraints::GroupSequence\n  #     # When returning a 1D array, if there is a vaiolation in any group\n  #     # the rest of the groups are not validated.  E.g. if `User` fails,\n  #     # `registered` and `api` are not validated:\n  #     return [\"User\", \"registered\", \"api\"]\n  #\n  #     # When returning a nested array, all groups included in each array are validated.\n  #     # E.g. if `User` fails, `Premium` is also validated (and you'll get its violations),\n  #     # but `api` will not be validated\n  #     return [[\"User\", \"registered\"], \"api\"]\n  #   end\n  # end\n  # ```\n  #\n  # See `AVD::Constraints::Sequentially` for a more straightforward method of applying constraints sequentially on a single property.\n  module Provider\n    abstract def group_sequence : Array(String | Array(String)) | AVD::Constraints::GroupSequence\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/image.cr",
    "content": "require \"athena-image_size\"\n\n# An extension of `AVD::Constraints::File` whose `AVD::Constraints::File#mime_types` and `AVD::Constraints::File#mime_type_message` are setup to specifically handle image files.\n# This constraint also provides the ability to validate against various image specific parameters.\n#\n# See `AVD::Constraints::File` for common documentation.\n#\n# ```\n# class Profile\n#   include AVD::Validatable\n#\n#   def initialize(@avatar : ::File); end\n#\n#   @[Assert::Image]\n#   property avatar : ::File\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### mime_types\n#\n# **Type:** `Enumerable(String)?` **Default:** `{\"image/*\"}`\n#\n# Requires the file to have a valid image MIME type.\n# See [IANA website](https://www.iana.org/assignments/media-types/media-types.xhtml) for the full listing.\n#\n# ### mime_type_message\n#\n# **Type:** `String` **Default:** `This file is not a valid image.`\n#\n# The message that will be shown if the file is not an image.\n#\n# ### min_height\n#\n# **Type:** `Int32` **Default:** `nil`\n#\n# If set, the image's height in pixels must be greater than or equal to this value.\n#\n# ### min_height_message\n#\n# **Type:** `String` **Default:** `The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.`\n#\n# The message that will be shown if the height of the image is less than `#min_height`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ height }}` - The current (invalid) height.\n# * `{{ min_height }}` - The minimum required height.\n#\n# ### max_height\n#\n# **Type:** `Int32` **Default:** `nil`\n#\n# If set, the image's height in pixels must be less than or equal to this value.\n#\n# ### max_height_message\n#\n# **Type:** `String` **Default:** `The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.`\n#\n# The message that will be shown if the height of the image exceeds `#max_height`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ height }}` - The current (invalid) height.\n# * `{{ max_height }}` - The maximum allowed height.\n#\n# ### min_width\n#\n# **Type:** `Int32` **Default:** `nil`\n#\n# If set, the image's width in pixels must be greater than or equal to this value.\n#\n# ### min_width_message\n#\n# **Type:** `String` **Default:** `The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.`\n#\n# The message that will be shown if the width of the image is less than `#min_width`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ width }}` - The current (invalid) width.\n# * `{{ min_width }}` - The minimum required width.\n#\n# ### max_width\n#\n# **Type:** `Int32` **Default:** `nil`\n#\n# If set, the image's width in pixels must be less than or equal to this value.\n#\n# ### max_width_message\n#\n# **Type:** `String` **Default:** `The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.`\n#\n# The message that will be shown if the width of the image exceeds `#max_width`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ width }}` - The current (invalid) width.\n# * `{{ max_width }}` - The maximum allowed width.\n#\n# ### size_not_detected_message\n#\n# **Type:** `String` **Default:** `The size of the image could not be detected.`\n#\n# The message that will be shown if the size of the image is unable to be determined.\n# Will only occur if at least one of the size related options has been set.\n#\n# ### min_ratio\n#\n# **Type:** `Float64` **Default:** `nil`\n#\n# If set, the image's aspect ratio (`width / height`) must be greater than or equal to this value.\n#\n# ### min_ratio_message\n#\n# **Type:** `String` **Default:** `The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.`\n#\n# The message that will be shown if the aspect ratio of the image is less than `#min_ratio`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ ratio }}` - The current (invalid) ratio.\n# * `{{ min_ratio }}` - The minimum required ratio.\n#\n# ### max_ratio\n#\n# **Type:** `Float64` **Default:** `nil`\n#\n# If set, the image's aspect ratio (`width / height`) must be less than or equal to this value.\n#\n# ### max_ratio_message\n#\n# **Type:** `String` **Default:** `The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.`\n#\n# The message that will be shown if the aspect ratio of the image exceeds `#max_ratio`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ ratio }}` - The current (invalid) ratio.\n# * `{{ max_ratio }}` - The maximum allowed ratio.\n#\n# ### min_pixels\n#\n# **Type:** `Float64` **Default:** `nil`\n#\n# If set, the amount of pixels of the image file must be greater than or equal to this value.\n#\n# ### min_pixels_message\n#\n# **Type:** `String` **Default:** `The image has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.`\n#\n# The message that will be shown if the amount of pixels of the image is less than `#min_pixels`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ height }}` - The image's height.\n# * `{{ width }}` - The image's width.\n# * `{{ pixels }}` - The image's pixels.\n# * `{{ min_pixels }}` - The minimum required pixels.\n#\n# ### max_pixels\n#\n# **Type:** `Float64` **Default:** `nil`\n#\n# If set, the amount of pixels of the image file must be less than or equal to this value.\n#\n# ### max_pixels_message\n#\n# **Type:** `String` **Default:** `The image has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.`\n#\n# The message that will be shown if the amount of pixels of the image is greater than `#max_pixels`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ height }}` - The image's height.\n# * `{{ width }}` - The image's width.\n# * `{{ pixels }}` - The image's pixels.\n# * `{{ max_pixels }}` - The maximum allowed pixels.\n#\n# ### allow_landscape\n#\n# **Type:** `Bool` **Default:** `true`\n#\n# If `false`, the image cannot be landscape oriented.\n#\n# ### allow_landscape_message\n#\n# **Type:** `String` **Default:** `The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.`\n#\n# The message that will be shown if the `#allow_landscape` is `false` and the image is landscape oriented.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ height }}` - The image's height.\n# * `{{ width }}` - The image's width.\n#\n# ### allow_portrait\n#\n# **Type:** `Bool` **Default:** `true`\n#\n# If `false`, the image cannot be portrait oriented.\n#\n# ### allow_portrait_message\n#\n# **Type:** `String` **Default:** `The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.`\n#\n# The message that will be shown if the `#allow_portrait` is `false` and the image is portrait oriented.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ height }}` - The image's height.\n# * `{{ width }}` - The image's width.\n#\n# ### allow_square\n#\n# **Type:** `Bool` **Default:** `true`\n#\n# If `false`, the image cannot be a square.\n# If you want to force the image to be a square, keep this as is and set `#allow_landscape` and `#allow_portrait` to `false`.\n#\n# ### allow_square_message\n#\n# **Type:** `String` **Default:** `The image is square ({{ width }}x{{ height }}px). Square images are not allowed.`\n#\n# The message that will be shown if the `#allow_square` is `false` and the image is square.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ height }}` - The image's height.\n# * `{{ width }}` - The image's width.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Image < Athena::Validator::Constraints::File\n  SIZE_NOT_DETECTED_ERROR     = \"6d55c3f4-e58e-4fe3-91ee-74b492199956\"\n  TOO_WIDE_ERROR              = \"7f87163d-878f-47f5-99ba-a8eb723a1ab2\"\n  TOO_NARROW_ERROR            = \"9afbd561-4f90-4a27-be62-1780fc43604a\"\n  TOO_HIGH_ERROR              = \"7efae81c-4877-47ba-aa65-d01ccb0d4645\"\n  TOO_LOW_ERROR               = \"aef0cb6a-c07f-4894-bc08-1781420d7b4c\"\n  TOO_FEW_PIXEL_ERROR         = \"1b06b97d-ae48-474e-978f-038a74854c43\"\n  TOO_MANY_PIXEL_ERROR        = \"ee0804e8-44db-4eac-9775-be91aaf72ce1\"\n  RATIO_TOO_BIG_ERROR         = \"70cafca6-168f-41c9-8c8c-4e47a52be643\"\n  RATIO_TOO_SMALL_ERROR       = \"59b8c6ef-bcf2-4ceb-afff-4642ed92f12e\"\n  SQUARE_NOT_ALLOWED_ERROR    = \"5d41425b-facb-47f7-a55a-de9fbe45cb46\"\n  LANDSCAPE_NOT_ALLOWED_ERROR = \"6f895685-7cf2-4d65-b3da-9029c5581d88\"\n  PORTRAIT_NOT_ALLOWED_ERROR  = \"65608156-77da-4c79-a88c-02ef6d18c782\"\n  CORRUPTED_IMAGE_ERROR       = \"5d4163f3-648f-4e39-87fd-cc5ea7aad2d1\"\n\n  @@error_names = {\n    AVD::Constraints::File::NOT_FOUND_ERROR         => \"NOT_FOUND_ERROR\",\n    AVD::Constraints::File::NOT_READABLE_ERROR      => \"NOT_READABLE_ERROR\",\n    AVD::Constraints::File::EMPTY_ERROR             => \"EMPTY_ERROR\",\n    AVD::Constraints::File::TOO_LARGE_ERROR         => \"TOO_LARGE_ERROR\",\n    AVD::Constraints::File::INVALID_MIME_TYPE_ERROR => \"INVALID_MIME_TYPE_ERROR\",\n    SIZE_NOT_DETECTED_ERROR                         => \"SIZE_NOT_DETECTED_ERROR\",\n    TOO_WIDE_ERROR                                  => \"TOO_WIDE_ERROR\",\n    TOO_NARROW_ERROR                                => \"TOO_NARROW_ERROR\",\n    TOO_HIGH_ERROR                                  => \"TOO_HIGH_ERROR\",\n    TOO_LOW_ERROR                                   => \"TOO_LOW_ERROR\",\n    TOO_FEW_PIXEL_ERROR                             => \"TOO_FEW_PIXEL_ERROR\",\n    TOO_MANY_PIXEL_ERROR                            => \"TOO_MANY_PIXEL_ERROR\",\n    RATIO_TOO_BIG_ERROR                             => \"RATIO_TOO_BIG_ERROR\",\n    RATIO_TOO_SMALL_ERROR                           => \"RATIO_TOO_SMALL_ERROR\",\n    SQUARE_NOT_ALLOWED_ERROR                        => \"SQUARE_NOT_ALLOWED_ERROR\",\n    LANDSCAPE_NOT_ALLOWED_ERROR                     => \"LANDSCAPE_NOT_ALLOWED_ERROR\",\n    PORTRAIT_NOT_ALLOWED_ERROR                      => \"PORTRAIT_NOT_ALLOWED_ERROR\",\n    CORRUPTED_IMAGE_ERROR                           => \"CORRUPTED_IMAGE_ERROR\",\n  }\n\n  getter min_width : Int32?\n  getter max_width : Int32?\n  getter min_height : Int32?\n  getter max_height : Int32?\n  getter min_ratio : Float64?\n  getter max_ratio : Float64?\n  getter min_pixels : Float64?\n  getter max_pixels : Float64?\n  getter? allow_square : Bool\n  getter? allow_landscape : Bool\n  getter? allow_portrait : Bool\n\n  getter size_not_detected_message : String\n  getter min_width_message : String\n  getter max_width_message : String\n  getter min_height_message : String\n  getter max_height_message : String\n  getter min_pixels_message : String\n  getter max_pixels_message : String\n  getter min_ratio_message : String\n  getter max_ratio_message : String\n  getter allow_square_message : String\n  getter allow_landscape_message : String\n  getter allow_portrait_message : String\n\n  def initialize(\n    @min_width : Int32? = nil,\n    @max_width : Int32? = nil,\n    @min_height : Int32? = nil,\n    @max_height : Int32? = nil,\n    @min_ratio : Float64? = nil,\n    @max_ratio : Float64? = nil,\n    @min_pixels : Float64? = nil,\n    @max_pixels : Float64? = nil,\n    @allow_square : Bool = true,\n    @allow_landscape : Bool = true,\n    @allow_portrait : Bool = true,\n    @size_not_detected_message : String = \"The size of the image could not be detected.\",\n    @min_width_message : String = \"The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.\",\n    @max_width_message : String = \"The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.\",\n    @min_height_message : String = \"The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.\",\n    @max_height_message : String = \"The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.\",\n    @min_pixels_message : String = \"The image has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.\",\n    @max_pixels_message : String = \"The image has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.\",\n    @min_ratio_message : String = \"The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.\",\n    @max_ratio_message : String = \"The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.\",\n    @allow_square_message : String = \"The image is square ({{ width }}x{{ height }}px). Square images are not allowed.\",\n    @allow_landscape_message : String = \"The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.\",\n    @allow_portrait_message : String = \"The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.\",\n    max_size : Int | String | Nil = nil,\n    binary_format : Bool? = nil,\n    mime_types : Enumerable(String)? = {\"image/*\"},\n    not_found_message : String = \"The file could not be found.\",\n    not_readable_message : String = \"The file is not readable.\",\n    empty_message : String = \"An empty file is not allowed.\",\n    max_size_message : String = \"The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.\",\n    mime_type_message : String = \"This file is not a valid image.\",\n    upload_file_size_message : String = \"The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super max_size, binary_format, mime_types, not_found_message, not_readable_message, empty_message, max_size_message, mime_type_message, upload_file_size_message, groups, payload\n\n    # Use the default message `File` uses when only specific mime types are shown such that it renders the valid types.\n    # OPTIMIZE: Figure out a better way to know if the message has been customized.\n    if !mime_types.includes?(\"image/*\") && mime_type_message == \"This file is not a valid image.\"\n      @mime_type_message = \"The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.\"\n    end\n  end\n\n  class Validator < Athena::Validator::Constraints::File::Validator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Image) : Nil\n      violations = self.context.violations.size\n\n      super\n\n      failed = self.context.violations.size != violations\n\n      return if failed || value.nil? || value == \"\"\n\n      # Return early is no extra validation is being applied.\n      return if {\n                  constraint.min_width, constraint.max_width, constraint.min_height, constraint.max_height,\n                  constraint.min_pixels, constraint.max_pixels, constraint.min_ratio, constraint.max_ratio,\n                  !constraint.allow_square?, !constraint.allow_landscape?, !constraint.allow_portrait?,\n                }.none?\n\n      path = case value\n             when Path   then value\n             when ::File then value.path\n             else\n               value.to_s\n             end\n\n      image_size = AIS::Image.from_file_path? path\n\n      if image_size.nil? || image_size.width.zero? || image_size.height.zero?\n        self\n          .context\n          .add_violation(constraint.size_not_detected_message, SIZE_NOT_DETECTED_ERROR)\n\n        return\n      end\n\n      self.validate_size image_size, constraint\n      self.validate_pixels image_size, constraint\n      self.validate_ratios image_size, constraint\n      self.validate_shape image_size, constraint\n\n      # TODO: Somehow check if image is actually valid?\n    end\n\n    private def validate_size(image_size : AIS::Image, constraint : AVD::Constraints::Image) : Nil\n      if (min_width = constraint.min_width) && (image_size.width < min_width)\n        self\n          .context\n          .build_violation(constraint.min_width_message, TOO_NARROW_ERROR)\n          .add_parameter(\"{{ width }}\", image_size.width)\n          .add_parameter(\"{{ min_width }}\", constraint.min_width)\n          .add\n      end\n\n      if (max_width = constraint.max_width) && (image_size.width > max_width)\n        self\n          .context\n          .build_violation(constraint.max_width_message, TOO_WIDE_ERROR)\n          .add_parameter(\"{{ width }}\", image_size.width)\n          .add_parameter(\"{{ max_width }}\", constraint.max_width)\n          .add\n      end\n\n      if (min_height = constraint.min_height) && (image_size.height < min_height)\n        self\n          .context\n          .build_violation(constraint.min_height_message, TOO_LOW_ERROR)\n          .add_parameter(\"{{ height }}\", image_size.height)\n          .add_parameter(\"{{ min_height }}\", constraint.min_height)\n          .add\n      end\n\n      if (max_height = constraint.max_height) && (image_size.height > max_height)\n        self\n          .context\n          .build_violation(constraint.max_height_message, TOO_HIGH_ERROR)\n          .add_parameter(\"{{ height }}\", image_size.height)\n          .add_parameter(\"{{ max_height }}\", constraint.max_height)\n          .add\n      end\n    end\n\n    private def validate_pixels(image_size : AIS::Image, constraint : AVD::Constraints::Image) : Nil\n      pixels = image_size.width * image_size.height\n\n      if (min_pixels = constraint.min_pixels) && (pixels < min_pixels)\n        self\n          .context\n          .build_violation(constraint.min_pixels_message, TOO_FEW_PIXEL_ERROR)\n          .add_parameter(\"{{ pixels }}\", pixels)\n          .add_parameter(\"{{ min_pixels }}\", min_pixels)\n          .add_parameter(\"{{ width }}\", image_size.width)\n          .add_parameter(\"{{ height }}\", image_size.height)\n          .add\n      end\n\n      if (max_pixels = constraint.max_pixels) && (pixels > max_pixels)\n        self\n          .context\n          .build_violation(constraint.max_pixels_message, TOO_MANY_PIXEL_ERROR)\n          .add_parameter(\"{{ pixels }}\", pixels)\n          .add_parameter(\"{{ max_pixels }}\", max_pixels)\n          .add_parameter(\"{{ width }}\", image_size.width)\n          .add_parameter(\"{{ height }}\", image_size.height)\n          .add\n      end\n    end\n\n    private def validate_ratios(image_size : AIS::Image, constraint : AVD::Constraints::Image) : Nil\n      ratio = (image_size.width / image_size.height).round 2, mode: :ties_away\n\n      if (min_ratio = constraint.min_ratio) && (ratio < min_ratio.round(2, mode: :ties_away))\n        self\n          .context\n          .build_violation(constraint.min_ratio_message, RATIO_TOO_SMALL_ERROR)\n          .add_parameter(\"{{ ratio }}\", ratio)\n          .add_parameter(\"{{ min_ratio }}\", min_ratio)\n          .add\n      end\n\n      if (max_ratio = constraint.max_ratio) && (ratio > max_ratio.round(2, mode: :ties_away))\n        self\n          .context\n          .build_violation(constraint.max_ratio_message, RATIO_TOO_BIG_ERROR)\n          .add_parameter(\"{{ ratio }}\", ratio)\n          .add_parameter(\"{{ max_ratio }}\", max_ratio)\n          .add\n      end\n    end\n\n    private def validate_shape(image_size : AIS::Image, constraint : AVD::Constraints::Image) : Nil\n      if !constraint.allow_square? && image_size.width == image_size.height\n        self\n          .context\n          .build_violation(constraint.allow_square_message, SQUARE_NOT_ALLOWED_ERROR)\n          .add_parameter(\"{{ width }}\", image_size.width)\n          .add_parameter(\"{{ height }}\", image_size.height)\n          .add\n      end\n\n      if !constraint.allow_landscape? && image_size.width > image_size.height\n        self\n          .context\n          .build_violation(constraint.allow_landscape_message, LANDSCAPE_NOT_ALLOWED_ERROR)\n          .add_parameter(\"{{ width }}\", image_size.width)\n          .add_parameter(\"{{ height }}\", image_size.height)\n          .add\n      end\n\n      if !constraint.allow_portrait? && image_size.width < image_size.height\n        self\n          .context\n          .build_violation(constraint.allow_portrait_message, PORTRAIT_NOT_ALLOWED_ERROR)\n          .add_parameter(\"{{ width }}\", image_size.width)\n          .add_parameter(\"{{ height }}\", image_size.height)\n          .add\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/ip.cr",
    "content": "require \"socket\"\n\n# Validates that a value is a valid IP address.\n# By default validates the value as an `IPv4` address, but can be customized to validate `IPv6`s, or both.\n# The underlying value is converted to a string via `#to_s` before being validated.\n#\n# NOTE: As with most other constraints, `nil` and empty strings are considered valid values, in order to allow the value to be optional.\n# If the value is required, consider combining this constraint with `AVD::Constraints::NotBlank`.\n#\n# ```\n# class Machine\n#   include AVD::Validatable\n#\n#   def initialize(@ip_address : String); end\n#\n#   @[Assert::IP]\n#   property ip_address : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### version\n#\n# **Type:** `AVD::Constraints::IP::Version` **Default:** `AVD::Constraints::IP::Version::V4`\n#\n# Defines the pattern that should be used to validate the IP address.\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This is not a valid IP address.`\n#\n# The message that will be shown if the value is not a valid IP address.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::IP < Athena::Validator::Constraint\n  # Determines _how_ the IP address should be validated.\n  enum Version\n    # Validates for `IPv4` addresses.\n    V4\n\n    # Validates for `IPv6` addresses.\n    V6\n\n    # Validates for `IPv4` or `IPv6` addresses.\n    V4_V6\n  end\n\n  INVALID_IP_ERROR = \"326b0aa4-3871-404d-986d-fe3e6c82005c\"\n\n  @@error_names = {\n    INVALID_IP_ERROR => \"INVALID_IP_ERROR\",\n  }\n\n  getter version : AVD::Constraints::IP::Version\n\n  def initialize(\n    @version : AVD::Constraints::IP::Version = :v4,\n    message : String = \"This value is not a valid IP address.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::IP) : Nil\n      value = value.to_s\n\n      return if value.nil? || value.empty?\n\n      case constraint.version\n      in .v4?    then return if Socket::IPAddress.valid_v4? value\n      in .v6?    then return if Socket::IPAddress.valid_v6? value\n      in .v4_v6? then return if Socket::IPAddress.valid? value\n      end\n\n      self.context.add_violation constraint.message, INVALID_IP_ERROR, value\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/is_false.cr",
    "content": "# Validates that a value is `false`.\n#\n# ```\n# class Post\n#   include AVD::Validatable\n#\n#   def initialize(@is_published : Bool); end\n#\n#   @[Assert::IsFalse]\n#   property is_published : Bool\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be false.`\n#\n# The message that will be shown if the value is not `false`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::IsFalse < Athena::Validator::Constraint\n  NOT_FALSE_ERROR = \"55c076a0-dbaf-453c-90cf-b94664276dbc\"\n\n  @@error_names = {\n    NOT_FALSE_ERROR => \"NOT_FALSE_ERROR\",\n  }\n\n  def initialize(\n    message : String = \"This value should be false.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::IsFalse) : Nil\n      return if value.nil? || value == false\n\n      self.context.add_violation constraint.message, NOT_FALSE_ERROR, value\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/is_nil.cr",
    "content": "# Validates that a value is `nil`.\n#\n# ```\n# class Post\n#   include AVD::Validatable\n#\n#   def initialize(@updated_at : Time?); end\n#\n#   @[Assert::IsNil]\n#   property updated_at : Time?\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be null.`\n#\n# The message that will be shown if the value is not `nil`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::IsNil < Athena::Validator::Constraint\n  NOT_NIL_ERROR = \"2c88e3c7-9275-4b9b-81b4-48c6c44b1804\"\n\n  @@error_names = {\n    NOT_NIL_ERROR => \"NOT_NIL_ERROR\",\n  }\n\n  def initialize(\n    message : String = \"This value should be null.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::IsNil) : Nil\n      return if value.nil?\n\n      self.context.add_violation constraint.message, NOT_NIL_ERROR, value\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/is_true.cr",
    "content": "# Validates that a value is `true`.\n#\n# ```\n# class Post\n#   include AVD::Validatable\n#\n#   def initialize(@is_published : Bool); end\n#\n#   @[Assert::IsTrue]\n#   property is_published : Bool\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be true.`\n#\n# The message that will be shown if the value is not `true`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::IsTrue < Athena::Validator::Constraint\n  NOT_TRUE_ERROR = \"beabd93e-3673-4dfc-8796-01bd1504dd19\"\n\n  @@error_names = {\n    NOT_TRUE_ERROR => \"NOT_TRUE_ERROR\",\n  }\n\n  def initialize(\n    message : String = \"This value should be true.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::IsTrue) : Nil\n      return if value.nil? || value == true\n\n      self.context.add_violation constraint.message, NOT_TRUE_ERROR, value\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/isbn.cr",
    "content": "# Validates that an [International Standard Book Number (ISBN)](https://en.wikipedia.org/wiki/Isbn) is either a valid `ISBN-10` or `ISBN-13`.\n# The underlying value is converted to a string via `#to_s` before being validated.\n#\n# NOTE: As with most other constraints, `nil` and empty strings are considered valid values, in order to allow the value to be optional.\n# If the value is required, consider combining this constraint with `AVD::Constraints::NotBlank`.\n#\n# ```\n# class Book\n#   include AVD::Validatable\n#\n#   def initialize(@isbn : String); end\n#\n#   @[Assert::ISBN]\n#   property isbn : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### type\n#\n# **Type:** `AVD::Constraints::ISBN::Type` **Default:** `AVD::Constraints::ISBN::Type::Both`\n#\n# Type of ISBN to validate against.\n#\n# ### message\n#\n# **Type:** `String` **Default:** `\"\"`\n#\n# The message that will be shown if the value is invalid.\n# This message has priority over the other messages if not empty.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### isbn10_message\n#\n# **Type:** `String` **Default:** `This value is not a valid ISBN-10.`\n#\n# The message that will be shown if [type](#type) is `AVD::Constraints::ISBN::Type::ISBN10` and the value is invalid.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### isbn13_message\n#\n# **Type:** `String` **Default:** `This value is not a valid ISBN-13.`\n#\n# The message that will be shown if [type](#type) is `AVD::Constraints::ISBN::Type::ISBN13` and the value is invalid.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### both_message\n#\n# **Type:** `String` **Default:** `This value is neither a valid ISBN-10 nor a valid ISBN-13.`\n#\n# The message that will be shown if [type](#type) is `AVD::Constraints::ISBN::Type::Both` and the value is invalid.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::ISBN < Athena::Validator::Constraint\n  enum Type\n    ISBN10\n    ISBN13\n    Both\n\n    def message(constraint : AVD::Constraints::ISBN) : String\n      case self\n      in .isbn10? then constraint.isbn10_message\n      in .isbn13? then constraint.isbn13_message\n      in .both?   then constraint.both_message\n      end\n    end\n  end\n\n  TOO_SHORT_ERROR           = \"5da9e91f-7956-40f7-9788-4124463d783e\"\n  TOO_LONG_ERROR            = \"ebd28c75-bb42-43d6-9053-f0ea2ea93d44\"\n  INVALID_CHARACTERS_ERROR  = \"25d35907-d822-4bcc-82cc-852e30c89c0d\"\n  CHECKSUM_FAILED_ERROR     = \"f51bae62-6833-43b1-bc27-ae4445c59e30\"\n  TYPE_NOT_RECOGNIZED_ERROR = \"8d83f04d-2503-4547-97a1-935fcccd1ae1\"\n\n  @@error_names = {\n    TOO_SHORT_ERROR           => \"TOO_SHORT_ERROR\",\n    TOO_LONG_ERROR            => \"TOO_LONG_ERROR\",\n    INVALID_CHARACTERS_ERROR  => \"INVALID_CHARACTERS_ERROR\",\n    CHECKSUM_FAILED_ERROR     => \"CHECKSUM_FAILED_ERROR\",\n    TYPE_NOT_RECOGNIZED_ERROR => \"TYPE_NOT_RECOGNIZED_ERROR\",\n  }\n\n  getter type : AVD::Constraints::ISBN::Type\n  getter isbn10_message : String\n  getter isbn13_message : String\n  getter both_message : String\n\n  def initialize(\n    @type : AVD::Constraints::ISBN::Type = :both,\n    @isbn10_message : String = \"This value is not a valid ISBN-10.\",\n    @isbn13_message : String = \"This value is not a valid ISBN-13.\",\n    @both_message : String = \"This value is neither a valid ISBN-10 nor a valid ISBN-13.\",\n    message : String = \"\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  def message : String\n    return @message unless @message.empty?\n\n    @type.message self\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::ISBN) : Nil\n      value = value.to_s\n\n      return if value.nil? || value.empty?\n\n      canonical = value.gsub '-', \"\"\n\n      code = case constraint.type\n             in .isbn10? then self.validate_isbn10 canonical\n             in .isbn13? then self.validate_isbn13 canonical\n             in .both?\n               both_code = self.validate_isbn10 canonical\n\n               if TOO_LONG_ERROR == both_code\n                 both_code = self.validate_isbn13 canonical\n\n                 if TOO_SHORT_ERROR == both_code\n                   both_code = TYPE_NOT_RECOGNIZED_ERROR\n                 end\n               end\n\n               both_code\n             end\n\n      return if code.nil?\n\n      self.context.add_violation constraint.message, code, value\n    end\n\n    private def validate_isbn10(isbn : String) : String?\n      checksum = 0\n\n      10.times do |idx|\n        char = isbn.char_at(idx) { return TOO_SHORT_ERROR }\n\n        digit = case char\n                when 'X'      then 10\n                when .number? then char.to_i\n                else\n                  return INVALID_CHARACTERS_ERROR\n                end\n\n        checksum += digit * (10 - idx)\n      end\n\n      return TOO_LONG_ERROR unless isbn[10]?.nil?\n\n      checksum.divisible_by?(11) ? nil : CHECKSUM_FAILED_ERROR\n    end\n\n    private def validate_isbn13(isbn : String) : String?\n      return INVALID_CHARACTERS_ERROR unless isbn.each_char.all? &.number?\n\n      case isbn.size\n      when .< 13 then return TOO_SHORT_ERROR\n      when .> 13 then return TOO_LONG_ERROR\n      end\n\n      checksum = 0\n\n      0.step(to: 12, by: 2) do |idx|\n        checksum += isbn[idx].to_i\n      end\n\n      1.step(to: 12, by: 2) do |idx|\n        checksum += isbn[idx].to_i * 3\n      end\n\n      checksum.divisible_by?(10) ? nil : CHECKSUM_FAILED_ERROR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/isin.cr",
    "content": "# Validates that a value is a valid [International Securities Identification Number (ISIN)](https://en.wikipedia.org/wiki/International_Securities_Identification_Number).\n# The underlying value is converted to a string via `#to_s` before being validated.\n#\n# NOTE: As with most other constraints, `nil` and empty strings are considered valid values, in order to allow the value to be optional.\n# If the value is required, consider combining this constraint with `AVD::Constraints::NotBlank`.\n#\n# ```\n# class UnitAccount\n#   include AVD::Validatable\n#\n#   def initialize(@isin : String); end\n#\n#   @[Assert::ISIN]\n#   property isin : String\n# end\n# ```\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value is not a valid International Securities Identification Number (ISIN).`\n#\n# The message that will be shown if the value is not a valid ISIN.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::ISIN < Athena::Validator::Constraint\n  INVALID_LENGTH_ERROR   = \"1d1c3fbe-5b6f-42be-afa5-6840655865da\"\n  INVALID_PATTERN_ERROR  = \"0b6ba8c4-b6aa-44dc-afac-a6f7a9a2556d\"\n  INVALID_CHECKSUM_ERROR = \"c7d37ffb-0273-4f57-91f7-f47bf49aad08\"\n\n  private VALIDATION_LENGTH  = 12\n  private VALIDATION_PATTERN = /[A-Z]{2}[A-Z0-9]{9}[0-9]{1}/\n\n  @@error_names = {\n    INVALID_LENGTH_ERROR   => \"INVALID_LENGTH_ERROR\",\n    INVALID_PATTERN_ERROR  => \"INVALID_PATTERN_ERROR\",\n    INVALID_CHECKSUM_ERROR => \"INVALID_CHECKSUM_ERROR\",\n  }\n\n  def initialize(\n    message : String = \"This value is not a valid International Securities Identification Number (ISIN).\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::ISIN) : Nil\n      value = value.to_s\n\n      return if value.nil? || value.empty?\n\n      value = value.upcase\n\n      if VALIDATION_LENGTH != value.size\n        return self.context.add_violation constraint.message, INVALID_LENGTH_ERROR, value\n      end\n\n      unless value.matches? VALIDATION_PATTERN\n        return self.context.add_violation constraint.message, INVALID_PATTERN_ERROR, value\n      end\n\n      return if self.correct_checksum? value\n\n      self.context.add_violation constraint.message, INVALID_CHECKSUM_ERROR, value\n    end\n\n    private def correct_checksum?(isin : String) : Bool\n      number = isin.chars.join &.to_i 36\n      self.context.validator.validate(number, AVD::Constraints::Luhn.new).empty?\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/issn.cr",
    "content": "# Validates that a value is a valid [International Standard Serial Number (ISSN)](https://en.wikipedia.org/wiki/Issn).\n# The underlying value is converted to a string via `#to_s` before being validated.\n#\n# NOTE: As with most other constraints, `nil` and empty strings are considered valid values, in order to allow the value to be optional.\n# If the value is required, consider combining this constraint with `AVD::Constraints::NotBlank`.\n#\n# ```\n# class Journal\n#   include AVD::Validatable\n#\n#   def initialize(@issn : String); end\n#\n#   @[Assert::ISSN]\n#   property issn : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### case_sensitive\n#\n# **Type:** `Bool` **Default:** `false`\n#\n# The validator will allow ISSN values to end with a lowercase `x` by default.\n# When set to `true`, this requires an uppcase case `X`.\n#\n# ### require_hyphen\n#\n# **Type:** `Bool` **Default:** `false`\n#\n# The validator will allow non hyphenated values by default.\n# When set to `true`, this requires a hyphenated ISSN value.\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value is not a valid International Standard Serial Number (ISSN).`\n#\n# The message that will be shown if the value is not a valid ISSN.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::ISSN < Athena::Validator::Constraint\n  TOO_SHORT_ERROR          = \"85c5d3aa-fd0a-4cd0-8cf7-e014e6379d59\"\n  TOO_LONG_ERROR           = \"fab8e3ea-2f77-4da7-b40f-d9b24ff8c0cc\"\n  MISSING_HYPHEN_ERROR     = \"d6c120a9-0b56-4e45-b4bc-7fd186f2cfbd\"\n  INVALID_CHARACTERS_ERROR = \"85c5d3aa-fd0a-4cd0-8cf7-e014e6379d59\"\n  INVALID_CASE_ERROR       = \"66f892f3-9eed-4176-b823-0dafde72202a\"\n  CHECKSUM_FAILED_ERROR    = \"62c01bab-fe8f-4072-aac8-aa4bdcde8361\"\n\n  @@error_names = {\n    TOO_SHORT_ERROR          => \"TOO_SHORT_ERROR\",\n    TOO_LONG_ERROR           => \"TOO_LONG_ERROR\",\n    MISSING_HYPHEN_ERROR     => \"MISSING_HYPHEN_ERROR\",\n    INVALID_CHARACTERS_ERROR => \"INVALID_CHARACTERS_ERROR\",\n    INVALID_CASE_ERROR       => \"INVALID_CASE_ERROR\",\n    CHECKSUM_FAILED_ERROR    => \"CHECKSUM_FAILED_ERROR\",\n  }\n\n  getter? case_sensitive : Bool\n  getter? require_hyphen : Bool\n\n  def initialize(\n    @case_sensitive : Bool = false,\n    @require_hyphen : Bool = false,\n    message : String = \"This value is not a valid International Standard Serial Number (ISSN).\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::ISSN) : Nil\n      value = value.to_s\n\n      return if value.nil? || value.empty?\n\n      canonical = value\n\n      if canonical[4]? == '-'\n        canonical = canonical.delete '-'\n      elsif constraint.require_hyphen?\n        return self.context.add_violation constraint.message, MISSING_HYPHEN_ERROR, value\n      end\n\n      self.validate_size canonical do |code|\n        return self.context.add_violation constraint.message, code, value\n      end\n\n      char = self.validate_characters canonical do\n        return self.context.add_violation constraint.message, INVALID_CHARACTERS_ERROR, value\n      end\n\n      if constraint.case_sensitive? && char == 'x'\n        return self.context.add_violation constraint.message, INVALID_CASE_ERROR, value\n      end\n\n      self.validate_checksum char, canonical do\n        self.context.add_violation constraint.message, CHECKSUM_FAILED_ERROR, value\n      end\n    end\n\n    private def validate_size(issn : String, & : String ->) : Nil\n      yield TOO_SHORT_ERROR if issn.size < 8\n      yield TOO_LONG_ERROR if issn.size > 8\n    end\n\n    private def validate_characters(issn : String, &) : Char\n      yield unless issn[...7].each_char.all? &.number?\n      yield if (char = issn[7]) && !char.number? && !char.in? 'x', 'X'\n\n      char\n    end\n\n    private def validate_checksum(char : Char, issn : String, &) : Nil\n      checksum = char.in?('x', 'X') ? 10 : char.to_i\n\n      7.times do |idx|\n        checksum += (8 - idx) * issn[idx].to_i\n      end\n\n      yield unless checksum.divisible_by? 11\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/length.cr",
    "content": "# Validates that the length of a `String` is between some minimum and maximum.\n# Non `String` values are stringified via `#to_s`.\n#\n# ```\n# class User\n#   include AVD::Validatable\n#\n#   def initialize(@username : String); end\n#\n#   @[Assert::Length(3..30)]\n#   property username : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### range\n#\n# **Type:** `::Range`\n#\n# The `::Range` that defines the minimum and maximum values, if any.\n# An endless range can be used to only have a minimum or maximum.\n#\n# ## Optional Arguments\n#\n# NOTE: This constraint does not support a `message` argument.\n#\n# ### unit\n#\n# **Type:** `AVD::Constraints::Length::Unit` **Default:** `AVD::Constraints::Length::Unit::CODEPOINTS`\n#\n# Which unit should be used to determine the length of the string.\n#\n# ### exact_message\n#\n# **Type:** `String` **Default:** `This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.`\n#\n# The message that will be shown if min and max values are equal and the underlying value’s length is not exactly this value.\n# The message is pluralized depending on how many elements/characters the underlying value has.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ limit }}` - The exact expected length.\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ value_length }}` - The current value's length.\n#\n# ### min_message\n#\n# **Type:** `String` **Default:** `This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.`\n#\n# The message that will be shown if the underlying value’s length is less than the min.\n# The message is pluralized depending on how many elements/characters the underlying value has.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ limit }}` - The exact minimum length.\n# * `{{ min }}` - The expected minimum length.\n# * `{{ max }}` - The expected maximum length.\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ value_length }}` - The current value's length.\n#\n# ### max_message\n#\n# **Type:** `String` **Default:** `This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.`\n#\n# The message that will be shown if the underlying value’s length is greater than the max.\n# The message is pluralized depending on how many elements/characters the underlying value has.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ limit }}` - The exact maximum length.\n# * `{{ min }}` - The expected minimum length.\n# * `{{ max }}` - The expected maximum length.\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ value_length }}` - The current value's length.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Length < Athena::Validator::Constraint\n  # The unit used for the length check, defaulting to `CODEPOINTS`.\n  enum Unit\n    # Uses [String#size](https://crystal-lang.org/api/String.html#size%3AInt32-instance-method) to return the number of Unicode codepoints.\n    CODEPOINTS\n\n    # Uses [String#bytesize](https://crystal-lang.org/api/String.html#bytesize%3AInt32-instance-method) to return the number of bytes.\n    BYTES\n\n    # Uses [String#grapheme_size](https://crystal-lang.org/api/String.html#grapheme_size%3AInt32-instance-method) to return the number of Unicode graphemes clusters.\n    GRAPHEMES\n  end\n\n  TOO_SHORT_ERROR        = \"643f9d15-a5fd-41b7-b6d8-85f40855ba11\"\n  TOO_LONG_ERROR         = \"e07eee2c-be7a-4ac3-be6b-2ea344250f99\"\n  NOT_EQUAL_LENGTH_ERROR = \"03ef6899-6e39-4e7a-9ac9-5f4374736273\"\n\n  @@error_names = {\n    TOO_SHORT_ERROR        => \"TOO_SHORT_ERROR\",\n    TOO_LONG_ERROR         => \"TOO_LONG_ERROR\",\n    NOT_EQUAL_LENGTH_ERROR => \"NOT_EQUAL_LENGTH_ERROR\",\n  }\n\n  getter min : Int32?\n  getter max : Int32?\n  getter min_message : String\n  getter max_message : String\n  getter exact_message : String\n  getter unit : AVD::Constraints::Length::Unit\n\n  def self.new(\n    range : ::Range,\n    min_message : String = \"This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.\",\n    max_message : String = \"This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.\",\n    exact_message : String = \"This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.\",\n    unit : AVD::Constraints::Length::Unit = :codepoints,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    new range.begin, range.end, min_message, max_message, exact_message, unit, groups, payload\n  end\n\n  private def initialize(\n    @min : Int32?,\n    @max : Int32?,\n    @min_message : String = \"This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.\",\n    @max_message : String = \"This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.\",\n    @exact_message : String = \"This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.\",\n    @unit : AVD::Constraints::Length::Unit = :codepoints,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super \"\", groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    # ameba:disable Metrics/CyclomaticComplexity\n    def validate(value : _, constraint : AVD::Constraints::Length) : Nil\n      return if value.nil?\n\n      length = case constraint.unit\n               in .codepoints? then value.to_s.size\n               in .bytes?      then value.to_s.bytesize\n               in .graphemes?  then value.to_s.grapheme_size\n               end\n\n      min = constraint.min\n      max = constraint.max\n\n      if max && length > max\n        exactly_option_enabled = min == max\n\n        builder = self\n          .context\n          .build_violation(\n            exactly_option_enabled ? constraint.exact_message : constraint.max_message,\n            exactly_option_enabled ? NOT_EQUAL_LENGTH_ERROR : TOO_LONG_ERROR,\n            value\n          )\n\n        if min\n          builder.add_parameter(\"{{ min }}\", min)\n        end\n\n        builder\n          .add_parameter(\"{{ limit }}\", max)\n          .add_parameter(\"{{ max }}\", max)\n          .add_parameter(\"{{ value_length }}\", length)\n          .invalid_value(value)\n          .plural(max)\n          .add\n      end\n\n      if min && length < min\n        exactly_option_enabled = min == max\n\n        builder = self\n          .context\n          .build_violation(\n            exactly_option_enabled ? constraint.exact_message : constraint.min_message,\n            exactly_option_enabled ? NOT_EQUAL_LENGTH_ERROR : TOO_SHORT_ERROR,\n            value\n          )\n\n        if max\n          builder.add_parameter(\"{{ max }}\", max)\n        end\n\n        builder\n          .add_parameter(\"{{ limit }}\", min)\n          .add_parameter(\"{{ min }}\", min)\n          .add_parameter(\"{{ value_length }}\", length)\n          .invalid_value(value)\n          .plural(min)\n          .add\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/less_than.cr",
    "content": "# Validates that a value is less than another.\n#\n# ```\n# class Employee\n#   include AVD::Validatable\n#\n#   def initialize(@age : Number); end\n#\n#   @[Assert::LessThan(60)]\n#   property age : Number\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### value\n#\n# **Type:** `Number | String | Time`\n#\n# Defines the value that the value being validated should be compared to.\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be less than {{ compared_value }}.`\n#\n# The message that will be shown if the value is not less than the comparison value.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::LessThan(ValueType) < Athena::Validator::Constraint\n  include Athena::Validator::Constraints::AbstractComparison(ValueType)\n\n  TOO_HIGH_ERROR = \"d9fbedb3-c576-45b5-b4dc-996030349bbf\"\n\n  @@error_names = {\n    TOO_HIGH_ERROR => \"TOO_HIGH_ERROR\",\n  }\n\n  def default_error_message : String\n    \"This value should be less than {{ compared_value }}.\"\n  end\n\n  class Validator < Athena::Validator::Constraints::ComparisonValidator\n    def compare_values(actual : Number, expected : Number) : Bool\n      actual < expected\n    end\n\n    def compare_values(actual : String, expected : String) : Bool\n      actual < expected\n    end\n\n    def compare_values(actual : Time, expected : Time) : Bool\n      actual < expected\n    end\n\n    # :inherit:\n    def compare_values(actual : _, expected : _) : NoReturn\n      # TODO: Support checking if arbitrarily typed values are actually comparable once `#responds_to?` supports it.\n      self.raise_invalid_type actual, \"Number | String | Time\"\n    end\n\n    # :inherit:\n    def error_code : String\n      TOO_HIGH_ERROR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/less_than_or_equal.cr",
    "content": "# Validates that a value is less than or equal to another.\n#\n# ```\n# class Employee\n#   include AVD::Validatable\n#\n#   def initialize(@age : Number); end\n#\n#   @[Assert::LessThanOrEqual(60)]\n#   property age : Number\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### value\n#\n# **Type:** `Number | String | Time`\n#\n# Defines the value that the value being validated should be compared to.\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be less than or equal to {{ compared_value }}.`\n#\n# The message that will be shown if the value is not less than or equal to the comparison value.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::LessThanOrEqual(ValueType) < Athena::Validator::Constraint\n  include Athena::Validator::Constraints::AbstractComparison(ValueType)\n\n  TOO_HIGH_ERROR = \"515a12ff-82f2-4434-9635-137164d5b467\"\n\n  @@error_names = {\n    TOO_HIGH_ERROR => \"TOO_HIGH_ERROR\",\n  }\n\n  def default_error_message : String\n    \"This value should be less than or equal to {{ compared_value }}.\"\n  end\n\n  class Validator < Athena::Validator::Constraints::ComparisonValidator\n    def compare_values(actual : Number, expected : Number) : Bool\n      actual <= expected\n    end\n\n    def compare_values(actual : String, expected : String) : Bool\n      actual <= expected\n    end\n\n    def compare_values(actual : Time, expected : Time) : Bool\n      actual <= expected\n    end\n\n    # :inherit:\n    def compare_values(actual : _, expected : _) : NoReturn\n      # TODO: Support checking if arbitrarily typed values are actually comparable once `#responds_to?` supports it.\n      self.raise_invalid_type actual, \"Number | String | Time\"\n    end\n\n    # :inherit:\n    def error_code : String\n      TOO_HIGH_ERROR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/luhn.cr",
    "content": "# Validates that a credit card number passes the [Luhn algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm); a useful first step to validating a credit card.\n# The underlying value is converted to a string via `#to_s` before being validated.\n#\n# NOTE: As with most other constraints, `nil` and empty strings are considered valid values, in order to allow the value to be optional.\n# If the value is required, consider combining this constraint with `AVD::Constraints::NotBlank`.\n#\n# ```\n# class Transaction\n#   include AVD::Validatable\n#\n#   def initialize(@card_number : String); end\n#\n#   @[Assert::Luhn]\n#   property card_number : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value is not a valid credit card number.`\n#\n# The message that will be shown if the value is not pass the Luhn check.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Luhn < Athena::Validator::Constraint\n  INVALID_CHARACTERS_ERROR = \"c42b8d36-d9e9-4f5f-aad6-5190e27a1102\"\n  CHECKSUM_FAILED_ERROR    = \"a4f089dd-fd63-4d50-ac30-34ed2a8dc9dd\"\n\n  @@error_names = {\n    INVALID_CHARACTERS_ERROR => \"INVALID_CHARACTERS_ERROR\",\n    CHECKSUM_FAILED_ERROR    => \"CHECKSUM_FAILED_ERROR\",\n  }\n\n  def initialize(\n    message : String = \"This value is not a valid credit card number.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Luhn) : Nil\n      value = value.to_s\n\n      return if value.nil? || value.empty?\n\n      characters = value.chars\n\n      unless characters.all? &.number?\n        return self.context.add_violation constraint.message, INVALID_CHARACTERS_ERROR, value\n      end\n\n      last_dig : Int32 = characters.pop.to_i\n      checksum : Int32 = (characters.reverse.map_with_index { |n, idx| val = idx.even? ? n.to_i * 2 : n.to_i; val -= 9 if val > 9; val }.sum + last_dig)\n\n      return if !checksum.zero? && checksum.divisible_by?(10)\n\n      self.context.add_violation constraint.message, CHECKSUM_FAILED_ERROR, value\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/negative.cr",
    "content": "# Validates that a value is a negative number.\n# Use `AVD::Constraints::NegativeOrZero` if you wish to also allow `0`.\n#\n# ```\n# class Mall\n#   include AVD::Validatable\n#\n#   def initialize(@lowest_floor : Number); end\n#\n#   @[Assert::Negative]\n#   property lowest_floor : Number\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be negative.`\n#\n# The message that will be shown if the value is not less than `0`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Negative < Athena::Validator::Constraints::LessThan(Int32)\n  def initialize(\n    message : String = \"This value should be negative.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super Int32.zero, message, groups, payload\n  end\n\n  def validated_by : AVD::ConstraintValidator.class\n    AVD::Constraints::LessThan::Validator\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/negative_or_zero.cr",
    "content": "# Validates that a value is a negative number, or `0`.\n# Use `AVD::Constraints::Negative` if you don't want to allow `0`.\n#\n# ```\n# class Mall\n#   include AVD::Validatable\n#\n#   def initialize(@lowest_floor : Number); end\n#\n#   @[Assert::NegativeOrZero]\n#   property lowest_floor : Number\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be negative or zero.`\n#\n# The message that will be shown if the value is not less than or equal to `0`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The `AVD::Constraint@payload` is not used by `Athena::Validator`, but its processing is completely up to you\nclass Athena::Validator::Constraints::NegativeOrZero < Athena::Validator::Constraints::LessThanOrEqual(Int32)\n  def initialize(\n    message : String = \"This value should be negative or zero.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super Int32.zero, message, groups, payload\n  end\n\n  def validated_by : AVD::ConstraintValidator.class\n    AVD::Constraints::LessThanOrEqual::Validator\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/not_blank.cr",
    "content": "# Validates that a value is not blank; meaning not equal to a blank string, an empty `Iterable`, `false`, or optionally `nil`.\n#\n# ```\n# class User\n#   include AVD::Validatable\n#\n#   def initialize(@name : String); end\n#\n#   @[Assert::NotBlank]\n#   property name : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### allow_nil\n#\n# **Type:** `Bool` **Default:** `false`\n#\n# If set to `true`, `nil` values are considered valid and will not trigger a violation.\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should not be blank.`\n#\n# The message that will be shown if the value is blank.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::NotBlank < Athena::Validator::Constraint\n  IS_BLANK_ERROR = \"0d0c3254-3642-4cb0-9882-46ee5918e6e3\"\n\n  @@error_names = {\n    IS_BLANK_ERROR => \"IS_BLANK_ERROR\",\n  }\n\n  getter? allow_nil : Bool\n\n  def initialize(\n    @allow_nil : Bool = false,\n    message : String = \"This value should not be blank.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : String?, constraint : AVD::Constraints::NotBlank) : Nil\n      validate_value(value, constraint) do |v|\n        v.blank?\n      end\n    end\n\n    # :inherit:\n    def validate(value : Bool?, constraint : AVD::Constraints::NotBlank) : Nil\n      validate_value(value, constraint) do |v|\n        v == false\n      end\n    end\n\n    # :inherit:\n    def validate(value : Iterable?, constraint : AVD::Constraints::NotBlank) : Nil\n      validate_value(value, constraint) do |v|\n        v.empty?\n      end\n    end\n\n    private def validate_value(value : _, constraint : AVD::Constraints::NotBlank, & : -> Bool) : Nil\n      return if value.nil? && constraint.allow_nil?\n\n      if value.nil? || yield value\n        self.context.add_violation constraint.message, IS_BLANK_ERROR, value\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/not_equal_to.cr",
    "content": "# Validates that a value is not equal to another.\n#\n# ```\n# class User\n#   include AVD::Validatable\n#\n#   def initialize(@name : String); end\n#\n#   @[Assert::NotEqualTo(\"John Doe\")]\n#   property name : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### value\n#\n# Defines the value that the value being validated should be compared to.\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should not be equal to {{ compared_value }}.`\n#\n# The message that will be shown if the value is equal to the comparison value.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::NotEqualTo(ValueType) < Athena::Validator::Constraint\n  include Athena::Validator::Constraints::AbstractComparison(ValueType)\n\n  IS_EQUAL_ERROR = \"984a0525-d73e-40c0-81c2-2ecbca7e4c96\"\n\n  @@error_names = {\n    IS_EQUAL_ERROR => \"IS_EQUAL_ERROR\",\n  }\n\n  def default_error_message : String\n    \"This value should not be equal to {{ compared_value }}.\"\n  end\n\n  class Validator < Athena::Validator::Constraints::ComparisonValidator\n    # :inherit:\n    def compare_values(actual : _, expected : _) : Bool\n      actual != expected\n    end\n\n    # :inherit:\n    def error_code : String\n      IS_EQUAL_ERROR\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/not_nil.cr",
    "content": "# Validates that a value is not `nil`.\n#\n# NOTE: Due to Crystal's static typing, when validating objects the property's type must be nilable,\n# otherwise `nil` is inherently not allowed due to the compiler's type checking.\n#\n# ```\n# class Post\n#   include AVD::Validatable\n#\n#   def initialize(@title : String?, @description : String?); end\n#\n#   @[Assert::NotNil]\n#   property title : String?\n#\n#   @[Assert::NotNil]\n#   property description : String?\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should not be null.`\n#\n# The message that will be shown if the value is `nil`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::NotNil < Athena::Validator::Constraint\n  IS_NIL_ERROR = \"c7e77b14-744e-44c0-aa7e-391c69cc335c\"\n\n  @@error_names = {\n    IS_NIL_ERROR => \"IS_NIL_ERROR\",\n  }\n\n  def initialize(\n    message : String = \"This value should not be null.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::NotNil) : Nil\n      return unless value.nil?\n\n      self.context.add_violation constraint.message, IS_NIL_ERROR, value\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/optional.cr",
    "content": "# Allows wrapping `AVD::Constraint`(s) to denote it as being optional within an `AVD::Constraints::Collection`.\n# See [this][Athena::Validator::Constraints::Collection--required-and-optional-constraints] for more information.\nclass Athena::Validator::Constraints::Optional < Athena::Validator::Constraints::Existence\n  # :inherit:\n  def validated_by : NoReturn\n    raise \"BUG: #{self} cannot be validated\"\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/positive.cr",
    "content": "# Validates that a value is a positive number.\n# Use `AVD::Constraints::PositiveOrZero` if you wish to also allow `0`.\n#\n# ```\n# class Account\n#   include AVD::Validatable\n#\n#   def initialize(@balance : Number); end\n#\n#   @[Assert::Positive]\n#   property balance : Number\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be positive.`\n#\n# The message that will be shown if the value is not greater than `0`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Positive < Athena::Validator::Constraints::GreaterThan(Int32)\n  def initialize(\n    message : String = \"This value should be positive.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super Int32.zero, message, groups, payload\n  end\n\n  def validated_by : AVD::ConstraintValidator.class\n    AVD::Constraints::GreaterThan::Validator\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/positive_or_zero.cr",
    "content": "# Validates that a value is a positive number, or `0`.\n# Use `AVD::Constraints::Positive` if you don't want to allow `0`.\n#\n# ```\n# class Account\n#   include AVD::Validatable\n#\n#   def initialize(@balance : Number); end\n#\n#   @[Assert::PositiveOrZero]\n#   property balance : Number\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should be positive or zero.`\n#\n# The message that will be shown if the value is not greater than or equal to `0`.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ compared_value }}` - The expected value.\n# * `{{ compared_value_type }}` - The type of the expected value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::PositiveOrZero < Athena::Validator::Constraints::GreaterThanOrEqual(Int32)\n  def initialize(\n    message : String = \"This value should be positive or zero.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super Int32.zero, message, groups, payload\n  end\n\n  def validated_by : AVD::ConstraintValidator.class\n    AVD::Constraints::GreaterThanOrEqual::Validator\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/range.cr",
    "content": "# Validates that a `Number` or `Time` value is between some minimum and maximum.\n#\n# ```\n# class House\n#   include AVD::Validatable\n#\n#   def initialize(@area : Number); end\n#\n#   @[Assert::Range(15..100)]\n#   property area : Number\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### range\n#\n# **Type:** `::Range`\n#\n# The `::Range` that defines the minimum and maximum values, if any.\n# An endless range can be used to only have a minimum or maximum.\n#\n# ## Optional Arguments\n#\n# NOTE: This constraint does not support a `message` argument.\n#\n# ### not_in_range_message\n#\n# **Type:** `String` **Default:** `This value should be between {{ min }} and {{ max }}.`\n#\n# The message that will be shown if the value is less than the min or greater than the max.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ min }}` - The lower limit.\n# * `{{ max }}` - The upper limit.\n#\n# ### min_message\n#\n# **Type:** `String` **Default:** `This value should be {{ limit }} or more.`\n#\n# The message that will be shown if the value is less than the min, and no max has been provided.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ limit }}` - The lower limit.\n#\n# ### max_message\n#\n# **Type:** `String` **Default:** `This value should be {{ limit }} or less.`\n#\n# The message that will be shown if the value is more than the max, and no min has been provided.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ limit }}` - The upper limit.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Range < Athena::Validator::Constraint\n  NOT_IN_RANGE_ERROR = \"7e62386d-30ae-4e7c-918f-1b7e571c6d69\"\n  TOO_HIGH_ERROR     = \"5d9aed01-ac49-4d8e-9c16-e4aab74ea774\"\n  TOO_LOW_ERROR      = \"f0316644-882e-4779-a404-ee7ac97ddecc\"\n\n  @@error_names = {\n    NOT_IN_RANGE_ERROR => \"NOT_IN_RANGE_ERROR\",\n    TOO_HIGH_ERROR     => \"TOO_HIGH_ERROR\",\n    TOO_LOW_ERROR      => \"TOO_LOW_ERROR\",\n  }\n\n  getter min : Number::Primitive | Time | Nil\n  getter max : Number::Primitive | Time | Nil\n  getter not_in_range_message : String\n  getter min_message : String\n  getter max_message : String\n\n  def self.new(\n    range : ::Range,\n    not_in_range_message : String = \"This value should be between {{ min }} and {{ max }}.\",\n    min_message : String = \"This value should be {{ limit }} or more.\",\n    max_message : String = \"This value should be {{ limit }} or less.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    range_end = range.end\n\n    if range_end && range_end.is_a? Number::Primitive && range.excludes_end?\n      range_end -= 1\n    end\n\n    new range.begin, range_end, not_in_range_message, min_message, max_message, groups, payload\n  end\n\n  private def initialize(\n    @min : Number::Primitive | Time | Nil,\n    @max : Number::Primitive | Time | Nil,\n    @not_in_range_message : String,\n    @min_message : String,\n    @max_message : String,\n    groups : Array(String) | String | Nil,\n    payload : Hash(String, String)?,\n  )\n    super \"\", groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    #\n    # ameba:disable Metrics/CyclomaticComplexity\n    def validate(value : Number | Time | Nil, constraint : AVD::Constraints::Range) : Nil\n      return if value.nil?\n\n      min = constraint.min\n      max = constraint.max\n\n      case {value, min, max}\n      when {Number, Number::Primitive?, Number::Primitive?}\n        return self.add_not_in_range_violation constraint, value, min, max if min && max && (value < min || value > max)\n        return self.add_too_high_violation constraint, value, max if max && value > max\n\n        add_too_low_violation constraint, value, min if min && value < min\n      when {Time, Time?, Time?}\n        return self.add_not_in_range_violation constraint, value, min, max if min && max && (value < min || value > max)\n        return self.add_too_high_violation constraint, value, max if max && value > max\n\n        add_too_low_violation constraint, value, min if min && value < min\n      end\n    end\n\n    def validate(value : _, constraint : AVD::Constraints::Range) : Nil\n      raise AVD::Exception::UnexpectedValueError.new value, \"Number | Time\"\n    end\n\n    private def add_not_in_range_violation(constraint, value, min, max) : Nil\n      self\n        .context\n        .build_violation(constraint.not_in_range_message, NOT_IN_RANGE_ERROR, value)\n        .add_parameter(\"{{ min }}\", min)\n        .add_parameter(\"{{ max }}\", max)\n        .add\n    end\n\n    private def add_too_high_violation(constraint, value, max) : Nil\n      self\n        .context\n        .build_violation(constraint.max_message, TOO_HIGH_ERROR, value)\n        .add_parameter(\"{{ limit }}\", max)\n        .add\n    end\n\n    private def add_too_low_violation(constraint, value, min) : Nil\n      self\n        .context\n        .build_violation(constraint.min_message, TOO_LOW_ERROR, value)\n        .add_parameter(\"{{ limit }}\", min)\n        .add\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/regex.cr",
    "content": "# Validates that a value matches a regular expression.\n# The underlying value is converted to a string via `#to_s` before being validated.\n#\n# NOTE: As with most other constraints, `nil` and empty strings are considered valid values, in order to allow the value to be optional.\n# If the value is required, consider combining this constraint with `AVD::Constraints::NotBlank`.\n#\n# ```\n# class User\n#   include AVD::Validatable\n#\n#   def initialize(@username : String); end\n#\n#   # this regex verifies that username contains alphanumeric chars\n#   # and some special characters (underscore, space and dash).\n#   @[Assert::Regex(/^[a-zA-Z0-9]+([_ -]?[a-zA-Z0-9])*$/)]\n#   property username : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### pattern\n#\n# **Type:** `::Regex`\n#\n# The `::Regex` pattern that the value should match.\n#\n# ## Optional Arguments\n#\n# ### match\n#\n# **Type:** `Bool` **Default:** `true`\n#\n# If set to `false`, validation will require the value does _NOT_ match the [pattern](#pattern).\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value should match '{{ pattern }}'.`\n#\n# The message that will be shown if the value does not match the [pattern](#pattern).\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n# * `{{ pattern }}` - The regular expression pattern that the value should match.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Regex < Athena::Validator::Constraint\n  REGEX_FAILED_ERROR = \"108987a0-2d81-44a0-b8d4-1c7ab8815343\"\n\n  @@error_names = {\n    REGEX_FAILED_ERROR => \"REGEX_FAILED_ERROR\",\n  }\n\n  getter pattern : ::Regex\n  getter? match : Bool\n\n  def initialize(\n    @pattern : ::Regex,\n    @match : Bool = true,\n    message : String = \"This value should match '{{ pattern }}'.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Regex) : Nil\n      value = value.to_s\n\n      return if value.nil? || value.empty?\n      return unless constraint.match? ^ value.matches? constraint.pattern\n\n      self\n        .context\n        .build_violation(constraint.message, REGEX_FAILED_ERROR, value)\n        .add_parameter(\"{{ pattern }}\", constraint.pattern)\n        .add\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/required.cr",
    "content": "# Allows wrapping `AVD::Constraint`(s) to denote it as being required within an `AVD::Constraints::Collection`.\n# See [this][Athena::Validator::Constraints::Collection--required-and-optional-constraints] for more information.\nclass Athena::Validator::Constraints::Required < Athena::Validator::Constraints::Existence\n  # :inherit:\n  def validated_by : NoReturn\n    raise \"BUG: #{self} cannot be validated\"\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/sequentially.cr",
    "content": "# Validates a value against a collection of constraints, stopping once the first violation is raised.\n#\n# # Configuration\n#\n# ## Required Arguments\n#\n# ### constraints\n#\n# **Type:** `Array(AVD::Constraint) | AVD::Constraint`\n#\n# The `AVD::Constraint`(s) that are to be applied sequentially.\n#\n# ## Optional Arguments\n#\n# NOTE: This constraint does not support a `message` argument.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\n#\n# # Usage\n#\n# Suppose you have an object with a `address` property which should meet the following criteria:\n#\n# * Is not a blank string\n# * Is at least 10 characters long\n# * Is in a specific format\n# * Is geolocalizable using an external API\n#\n# If you were to apply all of these constraints to the `address` property, you may run into some problems.\n# For example, multiple violations may be added for the same property, or you may perform a useless and heavy\n# external call to geolocalize the address when it is not in a proper format.\n#\n# To solve this we can validate these constraints sequentially.\n#\n# ```\n# class Location\n#   include AVD::Validatable\n#\n#   PATTERN = /some_pattern/\n#\n#   def initialize(@address : String); end\n#\n#   @[Assert::Sequentially([\n#     @[Assert::NotBlank],\n#     @[Assert::Size(10..)],\n#     @[Assert::Regex(Location::PATTERN)],\n#     @[Assert::CustomGeolocalizationConstraint],\n#   ])]\n#   getter address : String\n# end\n# ```\n#\n# NOTE: The annotation approach only supports two levels of nested annotations.\n# Manually wire up the constraint via code if you require more than that.\nclass Athena::Validator::Constraints::Sequentially < Athena::Validator::Constraints::Composite\n  def initialize(\n    constraints : AVD::Constraints::Composite::Type,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super constraints, \"\", groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Sequentially) : Nil\n      validator = self.context.validator.in_context self.context\n\n      original_count = validator.violations.size\n\n      constraint.constraints.each_value do |c|\n        break if original_count != validator.validate(value, c).violations.size\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/unique.cr",
    "content": "# Validates that all elements of an `Indexable` are unique.\n#\n# ```\n# class School\n#   include AVD::Validatable\n#\n#   def initialize(@rooms : Array(String)); end\n#\n#   @[Assert::Unique]\n#   property rooms : Array(String)\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This collection should contain only unique elements.`\n#\n# The message that will be shown if at least one element is repeated in the collection.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::Unique < Athena::Validator::Constraint\n  IS_NOT_UNIQUE_ERROR = \"fd1f83d6-94b5-44bc-b39d-b1ff367ebfb8\"\n\n  @@error_names = {\n    IS_NOT_UNIQUE_ERROR => \"IS_NOT_UNIQUE_ERROR\",\n  }\n\n  def initialize(\n    message : String = \"This collection should contain only unique elements.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : Indexable?, constraint : AVD::Constraints::Unique) : Nil\n      return if value.nil?\n\n      set = Set(typeof(value[0])).new value.size\n\n      unless value.all? { |x| set.add?(x) }\n        self.context.add_violation constraint.message, IS_NOT_UNIQUE_ERROR, value\n      end\n    end\n\n    # :inherit:\n    def validate(actual : _, expected : _) : NoReturn\n      # TODO: Support checking if arbitrarily typed values are actually comparable once `#responds_to?` supports it.\n      self.raise_invalid_type actual, \"Indexable\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/url.cr",
    "content": "# Validates that a value is a valid URL string.\n# The underlying value is converted to a string via `#to_s` before being validated.\n#\n# NOTE: As with most other constraints, `nil` and empty strings are considered valid values, in order to allow the value to be optional.\n# If the value is required, consider combining this constraint with `AVD::Constraints::NotBlank`.\n#\n# ```\n# class Profile\n#   include AVD::Validatable\n#\n#   def initialize(@avatar_url : String); end\n#\n#   @[Assert::URL]\n#   property avatar_url : String\n# end\n# ```\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# ### protocols\n#\n# **Type:** `Array(String)` **Default:** `[\"http\", \"https\"]`\n#\n# The protocols considered to be valid for the URL.\n#\n# ### relative_protocol\n#\n# **Type:** `Bool` **Default:** `false`\n#\n# If `true` the protocol is considered optional.\n#\n# ### require_tld\n#\n# **Type:** `Bool` **Default:** `true`\n#\n# The [URL spec](https://datatracker.ietf.org/doc/html/rfc1738) considers URLs like `https://aaa` or `https://foobar` to be valid\n# However, this is most likely not desirable for most use cases.\n# As such, this argument defaults to `true` and can be used to require that the host part of the URL will have to include a TLD (top-level domain name).\n# E.g. `https://example.com` is valid but `https://example` is not.\n#\n# NOTE: This constraint does _NOT_ validate that the provided TLD is a valid one according to the [official list](https://en.wikipedia.org/wiki/List_of_Internet_top-level_domains).\n#\n# ### tld_message\n#\n# **Type:** `String` **Default:** `This URL is missing a top-level domain.`\n#\n# The message that will be shown if `#require_tld?` is `true` and the URL does not contain at least one TLD.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### message\n#\n# **Type:** `String` **Default:** `This value is not a valid URL.`\n#\n# The message that will be shown if the URL is not valid.\n#\n# #### Placeholders\n#\n# The following placeholders can be used in this message:\n#\n# * `{{ value }}` - The current (invalid) value.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\nclass Athena::Validator::Constraints::URL < Athena::Validator::Constraint\n  INVALID_URL_ERROR = \"e87ceba6-a896-4906-9957-b102045272ee\"\n  MISSING_TLD_ERROR = \"4507f4cc-90fd-4616-989b-2166fc0d1083\"\n\n  @@error_names = {\n    INVALID_URL_ERROR => \"INVALID_URL_ERROR\",\n    MISSING_TLD_ERROR => \"MISSING_TLD_ERROR\",\n  }\n\n  getter protocols : Array(String)\n  getter? relative_protocol : Bool\n  getter? require_tld : Bool\n  getter tld_message : String\n\n  def initialize(\n    @protocols : Array(String) = [\"http\", \"https\"],\n    @relative_protocol : Bool = false,\n    @require_tld : Bool = true,\n    @tld_message : String = \"This URL is missing a top-level domain.\",\n    message : String = \"This value is not a valid URL.\",\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super message, groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::URL) : Nil\n      value = value.to_s\n\n      return if value.nil? || value.empty?\n      unless value.matches? self.pattern(constraint)\n        self.context.add_violation constraint.message, INVALID_URL_ERROR, value\n      end\n\n      return unless constraint.require_tld?\n      return unless url_host = URI.parse(value).host\n\n      # URL with a TLD must include at least a `.`, but cannot be an IP address\n      if !url_host.includes?('.') || Socket::IPAddress.valid?(url_host)\n        self.context.add_violation constraint.tld_message, MISSING_TLD_ERROR, value\n      end\n    end\n\n    def pattern(constraint : AVD::Constraints::URL) : ::Regex\n      /^#{constraint.relative_protocol? ? \"(?:(#{constraint.protocols.join('|')}):)?\" : \"(#{constraint.protocols.join('|')}):\"}\\/\\/(((?:[\\_\\.\\pL\\pN-]|\\%[0-9A-Fa-f]{2})+:)?((?:[\\_\\.\\pL\\pN-]|\\%[0-9A-Fa-f]{2})+)@)?(([\\pL\\pN\\pS\\-\\_\\.])+(\\.?([\\pL\\pN]|xn\\-\\-[\\pL\\pN-]+)+\\.?)|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\\[(?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))\\])(:[0-9]+)?(?:\\/ (?:[\\pL\\pN\\-._\\~!$&\\'()*+,;=:@]|\\%[0-9A-Fa-f]{2})* )*(?:\\? (?:[\\pL\\pN\\-._\\~!$&\\'[\\]()*+,;=:@\\/?]|\\%[0-9A-Fa-f]{2})* )?(?:\\# (?:[\\pL\\pN\\-._\\~!$&\\'()*+,;=:@\\/?]|\\%[0-9A-Fa-f]{2})* )?$/ix\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/constraints/valid.cr",
    "content": "# Tells the validator that it should also validate objects embedded as properties on an object being validated.\n#\n# # Configuration\n#\n# ## Optional Arguments\n#\n# NOTE: This constraint does not support a `message` argument.\n#\n# ### groups\n#\n# **Type:** `Array(String) | String | Nil` **Default:** `nil`\n#\n# The [validation groups][Athena::Validator::Constraint--validation-groups] this constraint belongs to.\n# `AVD::Constraint::DEFAULT_GROUP` is assumed if `nil`.\n#\n# ### payload\n#\n# **Type:** `Hash(String, String)?` **Default:** `nil`\n#\n# Any arbitrary domain-specific data that should be stored with this constraint.\n# The [payload][Athena::Validator::Constraint--payload] is not used by `Athena::Validator`, but its processing is completely up to you.\n#\n# # Usage\n#\n# Without this constraint, objects embedded in another object are not valided.\n#\n# ```\n# class SubObjectOne\n#   include AVD::Validatable\n#\n#   @[Assert::NotBlank]\n#   getter string : String = \"\"\n# end\n#\n# class SubObjectTwo\n#   include AVD::Validatable\n#\n#   @[Assert::NotBlank]\n#   getter string : String = \"\"\n# end\n#\n# class MyObject\n#   include AVD::Validatable\n#\n#   # This object is not validated when validating `MyObject`.\n#   getter sub_object_one : SubObjectOne = SubObjectOne.new\n#\n#   # Have the validator also validate `SubObjectTwo` when validating `MyObject`.\n#   @[Assert::Valid]\n#   getter sub_object_two : SubObjectTwo = SubObjectTwo.new\n# end\n# ```\nclass Athena::Validator::Constraints::Valid < Athena::Validator::Constraint\n  getter? traverse : Bool\n\n  def initialize(\n    @traverse : Bool = true,\n    groups : Array(String) | String | Nil = nil,\n    payload : Hash(String, String)? = nil,\n  )\n    super \"\", groups, payload\n  end\n\n  class Validator < Athena::Validator::ConstraintValidator\n    # :inherit:\n    def validate(value : _, constraint : AVD::Constraints::Valid) : Nil\n      return if value.nil?\n\n      self\n        .context\n        .validator\n        .in_context(self.context)\n        .validate value, groups: self.context.group\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/exception/invalid_argument.cr",
    "content": "# Represents a code logic error that should lead directly to a fix in your code.\nclass Athena::Validator::Exception::InvalidArgument < ArgumentError\n  include Athena::Validator::Exception\nend\n"
  },
  {
    "path": "src/components/validator/src/exception/logic.cr",
    "content": "# Represents a code logic error that should lead directly to a fix in your code.\nclass Athena::Validator::Exception::Logic < ::Exception\n  include Athena::Validator::Exception\nend\n"
  },
  {
    "path": "src/components/validator/src/exception/unexpected_value_error.cr",
    "content": "# Raised when an `AVD::ConstraintValidatorInterface` is unable to validate a value of an unsupported type.\n#\n# See `AVD::ConstraintValidator#raise_invalid_type`.\nclass Athena::Validator::Exception::UnexpectedValueError < ArgumentError\n  include Athena::Validator::Exception\n\n  # A string representing a union of the supported_type(s).\n  getter supported_types : String\n\n  def initialize(value : _, @supported_types : String)\n    super \"Expected argument of type '#{supported_types}', '#{typeof(value)}' given.\"\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/execution_context.cr",
    "content": "require \"./validator/validator_interface\"\nrequire \"./execution_context_interface\"\n\n# Basic implementation of `AVD::ExecutionContextInterface`.\nclass Athena::Validator::ExecutionContext\n  include Athena::Validator::ExecutionContextInterface\n\n  # :inherit:\n  getter constraint : AVD::Constraint?\n\n  # :inherit:\n  getter group : String?\n\n  # :inherit:\n  getter validator : AVD::Validator::ValidatorInterface\n\n  # :inherit:\n  getter violations : AVD::Violation::ConstraintViolationList = AVD::Violation::ConstraintViolationList.new\n\n  # :inherit:\n  @property_path : String = \"\"\n\n  # :inherit:\n  getter metadata : AVD::Metadata::MetadataInterface? = nil\n\n  # The value that is currently being validated.\n  @value_container : AVD::Container = AVD::ValueContainer.new(nil)\n\n  protected getter root_container : AVD::Container\n\n  # The object that is currently being validated.\n  getter object_container : AVD::Container = AVD::ValueContainer.new(nil)\n\n  protected def initialize(@validator : AVD::Validator::ValidatorInterface, root : _)\n    @root_container = AVD::ValueContainer.new root\n  end\n\n  # :nodoc:\n  def constraint=(@constraint : AVD::Constraint?); end\n\n  # :nodoc:\n  def group=(@group : String?); end\n\n  # :inherit:\n  def value\n    @value_container.value\n  end\n\n  # :inherit:\n  def object\n    @object_container.value\n  end\n\n  # :inherit:\n  def root\n    @root_container.value\n  end\n\n  # :inherit:\n  def class_name\n    @metadata.try &.class_name\n  end\n\n  # :inherit:\n  def property_name : String?\n    @metadata.try &.name\n  end\n\n  # :inherit:\n  def property_path(path : String = \"\") : String\n    AVD::PropertyPath.append @property_path, path\n  end\n\n  # :nodoc:\n  def set_node(value : _, object : _, metadata : AVD::Metadata::MetadataInterface?, property_path : String) : Nil\n    @value_container = AVD::ValueContainer.new value\n    @object_container = AVD::ValueContainer.new object\n    @metadata = metadata\n    @property_path = property_path\n  end\n\n  # :inherit:\n  def add_violation(message : String, code : String) : Nil\n    self.build_violation(message, code).add\n  end\n\n  # :inherit:\n  def add_violation(message : String, code : String, value : _) : Nil\n    self.build_violation(message, code, value).add\n  end\n\n  # :inherit:\n  def add_violation(message : String, parameters : Hash(String, String) = {} of String => String) : Nil\n    self.build_violation(message, parameters).add\n  end\n\n  # :inherit:\n  def build_violation(message : String, code : String) : AVD::Violation::ConstraintViolationBuilderInterface\n    self.build_violation(message).code(code)\n  end\n\n  # :inherit:\n  def build_violation(message : String, code : String, value : _) : AVD::Violation::ConstraintViolationBuilderInterface\n    self.build_violation(message).code(code).add_parameter(\"{{ value }}\", value)\n  end\n\n  # :inherit:\n  def build_violation(message : String, parameters : Hash(String, String) = {} of String => String) : AVD::Violation::ConstraintViolationBuilderInterface\n    AVD::Violation::ConstraintViolationBuilder.new(\n      @violations,\n      @constraint,\n      message,\n      parameters,\n      @root_container,\n      @property_path,\n      @value_container,\n    )\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/execution_context_interface.cr",
    "content": "# Stores contextual data related to the current validation run.\n#\n# This includes the violations generated so far, the current constraint, value being validated, etc.\n#\n# ### Adding Violations\n#\n# As mentioned in the `AVD::ConstraintValidatorInterface` documentation,\n# violations are not returned from the `AVD::ConstraintValidatorInterface#validate` method.\n# Instead they are added to the `AVD::ConstraintValidatorInterface#context` instance.\n#\n# The simplest way to do so is via the `#add_violation` method, which accepts the violation message,\n# and any parameters that should be used to render the message.\n# Additional overloads exist to make adding a value with a specific message, and code, or message, code, and `{{ value }}` placeholder value easier.\n#\n# #### Building violations\n#\n# In some cases you may wish to add additional data to the `AVD::Violation::ConstraintViolationInterface` before adding it to `self`.\n# To do this, you can also use the `#build_violation` method, which returns an `AVD::Violation::ConstraintViolationBuilderInterface`\n# that can be used to construct a violation, with an easier API.\nmodule Athena::Validator::ExecutionContextInterface\n  # Adds a violation with the provided *message*, and optionally *parameters* based on the node currently being validated.\n  abstract def add_violation(message : String, parameters : Hash(String, String) = {} of String => String) : Nil\n\n  # Adds a violation with the provided *message* and *code*\n  abstract def add_violation(message : String, code : String) : Nil\n\n  # Adds a violation with the provided *message*, and *code*, *value* parameter.\n  #\n  # The provided *value* is added to the violations' parameters as `\"{{ value }}\"`.\n  abstract def add_violation(message : String, code : String, value : _) : Nil\n\n  # Returns an `AVD::Violation::ConstraintViolationBuilderInterface` with the provided *message*.\n  #\n  # Can be used to add additional information to the `AVD::Violation::ConstraintViolationInterface` being adding it to `self`.\n  abstract def build_violation(message : String, parameters : Hash(String, String) = {} of String => String) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Returns an `AVD::Violation::ConstraintViolationBuilderInterface` with the provided *message*, and *code*.\n  abstract def build_violation(message : String, code : String) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Returns an `AVD::Violation::ConstraintViolationBuilderInterface` with the provided *message*, and *code*, and *value*.\n  #\n  # The provided *value* is added to the violations' parameters as `\"{{ value }}\"`.\n  abstract def build_violation(message : String, code : String, value : _) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Returns the class that is currently being validated.\n  abstract def class_name\n\n  # Returns the `AVD::Constraint` that is currently being validated, if any.\n  abstract def constraint : AVD::Constraint?\n\n  # Returns the group that is currently being validated, if any.\n  abstract def group : String?\n\n  # Returns an `AVD::Metadata::MetadataInterface` object for the value currently being validated.\n  #\n  # This would be an `AVD::Metadata::PropertyMetadataInterface` if the current value is an object,\n  # an `AVD::Metadata::GenericMetadata` if the current value is a plain value, and an\n  # `AVD::Metadata::ClassMetadata` if the current value value is an entire class.\n  abstract def metadata : AVD::Metadata::MetadataInterface?\n\n  # Returns the object that is currently being validated.\n  abstract def object\n\n  # Returns the property name of the node currently being validated.\n  abstract def property_name : String?\n\n  # Returns the path to the property that is currently being validated.\n  #\n  # For example, given a `Person` object that has an `Address` property;\n  # the property path would be empty initially.  When the `address` property\n  # is being validated the *property_path* would be `address`.\n  # When the street property of the related `Address` object is being validated\n  # the *property_path* would be `address.street`.\n  #\n  # This also works for collections of objects.  If the `Person` object had multiple\n  # addresses, the property path when validating the first street of the first address\n  # would be `addresses[0].street`.\n  abstract def property_path : String\n\n  # Returns the object initially passed to `AVD::Validator::ValidatorInterface#validate`.\n  abstract def root\n\n  # Returns a reference to an `AVD::Validator::ValidatorInterface` that can be used to validate\n  # additional constraints as part of another constraint.\n  abstract def validator : AVD::Validator::ValidatorInterface\n\n  # Returns the value that is currently being validated.\n  abstract def value\n\n  # Returns the `AVD::Violation::ConstraintViolationInterface` instances generated by the validator thus far.\n  abstract def violations : AVD::Violation::ConstraintViolationListInterface\n\n  # Internal\n\n  # :nodoc:\n  protected abstract def set_node(value : _, object : _, metadata : AVD::Metadata::MetadataInterface?, property_path : String) : Nil\n\n  # :nodoc:\n  protected abstract def group=(group : String)\n\n  # :nodoc:\n  protected abstract def constraint=(constraint : AVD::Constraint)\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/cascading_strategy.cr",
    "content": "# Determines whether an object should be cascaded.\n#\n# If cascading is enabled, the validator will also validate embedded objects.\nenum Athena::Validator::Metadata::CascadingStrategy\n  None\n  Cascade\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/class_metadata.cr",
    "content": "require \"./generic_metadata\"\n\n# Represents metadata associated with an `AVD::Validatable` instance.\n#\n# `self` is lazily initialized and cached at the class level.\n#\n# Includes metadata about the class; such as its name, constraints, etc.\nclass Athena::Validator::Metadata::ClassMetadata(T)\n  include Athena::Validator::Metadata::GenericMetadata\n\n  # Builds `self`, auto registering any annotation based annotations on `T`,\n  # as well as those registered via `T.load_metadata`.\n  def self.build : self\n    class_metadata = new\n\n    {% begin %}\n      # Add property constraints\n      {% for ivar, idx in T.instance_vars %}\n        {% for constraint in AVD::Constraint.all_subclasses.reject { |c| c.abstract? || (c.type_vars.size > 0 && c.type_vars.first.stringify != \"ValueType\") } %}\n          {% ann_name = constraint.name(generic_args: false).split(\"::\").last.id %}\n\n          {% if ann = ivar.annotation Assert.constant(ann_name).resolve %}\n            {% default_arg = ann.args.empty? ? nil : ann.args.first %}\n\n            {% if default_arg.is_a? ArrayLiteral %}\n              {% default_arg = default_arg.map do |arg|\n                   if arg.is_a? Annotation\n                     arg_name = arg.stringify.gsub(/@\\[/, \"\").gsub(/\\(.*/, \"\").split(\"::\").last.gsub(/\\]/, \"\")\n\n                     inner_default_arg = arg.args.empty? ? nil : arg.args.first\n\n                     # Support only 2 levels deep for now.\n                     inner_default_arg = if inner_default_arg.is_a? ArrayLiteral\n                                           inner_default_arg.map do |inner_arg|\n                                             if inner_arg.is_a? Annotation\n                                               inner_arg_name = inner_arg.stringify.gsub(/@\\[/, \"\").gsub(/\\(.*/, \"\").split(\"::\").last.gsub(/\\]/, \"\")\n\n                                               inner_inner_default_arg = inner_arg.args.empty? ? nil : inner_arg.args.first\n\n                                               %(AVD::Constraints::#{inner_arg_name.id}.new(#{inner_inner_default_arg ? \"#{inner_inner_default_arg},\".id : \"\".id}#{inner_arg.named_args.double_splat})).id\n                                             else\n                                               inner_arg\n                                             end\n                                           end\n                                         else\n                                           inner_default_arg\n                                         end\n\n                     # Hack this to work correctly for now.\n                     if arg_name == \"All\" || arg_name == \"AtLeastOneOf\"\n                       inner_default_arg = \"#{inner_default_arg} of AVD::Constraint\".id\n                     end\n\n                     # Resolve constraints from the annotations,\n                     # TODO: Figure out a better way to do this.\n                     %(AVD::Constraints::#{arg_name.id}.new(#{inner_default_arg ? \"#{inner_default_arg},\".id : \"\".id}#{arg.named_args.double_splat})).id\n                   else\n                     arg\n                   end\n                 end %}\n            {% end %}\n\n            class_metadata.add_property_constraint(\n              AVD::Metadata::PropertyMetadata(T, {{idx}}).new({{ivar.name.stringify}}),\n              {{constraint.name(generic_args: false).id}}.new(\n                {{ default_arg ? \"#{default_arg},\".id : \"\".id }} # Default argument\n                {{ ann.named_args.double_splat }}\n              )\n            )\n          {% end %}\n        {% end %}\n      {% end %}\n\n      # Add getter constraints\n      {% for m, idx in T.methods %}\n        {% for constraint in AVD::Constraint.all_subclasses.reject &.abstract? %}\n          {% ann_name = constraint.name(generic_args: false).split(\"::\").last.id %}\n\n          {% if ann_name != \"Callback\" && (ann = m.annotation Assert.constant(ann_name).resolve) %}\n            {% default_arg = ann.args.empty? ? nil : ann.args.first %}\n\n            class_metadata.add_getter_constraint(\n              AVD::Metadata::GetterMetadata(T, {{idx}}).new({{m.name.stringify}}),\n              {{constraint.name(generic_args: false).id}}.new(\n                {{ default_arg ? \"#{default_arg},\".id : \"\".id }} # Default argument\n                {{ ann.named_args.double_splat }}\n              )\n            )\n          {% end %}\n        {% end %}\n      {% end %}\n\n      # Add callback constraints\n      {% for callback in T.methods.select &.annotation(Assert::Callback) %}\n        class_metadata.add_constraint AVD::Constraints::Callback.new(callback_name: {{callback.name.stringify}}, {{callback.annotation(Assert::Callback).named_args.double_splat}})\n      {% end %}\n\n      {% for callback in T.class.methods.select &.annotation(Assert::Callback) %}\n        class_metadata.add_constraint AVD::Constraints::Callback.new(callback: ->T.{{callback.name.id}}(AVD::Constraints::Callback::ValueContainer, AVD::ExecutionContextInterface, Hash(String, String)?), {{callback.annotation(Assert::Callback).named_args.double_splat}})\n      {% end %}\n    {% end %}\n\n    # Also support adding constraints via code\n    {% if T.class.has_method? :load_metadata %}\n      T.load_metadata class_metadata\n    {% end %}\n\n    # Check for group sequences\n    {% if group_sequence = T.annotation Assert::GroupSequence %}\n      class_metadata.group_sequence = [{{group_sequence.args.splat}}]\n    {% end %}\n\n    class_metadata\n  end\n\n  # The `#class_name` based group for `self`.\n  getter default_group : String\n\n  # The `AVD::Constraints::GroupSequence` used by `self`, if any.\n  getter group_sequence : AVD::Constraints::GroupSequence? = nil\n\n  @getters : Hash(String, AVD::Metadata::PropertyMetadataInterface) = Hash(String, AVD::Metadata::PropertyMetadataInterface).new\n  @members : Hash(String, Array(AVD::Metadata::PropertyMetadataInterface)) = Hash(String, Array(AVD::Metadata::PropertyMetadataInterface)).new\n  @properties : Hash(String, AVD::Metadata::PropertyMetadataInterface) = Hash(String, AVD::Metadata::PropertyMetadataInterface).new\n\n  def initialize\n    @default_group = T.to_s\n  end\n\n  def class_name : T.class\n    T\n  end\n\n  # Adds each of the provided *constraints* to `self`.\n  def add_constraint(constraints : Array(AVD::Constraint)) : self\n    constraints.each do |c|\n      self.add_constraint c\n    end\n\n    self\n  end\n\n  # :inherit:\n  #\n  # Also adds the `#class_name` based group via `AVD::Constraint#add_implicit_group`.\n  def add_constraint(constraint : AVD::Constraint) : self\n    constraint.add_implicit_group @default_group\n\n    super constraint\n\n    self\n  end\n\n  # Adds the provided *constraint* to the provided *method_name*.\n  def add_getter_constraint(method_name : String, constraint : AVD::Constraint) : self\n    self.add_getter_constraint AVD::Metadata::GetterMetadata(T, Nil).new(method_name), constraint\n  end\n\n  # Adds a hash of constraints to `self`, where the keys represent the property names, and the value\n  # is the constraint/array of constraints to add.\n  def add_property_constraints(property_hash : Hash(String, AVD::Constraint | Array(AVD::Constraint))) : self\n    property_hash.each do |property_name, constraints|\n      self.add_property_constraint property_name, constraints\n    end\n\n    self\n  end\n\n  # Adds each of the provided *constraints* to the provided *property_name*.\n  def add_property_constraint(property_name : String, constraints : Array(AVD::Constraint)) : self\n    constraints.each do |c|\n      self.add_property_constraint property_name, c\n    end\n\n    self\n  end\n\n  # Adds the provided *constraint* to the provided *property_name*.\n  def add_property_constraint(property_name : String, constraint : AVD::Constraint) : self\n    self.add_property_constraint AVD::Metadata::PropertyMetadata(T, Nil).new(property_name), constraint\n  end\n\n  # Returns an array of the properties who `self` has constraints defined for.\n  def constrained_properties : Array(String)\n    @members.keys\n  end\n\n  # Sets the `AVD::Constraints::GroupSequence` that should be used for `self`.\n  #\n  # Raises an `AVD::Exception::InvalidArgument` if `self` is an `AVD::Constraints::GroupSequence::Provider`,\n  # the *sequence* contains `AVD::Constraint::DEFAULT_GROUP`,\n  # or the `#class_name` based group is missing.\n  def group_sequence=(sequence : Array(String) | AVD::Constraints::GroupSequence) : self\n    raise AVD::Exception::InvalidArgument.new \"Defining a static group sequence is not allowed with a group sequence provider.\" if @group_sequence_provider\n\n    if sequence.is_a? Array\n      sequence = AVD::Constraints::GroupSequence.new sequence\n    end\n\n    if sequence.groups.includes? AVD::Constraint::DEFAULT_GROUP\n      raise AVD::Exception::InvalidArgument.new \"The group '#{AVD::Constraint::DEFAULT_GROUP}' is not allowed in group sequences.\"\n    end\n\n    unless sequence.groups.includes? @default_group\n      raise AVD::Exception::InvalidArgument.new \"The group '#{@default_group}' is missing from the group sequence.\"\n    end\n\n    @group_sequence = sequence\n\n    self\n  end\n\n  # Denotes `self` as a `AVD::Constraints::GroupSequence::Provider`.\n  def group_sequence_provider=(active : Bool) : Nil\n    raise AVD::Exception::InvalidArgument.new \"Defining a group sequence provider is not allowed with a static group sequence.\" unless @group_sequence.nil?\n    # TODO: ensure `T` implements the module interface\n    @group_sequence_provider = active\n  end\n\n  # Returns `true` if `self` has property metadata for the provided *property_name*.\n  def has_property_metadata?(property_name : String) : Bool\n    @members.has_key? property_name\n  end\n\n  # Returns an `AVD::Metadata::PropertyMetadataInterface` instance for the provided *property_name*, if any.\n  def property_metadata(property_name : String) : Array(AVD::Metadata::PropertyMetadataInterface)\n    @members.fetch(property_name) { [] of AVD::Metadata::PropertyMetadataInterface }\n  end\n\n  def name : String?\n    nil\n  end\n\n  protected def add_getter_constraint(getter_metadata : AVD::Metadata::PropertyMetadataInterface, constraint : AVD::Constraint) : self\n    unless @getters.has_key? getter_metadata.name\n      @getters[getter_metadata.name] = getter_metadata\n\n      self.add_property_metadata getter_metadata\n    end\n\n    constraint.add_implicit_group @default_group\n\n    @getters[getter_metadata.name].add_constraint constraint\n\n    self\n  end\n\n  protected def add_property_constraint(property_metadata : AVD::Metadata::PropertyMetadataInterface, constraint : AVD::Constraint) : self\n    unless @properties.has_key? property_metadata.name\n      @properties[property_metadata.name] = property_metadata\n\n      self.add_property_metadata property_metadata\n    end\n\n    constraint.add_implicit_group @default_group\n\n    @properties[property_metadata.name].add_constraint constraint\n\n    self\n  end\n\n  protected def invoke_callback(name : String, object : AVD::Validatable, context : AVD::ExecutionContextInterface, payload : Hash(String, String)?) : Nil\n    {% begin %}\n      case name\n        {% for callback in T.methods.select &.annotation(Assert::Callback) %}\n          when {{callback.name.stringify}}\n            if object.responds_to?({{callback.name.id.symbolize}})\n              object.{{callback.name.id}}(context, payload)\n            end\n        {% end %}\n      else\n        raise \"BUG: Unknown method #{name} within #{T}\"\n      end\n    {% end %}\n  end\n\n  private def add_property_metadata(metadata : AVD::Metadata::PropertyMetadataInterface) : Nil\n    (@members[metadata.name] ||= Array(AVD::Metadata::PropertyMetadataInterface).new) << metadata\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/generic_metadata.cr",
    "content": "require \"./metadata_interface\"\n\nmodule Athena::Validator::Metadata::GenericMetadata\n  include Athena::Validator::Metadata::MetadataInterface\n\n  @constraints_by_group = {} of String => Array(AVD::Constraint)\n\n  getter constraints : Array(AVD::Constraint) = [] of AVD::Constraint\n\n  # :inherit:\n  getter cascading_strategy : AVD::Metadata::CascadingStrategy = AVD::Metadata::CascadingStrategy::None\n\n  # Adds the provided *constraint* to `self`'s `#constraints` array.\n  #\n  # Sets `#cascading_strategy` to `AVD::Metadata::CascadingStrategy::Cascade` if the *constraint* is `AVD::Constraints::Valid`.\n  def add_constraint(constraint : AVD::Constraint) : AVD::Metadata::GenericMetadata\n    if constraint.is_a? AVD::Constraints::Valid\n      @cascading_strategy = :cascade\n\n      return self\n    end\n\n    @constraints << constraint\n\n    constraint.groups.each do |group|\n      (@constraints_by_group[group] ||= Array(AVD::Constraint).new) << constraint\n    end\n\n    self\n  end\n\n  # Adds each of the provided *constraints* to `self`.\n  def add_constraints(constraints : Array(AVD::Constraint)) : AVD::Metadata::GenericMetadata\n    constraints.each &->add_constraint(AVD::Constraint)\n\n    self\n  end\n\n  # :inherit:\n  def find_constraints(group : String) : Array(AVD::Constraint)\n    @constraints_by_group[group]? || Array(AVD::Constraint).new\n  end\n\n  protected def value(entity : AVD::Validatable)\n    raise \"BUG: Invoked default value method.\"\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/getter_metadata.cr",
    "content": "require \"./property_metadata_interface\"\n\nclass Athena::Validator::Metadata::GetterMetadata(EntityType, MethodIdx)\n  include Athena::Validator::Metadata::GenericMetadata\n  include Athena::Validator::Metadata::PropertyMetadataInterface\n\n  # :inherit:\n  getter name : String\n\n  def initialize(@name : String); end\n\n  # Returns the class the method `self` represents, belongs to.\n  def class_name : EntityType.class\n    EntityType\n  end\n\n  protected def value(obj : EntityType)\n    {% begin %}\n      {% unless MethodIdx == Nil %}\n        obj.{{EntityType.methods[MethodIdx].name.id}}\n      {% else %}\n        case @name\n          {% for m in EntityType.methods.select &.args.empty? %}\n            when {{m.name.stringify}} then obj.{{m.name.id}}\n          {% end %}\n        else\n          raise \"BUG: Unknown method '#{@name}' within #{EntityType}.\"\n        end\n      {% end %}\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/metadata.cr",
    "content": "# :nodoc:\nclass Athena::Validator::Metadata::Metadata\n  include Athena::Validator::Metadata::GenericMetadata\n\n  def class_name : Nil\n  end\n\n  def name : String?\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/metadata_factory.cr",
    "content": "require \"./metadata_factory_interface\"\n\n# Basic implementation of `AVD::Metadata::MetadataFactoryInterface`.\nclass Athena::Validator::Metadata::MetadataFactory\n  include Athena::Validator::Metadata::MetadataFactoryInterface\n\n  def metadata(object : AVD::Validatable) : AVD::Metadata::ClassMetadata\n    object.class.validation_class_metadata\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/metadata_factory_interface.cr",
    "content": "module Athena::Validator::Metadata::MetadataFactoryInterface\n  # Returns an `AVD::Metadata::ClassMetadata` instance for the related `AVD::Validatable` *object*.\n  abstract def metadata(object : AVD::Validatable) : AVD::Metadata::ClassMetadata\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/metadata_interface.cr",
    "content": "module Athena::Validator::Metadata::MetadataInterface\n  # Returns the `AVD::Metadata::CascadingStrategy` for `self`.\n  abstract def cascading_strategy : AVD::Metadata::CascadingStrategy\n\n  abstract def constraints : Array(AVD::Constraint)\n\n  # Returns an array of all constraints in the provided *group*.\n  abstract def find_constraints(group : String) : Array(AVD::Constraint)\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/property_metadata.cr",
    "content": "require \"./property_metadata_interface\"\n\nclass Athena::Validator::Metadata::PropertyMetadata(EntityType, PropertyIdx)\n  include Athena::Validator::Metadata::GenericMetadata\n  include Athena::Validator::Metadata::PropertyMetadataInterface\n\n  # :inherit:\n  getter name : String\n\n  def initialize(@name : String); end\n\n  # Returns the class the property `self` represents, belongs to.\n  def class_name : EntityType.class\n    EntityType\n  end\n\n  protected def value(obj : EntityType)\n    {% begin %}\n      {% unless PropertyIdx == Nil %}\n        obj.@{{EntityType.instance_vars[PropertyIdx].name.id}}\n      {% else %}\n        case @name\n          {% for ivar in EntityType.instance_vars %}\n            when {{ivar.name.stringify}} then obj.@{{ivar.id}}\n          {% end %}\n        else\n          raise \"BUG: Unknown property '#{@name}' within #{EntityType}.\"\n        end\n      {% end %}\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/metadata/property_metadata_interface.cr",
    "content": "# Stores metadata associated with a specific property.\nmodule Athena::Validator::Metadata::PropertyMetadataInterface\n  include Athena::Validator::Metadata::MetadataInterface\n\n  # Returns the name of the member represented by `self`.\n  abstract def name : String\n\n  # Returns the value of the member represented by `self.\n  protected abstract def value(obj : ADVD::Valdatable)\nend\n"
  },
  {
    "path": "src/components/validator/src/property_path.cr",
    "content": "# Utility type for working with property paths.\nmodule Athena::Validator::PropertyPath\n  # Appends the provided *sub_path* to the provided *base_path* based on the following rules:\n  #\n  # * If the base path is empty, the sub path is returned as is.\n  # * If the base path is not empty, and the sub path starts with an `[`,\n  # the concatenation of the two paths is returned.\n  # * If the base path is not empty, and the sub path does not start with an `[`,\n  # the concatenation of the two paths is returned, separated by a `.`.\n  #\n  # ```\n  # AVD::PropertyPath.append \"\", \"sub_path\"          # => \"sub_path\"\n  # AVD::PropertyPath.append \"base_path\", \"[0]\"      # => \"base_path[0]\"\n  # AVD::PropertyPath.append \"base_path\", \"sub_path\" # => \"base_path.sub_path\"\n  # ```\n  def self.append(base_path : String, sub_path : String) : String\n    return base_path if sub_path.blank?\n\n    return \"#{base_path}#{sub_path}\" if sub_path.starts_with? '['\n\n    !base_path.blank? ? \"#{base_path}.#{sub_path}\" : sub_path\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/spec/abstract_validator_test_case.cr",
    "content": "# :nodoc:\nabstract struct Athena::Validator::Spec::AbstractValidatorTestCase < ASPEC::TestCase\n  private class SubEntity\n    include AVD::Validatable\n\n    property value : String?\n  end\n\n  private abstract class Parent\n    macro inherited\n      include AVD::Validatable\n    end\n  end\n\n  private class EntityParent < Parent\n    property data : String = \"data\"\n    property child : Entity? = nil\n  end\n\n  private class Entity < Parent\n    property first_name : String?\n    property! last_name : String\n    property! sub_object : SubEntity\n    property! sub_object2 : SubEntity\n    property! hash_sub_object : Hash(String, SubEntity)\n    property! nested_hash_sub_object : Hash(Int32, Hash(String, SubEntity))\n    property! scalar_array : Array(Int32 | String)\n    property! nil_array : Array(Nil)\n    property! data_hash : Hash(String, String)\n  end\n\n  @metadata : AVD::Metadata::ClassMetadata(Entity)\n  @sub_object_metadata : AVD::Metadata::ClassMetadata(SubEntity)\n  @metadata_factory : AVD::Spec::MockMetadataFactory(EntityParent, Entity, SubEntity, EntitySequenceProvider, EntityGroupSequenceProvider)\n\n  def initialize\n    @metadata = AVD::Metadata::ClassMetadata(Entity).new\n    @sub_object_metadata = AVD::Metadata::ClassMetadata(SubEntity).new\n    @metadata_factory = AVD::Spec::MockMetadataFactory(EntityParent, Entity, SubEntity, EntitySequenceProvider, EntityGroupSequenceProvider).new\n    @metadata_factory.add_metadata Entity, @metadata\n    @metadata_factory.add_metadata SubEntity, @sub_object_metadata\n  end\n\n  abstract def validate(value, constraints, groups) : AVD::Violation::ConstraintViolationListInterface\n  abstract def validate_property(object, property_name, groups) : AVD::Violation::ConstraintViolationListInterface\n  abstract def validate_property_value(object, property_name, value, groups) : AVD::Violation::ConstraintViolationListInterface\n\n  def test_validate : Nil\n    callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      context.class_name.should be_nil\n      context.property_name.should be_nil\n      context.property_path.should be_empty\n      context.group.should eq \"group\"\n      context.root.should eq \"Fred\"\n      context.value.should eq \"Fred\"\n      value.should eq \"Fred\"\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    constraint = AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n    violations = self.validate \"Fred\", constraint, \"group\"\n\n    violations.size.should eq 1\n\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should be_empty\n    violation.root.should eq \"Fred\"\n    violation.invalid_value.should eq \"Fred\"\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_class_constraint : Nil\n    object = Entity.new\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      context.class_name.should eq Entity\n      context.property_name.should be_nil\n      context.property_path.should be_empty\n      context.group.should eq \"group\"\n      context.root.should eq object\n      context.value.should eq object\n      value.should eq object\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n    violations = self.validate object, groups: \"group\"\n\n    violations.size.should eq 1\n\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should be_empty\n    violation.root.should eq object\n    violation.invalid_value.should eq object\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_property_constraint : Nil\n    object = Entity.new\n    object.first_name = \"Fred\"\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      property_metadatas = @metadata.property_metadata \"first_name\"\n\n      context.class_name.should eq Entity\n      context.property_name.should eq \"first_name\"\n      context.property_path.should eq \"first_name\"\n      context.group.should eq \"group\"\n      property_metadatas.first.should eq context.metadata\n      context.root.should eq object\n      context.value.should eq \"Fred\"\n      value.should eq \"Fred\"\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    @metadata.add_property_constraint \"first_name\", AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n    violations = self.validate object, groups: \"group\"\n\n    violations.size.should eq 1\n\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should eq \"first_name\"\n    violation.root.should eq object\n    violation.invalid_value.should eq \"Fred\"\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_getter_constraint : Nil\n    object = Entity.new\n    object.first_name = \"Fred\"\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      property_metadatas = @metadata.property_metadata \"first_name\"\n\n      context.class_name.should eq Entity\n      context.property_name.should eq \"first_name\"\n      context.property_path.should eq \"first_name\"\n      context.group.should eq \"group\"\n      property_metadatas.first.should eq context.metadata\n      context.root.should eq object\n      context.value.should eq \"Fred\"\n      value.should eq \"Fred\"\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    @metadata.add_getter_constraint \"first_name\", AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n    violations = self.validate object, groups: \"group\"\n\n    violations.size.should eq 1\n\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should eq \"first_name\"\n    violation.root.should eq object\n    violation.invalid_value.should eq \"Fred\"\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_object_in_hash : Nil\n    object = Entity.new\n    hash = {\"key\" => object}\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      context.class_name.should eq Entity\n      context.property_name.should be_nil\n      context.property_path.should eq \"[key]\"\n      context.group.should eq \"group\"\n      context.metadata.should eq @metadata\n      context.root.should eq hash\n      context.value.should eq object\n      value.should eq object\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n    violations = self.validate hash, groups: \"group\"\n\n    violations.size.should eq 1\n\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should eq \"[key]\"\n    violation.root.should eq hash\n    violation.invalid_value.should eq object\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_object_in_nested_hash : Nil\n    object = Entity.new\n    hash = {2 => {\"key\" => object}}\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      context.class_name.should eq Entity\n      context.property_name.should be_nil\n      context.property_path.should eq \"[2][key]\"\n      context.group.should eq \"group\"\n      context.metadata.should eq @metadata\n      context.root.should eq hash\n      context.value.should eq object\n      value.should eq object\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n    violations = self.validate hash, groups: \"group\"\n\n    violations.size.should eq 1\n\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should eq \"[2][key]\"\n    violation.root.should eq hash\n    violation.invalid_value.should eq object\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_ignores_null_sub_objects : Nil\n    object = Entity.new\n\n    @metadata.add_property_constraint \"sub_object\", AVD::Constraints::Valid.new\n\n    self.validate(object).should be_empty\n  end\n\n  def test_validate_only_traversal_cascaded_hash : Nil\n    object = Entity.new\n    object.hash_sub_object = {\"key\" => SubEntity.new}\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context, _payload|\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    @metadata.add_property_constraint \"hash_sub_object\", AVD::Constraints::Callback.new callback: AVD::Constraints::Callback::CallbackProc.new { }, groups: [\"group\"]\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n    self.validate(object, groups: \"group\").should be_empty\n  end\n\n  {% for method in [\"add_property_constraint\", \"add_getter_constraint\"] %}\n    {% type = method.gsub(/add_/, \"\").id %}\n\n    def test_validate_hash_sub_object_{{type}} : Nil\n      object = Entity.new\n      object.hash_sub_object = {\"key\" => SubEntity.new}\n\n      callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n        context.class_name.should eq SubEntity\n        context.property_name.should be_nil\n        context.property_path.should eq \"hash_sub_object[key]\"\n        context.group.should eq \"group\"\n        context.metadata.should eq @sub_object_metadata\n        context.root.should eq object\n        context.value.should eq object.hash_sub_object[\"key\"]\n        value.should eq object.hash_sub_object[\"key\"]\n\n        context.add_violation \"message \\{{ value }}\", {\"\\{{ value }}\" => \"value\"}\n      end\n\n      @metadata.{{method.id}} \"hash_sub_object\", AVD::Constraints::Valid.new\n      @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n      violations = self.validate object, groups: \"group\"\n\n      violations.size.should eq 1\n\n      violation = violations.first\n\n      violation.message.should eq \"message value\"\n      violation.message_template.should eq \"message \\{{ value }}\"\n      violation.parameters.should eq({\"\\{{ value }}\" => \"value\"})\n      violation.property_path.should eq \"hash_sub_object[key]\"\n      violation.root.should eq object\n      violation.invalid_value.should eq object.hash_sub_object[\"key\"]\n      violation.plural.should be_nil\n      violation.code.should be_nil\n    end\n\n    def test_validate_nested_hash_sub_object_{{type}} : Nil\n      object = Entity.new\n      object.nested_hash_sub_object = {2 => {\"key\" => SubEntity.new}}\n\n      callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n        context.class_name.should eq SubEntity\n        context.property_name.should be_nil\n        context.property_path.should eq \"nested_hash_sub_object[2][key]\"\n        context.group.should eq \"group\"\n        context.metadata.should eq @sub_object_metadata\n        context.root.should eq object\n        context.value.should eq object.nested_hash_sub_object[2][\"key\"]\n        value.should eq object.nested_hash_sub_object[2][\"key\"]\n\n        context.add_violation \"message \\{{ value }}\", {\"\\{{ value }}\" => \"value\"}\n      end\n\n      @metadata.{{method.id}} \"nested_hash_sub_object\", AVD::Constraints::Valid.new\n      @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n      violations = self.validate object, groups: \"group\"\n\n      violations.size.should eq 1\n\n      violation = violations.first\n\n      violation.message.should eq \"message value\"\n      violation.message_template.should eq \"message \\{{ value }}\"\n      violation.parameters.should eq({\"\\{{ value }}\" => \"value\"})\n      violation.property_path.should eq \"nested_hash_sub_object[2][key]\"\n      violation.root.should eq object\n      violation.invalid_value.should eq object.nested_hash_sub_object[2][\"key\"]\n      violation.plural.should be_nil\n      violation.code.should be_nil\n    end\n\n    def test_validate_hash_traversal_cannot_be_disabled_{{type}} : Nil\n      object = Entity.new\n      object.hash_sub_object = {\"key\" => SubEntity.new}\n\n      callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context, _payload|\n        context.add_violation \"message \\{{ value }}\", {\"\\{{ value }}\" => \"value\"}\n      end\n\n      @metadata.{{method.id}} \"hash_sub_object\", AVD::Constraints::Valid.new traverse: false\n      @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n      self.validate(object, groups: \"group\").size.should eq 1\n    end\n\n    def test_validate_nested_hash_traversal_cannot_be_disabled_{{type}} : Nil\n      object = Entity.new\n      object.nested_hash_sub_object = {2 => {\"key\" => SubEntity.new}}\n\n      callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context, _payload|\n        context.add_violation \"message \\{{ value }}\", {\"\\{{ value }}\" => \"value\"}\n      end\n\n      @metadata.{{method.id}} \"nested_hash_sub_object\", AVD::Constraints::Valid.new traverse: false\n      @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n\n      self.validate(object, groups: \"group\").size.should eq 1\n    end\n\n    def test_validate_ignore_scalars_during_array_traversal_{{type}} : Nil\n      object = Entity.new\n      object.scalar_array = [\"string\", 1234]\n\n      @metadata.{{method.id}} \"scalar_array\", AVD::Constraints::Valid.new\n\n      self.validate(object, groups: \"group\").should be_empty\n    end\n\n    def test_validate_ignore_null_during_array_traversal_{{type}} : Nil\n      object = Entity.new\n      object.nil_array = [nil]\n\n      @metadata.{{method.id}} \"nil_array\", AVD::Constraints::Valid.new\n\n      self.validate(object, groups: \"group\").should be_empty\n    end\n  {% end %}\n\n  def test_validate_property : Nil\n    object = Entity.new\n    object.first_name = \"Jon\"\n    object.last_name = \"Snow\"\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      property_metadatas = @metadata.property_metadata \"first_name\"\n\n      context.class_name.should eq Entity\n      context.property_name.should eq \"first_name\"\n      context.property_path.should eq \"first_name\"\n      context.group.should eq \"group\"\n      context.metadata.should eq property_metadatas.first\n      context.root.should eq object\n      context.value.should eq \"Jon\"\n      value.should eq \"Jon\"\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |_value, context, _payload|\n      context.add_violation \"other violation\"\n    end\n\n    @metadata.add_property_constraint \"first_name\", AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n    @metadata.add_property_constraint \"last_name\", AVD::Constraints::Callback.new callback: callback2, groups: [\"group\"]\n\n    violations = self.validate_property object, \"first_name\", \"group\"\n\n    violations.size.should eq 1\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should eq \"first_name\"\n    violation.root.should eq object\n    violation.invalid_value.should eq \"Jon\"\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_property_no_constraints : Nil\n    self.validate_property(Entity.new, \"last_name\").should be_empty\n  end\n\n  def test_validate_property_value : Nil\n    object = Entity.new\n    object.last_name = \"Snow\"\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      property_metadatas = @metadata.property_metadata \"first_name\"\n\n      context.class_name.should eq Entity\n      context.property_name.should eq \"first_name\"\n      context.property_path.should eq \"first_name\"\n      context.group.should eq \"group\"\n      context.metadata.should eq property_metadatas.first\n      context.root.should eq object\n      context.value.should eq \"Jon\"\n      value.should eq \"Jon\"\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |_value, context, _payload|\n      context.add_violation \"other violation\"\n    end\n\n    @metadata.add_property_constraint \"first_name\", AVD::Constraints::Callback.new callback: callback, groups: [\"group\"]\n    @metadata.add_property_constraint \"last_name\", AVD::Constraints::Callback.new callback: callback2, groups: [\"group\"]\n\n    violations = self.validate_property_value object, \"first_name\", \"Jon\", \"group\"\n\n    violations.size.should eq 1\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should eq \"first_name\"\n    violation.root.should eq object\n    violation.invalid_value.should eq \"Jon\"\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_property_value_no_constraints : Nil\n    self.validate_property_value(Entity.new, \"last_name\", \"foo\").should be_empty\n  end\n\n  def ptest_validate_object_only_once_per_group : Nil\n    object = Entity.new\n    object.sub_object = object.sub_object2 = SubEntity.new\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message\"\n    end\n\n    @metadata.add_property_constraint \"sub_object\", AVD::Constraints::Valid.new\n    @metadata.add_property_constraint \"sub_object2\", AVD::Constraints::Valid.new\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback\n\n    self.validate(object).size.should eq 1\n  end\n\n  def test_validate_different_objects_separately : Nil\n    object = Entity.new\n    object.sub_object = SubEntity.new\n    object.sub_object2 = SubEntity.new\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message\"\n    end\n\n    @metadata.add_property_constraint \"sub_object\", AVD::Constraints::Valid.new\n    @metadata.add_property_constraint \"sub_object2\", AVD::Constraints::Valid.new\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback\n\n    self.validate(object).size.should eq 2\n  end\n\n  def test_validate_single_group : Nil\n    object = Entity.new\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message\"\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group1\"]\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group2\"]\n\n    self.validate(object, groups: \"group1\").size.should eq 1\n  end\n\n  def test_validate_multiple_groups : Nil\n    object = Entity.new\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message\"\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group1\"]\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group2\"]\n\n    self.validate(object, groups: [\"group1\", \"group2\"]).size.should eq 2\n  end\n\n  def test_validate_replace_default_group_by_sequence_object : Nil\n    object = Entity.new\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"group2 message\"\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"group3 message\"\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: AVD::Constraints::Callback::CallbackProc.new { }, groups: [\"group1\"]\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: [\"group2\"]\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: [\"group3\"]\n\n    @metadata.group_sequence = AVD::Constraints::GroupSequence.new [\"group1\", \"group2\", \"group3\", \"Athena::Validator::Spec::AbstractValidatorTestCase::Entity\"]\n\n    violations = self.validate object, groups: \"default\"\n\n    violations.size.should eq 1\n    violations.first.message.should eq \"group2 message\"\n  end\n\n  def test_validate_replace_default_group_by_array : Nil\n    object = Entity.new\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"group2 message\"\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"group3 message\"\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: AVD::Constraints::Callback::CallbackProc.new { }, groups: [\"group1\"]\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: [\"group2\"]\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: [\"group3\"]\n\n    @metadata.group_sequence = [\"group1\", \"group2\", \"group3\", \"Athena::Validator::Spec::AbstractValidatorTestCase::Entity\"]\n\n    violations = self.validate object, groups: \"default\"\n\n    violations.size.should eq 1\n    violations.first.message.should eq \"group2 message\"\n  end\n\n  def test_validate_propagate_default_group_to_sub_object_when_replacing_default_group : Nil\n    object = Entity.new\n    object.sub_object = SubEntity.new\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"default group message\"\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"group sequence message\"\n    end\n\n    @metadata.add_property_constraint \"sub_object\", AVD::Constraints::Valid.new\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: [\"default\"]\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: [\"group1\"]\n\n    @metadata.group_sequence = AVD::Constraints::GroupSequence.new [\"group1\", \"Athena::Validator::Spec::AbstractValidatorTestCase::Entity\"]\n\n    violations = self.validate object, groups: \"default\"\n\n    violations.size.should eq 1\n    violations.first.message.should eq \"default group message\"\n  end\n\n  def test_validate_custom_group_when_default_group_was_replaced : Nil\n    object = Entity.new\n    object.sub_object = SubEntity.new\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"other group message\"\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"group sequence message\"\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: [\"other group\"]\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: [\"group1\"]\n\n    @metadata.group_sequence = AVD::Constraints::GroupSequence.new [\"group1\", \"Athena::Validator::Spec::AbstractValidatorTestCase::Entity\"]\n\n    violations = self.validate object, groups: \"other group\"\n\n    violations.size.should eq 1\n    violations.first.message.should eq \"other group message\"\n  end\n\n  @[DataProvider(\"get_replace_default_group\")]\n  def test_replace_default_group(sequence : Array(String | Array(String)) | AVD::Constraints::GroupSequence, expected_violations : Array) : Nil\n    object, metadata = case sequence\n                       in Array\n                         m = AVD::Metadata::ClassMetadata(EntitySequenceProvider).new\n                         @metadata_factory.add_metadata EntitySequenceProvider, m\n                         {EntitySequenceProvider.new(sequence), m}\n                       in AVD::Constraints::GroupSequence\n                         m = AVD::Metadata::ClassMetadata(EntityGroupSequenceProvider).new\n                         @metadata_factory.add_metadata EntityGroupSequenceProvider, m\n                         {EntityGroupSequenceProvider.new(sequence), m}\n                       end\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"violation in group2\"\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"violation in group3\"\n    end\n\n    metadata.add_constraint AVD::Constraints::Callback.new callback: AVD::Constraints::Callback::CallbackProc.new { }, groups: [\"group1\"]\n    metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: [\"group2\"]\n    metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: [\"group3\"]\n    metadata.group_sequence_provider = true\n\n    violations = self.validate object, groups: \"default\"\n\n    violations.size.should eq expected_violations.size\n\n    expected_violations.each_with_index do |message, idx|\n      violations[idx].message.should eq message\n    end\n  end\n\n  def get_replace_default_group : Tuple\n    {\n      {\n        AVD::Constraints::GroupSequence.new([\"group1\", \"group2\", \"group3\", \"Athena::Validator::Spec::AbstractValidatorTestCase::Entity\"]),\n        [\"violation in group2\"],\n      },\n      {\n        [\"group1\", \"group2\", \"group3\", \"Athena::Validator::Spec::AbstractValidatorTestCase::Entity\"] of String | Array(String),\n        [\"violation in group2\"],\n      },\n      {\n        AVD::Constraints::GroupSequence.new([\"group1\", [\"group2\", \"group3\"], \"Athena::Validator::Spec::AbstractValidatorTestCase::Entity\"]),\n        [\"violation in group2\", \"violation in group3\"],\n      },\n      {\n        [\"group1\", [\"group2\", \"group3\"], \"Athena::Validator::Spec::AbstractValidatorTestCase::Entity\"],\n        [\"violation in group2\", \"violation in group3\"],\n      },\n    }\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/spec/compound_constraint_test_case.cr",
    "content": "# The `AVD::Constraints::Compound` constraint allows grouping other constraints into a single reusable constraint.\n# Such as for defining requirements of a user's password.\n#\n# This type may be used to more easily test compound constraints.\n# For example, using the `AVD::Constraints::ValidPassword` constraint in the usage docs for the `Compound` constraint:\n#\n# ```\n# # The generic should be set to the type(s) that the compound constraint supports.\n# struct ValidPasswordTest < AVD::Spec::CompoundConstraintTestCase(String?)\n#   protected def create_compound : AVD::Constraints::Compound\n#     AVD::Constraints::ValidPassword.new\n#   end\n#\n#   def test_valid_password : Nil\n#     self.validate_value \"1VeryStr0ngP4$$wOrD\"\n#\n#     self.assert_no_violation\n#   end\n#\n#   @[TestWith(\n#     nil: {nil, AVD::Constraints::NotBlank.new},\n#     too_short: {\"123\", AVD::Constraints::Size.new(12..)},\n#     letter_first: {\"abc12345qwerty\", AVD::Constraints::Regex.new(/^\\d.*/)},\n#   )]\n#   def test_invalid_password(password : String?, expected_failing_constraint : AVD::Constraint) : Nil\n#     self.validate_value password\n#\n#     self.assert_violations_raised_by_compound expected_failing_constraint\n#   end\n# end\n# ```\nabstract struct Athena::Validator::Spec::CompoundConstraintTestCase(T) < ASPEC::TestCase\n  @validator : AVD::Validator::ValidatorInterface\n  @violation_list : Array(AVD::Violation::ConstraintViolationListInterface)? = nil\n  @context : AVD::ExecutionContextInterface\n  @root : String\n\n  private getter! validated_value : AVD::ValueContainer(T)\n\n  private class MockCompoundValidator < AVD::Constraints::Compound\n    def initialize(@tested_constraints : Array(AVD::Constraint))\n      super()\n    end\n\n    def constraints : AVD::Constraints::Composite::Type\n      @tested_constraints\n    end\n  end\n\n  protected def initialize\n    @root = \"root\"\n    @validator = validator = create_validator\n    @context = create_context validator\n  end\n\n  # :showdoc:\n  #\n  # Returns the compound constraint instance to be tested.\n  protected abstract def create_compound : AVD::Constraints::Compound\n\n  # :showdoc:\n  #\n  # Asserts that each of the provided *constraints* were properly raised.\n  protected def assert_violations_raised_by_compound(*constraints : AVD::Constraint) : Nil\n    validator = AVD::Constraints::Compound::Validator.new\n    context = self.create_context\n    validator.context = context\n\n    validator.validate self.validated_value.value, MockCompoundValidator.new(constraints.to_a.map(&.as(AVD::Constraint)))\n\n    expected_violations = context.violations\n\n    expected_violations.should_not be_empty, failure_message: \"Expected at least one violation for constraint(s): '#{constraints.join(\", \", &.class)}', got none.\"\n\n    failed_to_assert_violations = [] of AVD::Violation::ConstraintViolationInterface\n\n    @context.violations.each_with_index do |violation, idx|\n      if violation != expected_violations[idx]?\n        failed_to_assert_violations << violation\n      end\n    end\n\n    failed_to_assert_violations.should be_empty, failure_message: \"Expected violation(s) for constraint(s) '#{failed_to_assert_violations.join(\", \", &.constraint.class)}' to be raised by compound.\"\n  end\n\n  # :showdoc:\n  #\n  # Validates the provided *value*, populating the data required for further assertions.\n  protected def validate_value(value : T) : Nil\n    @validated_value = AVD::ValueContainer(T).new(value)\n    @validator.in_context(@context).validate(value, self.create_compound)\n  end\n\n  # :showdoc:\n  #\n  # Asserts there are *count* violations after calling `#validate_value`.\n  protected def assert_violation_count(count : Int) : Nil\n    @context.violations.size.should eq count\n  end\n\n  # :showdoc:\n  #\n  # Asserts there are no violations after calling `#validate_value`.\n  protected def assert_no_violation : Nil\n    @context.violations.should be_empty\n  end\n\n  private def create_validator : AVD::Validator::ValidatorInterface\n    AVD.validator\n  end\n\n  private def create_context(validator : AVD::Validator::ValidatorInterface? = nil) : AVD::ExecutionContextInterface\n    AVD::ExecutionContext.new validator || create_validator, @root\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/spec/constraint_validator_test_case.cr",
    "content": "# Test case designed to make testing `AVD::ConstraintValidatorInterface` easier.\n#\n# ### Example\n#\n# Using the spec from `AVD::Constraints::NotNil`:\n#\n# ```\n# # Makes for a bit less typing when needing to reference the constraint.\n# private alias CONSTRAINT = AVD::Constraints::NotNil\n#\n# # Define our test case inheriting from the abstract ConstraintValidatorTestCase.\n# struct NotNilValidatorTest < AVD::Spec::ConstraintValidatorTestCase\n#   @[DataProvider(\"valid_values\")]\n#   def test_valid_values(value : _) : Nil\n#     # Validate the value against a new instance of the constraint.\n#     self.validator.validate value, self.new_constraint\n#\n#     # Assert no violations were added to the context.\n#     self.assert_no_violation\n#   end\n#\n#   # Use data providers to reduce duplication.\n#   def valid_values : NamedTuple\n#     {\n#       string:       {\"\"},\n#       bool_false:   {false},\n#       bool_true:    {true},\n#       zero:         {0},\n#       null_pointer: {Pointer(Void).null},\n#     }\n#   end\n#\n#   def test_nil_is_invalid\n#     # Validate an invalid value against a new instance of the constraint with a custom message.\n#     self.validator.validate nil, self.new_constraint message: \"my_message\"\n#\n#     # Assert a violation with the expected message, code, and value parameter is added to the context.\n#     self\n#       .build_violation(\"my_message\", CONSTRAINT::IS_NULL_ERROR, nil)\n#       .assert_violation\n#   end\n#\n#   # Implement some abstract defs to return the validator and constraint class.\n#   private def create_validator : AVD::ConstraintValidatorInterface\n#     CONSTRAINT::Validator.new\n#   end\n#\n#   private def constraint_class : AVD::Constraint.class\n#     CONSTRAINT\n#   end\n# end\n# ```\n#\n# This type is an extension of `ASPEC::TestCase`, see that type for more information on this testing approach.\n# This approach also allows using `ASPEC::TestCase::DataProvider`s for reducing duplication within your test.\nabstract struct Athena::Validator::Spec::ConstraintValidatorTestCase < ASPEC::TestCase\n  # Used to assert that a violation added via the `AVD::ConstraintValidatorInterface` was built as expected.\n  #\n  # NOTE: This type should not be instantiated directly, use `AVD::Spec::ConstraintValidatorTestCase#build_violation` instead.\n  record Assertion, context : AVD::ExecutionContextInterface, message : String, constraint : AVD::Constraint do\n    @parameters : Hash(String, String) = Hash(String, String).new\n    @invalid_value : AVD::Container = AVD::ValueContainer.new(\"invalid_value\")\n    @property_path : String = \"property.path\"\n    @plural : Int32? = nil\n    @code : String? = nil\n    @cause : String? = nil\n\n    # Sets the `AVD::Violation::ConstraintViolationInterface#property_path` on the expected violation.\n    #\n    # Returns `self` for chaining.\n    def at_path(@property_path : String) : self\n      self\n    end\n\n    # Adds the provided *key* *value* pair to the expected violations' `AVD::Violation::ConstraintViolationInterface#parameters`.\n    #\n    # Returns `self` for chaining.\n    def add_parameter(key : String, value : _) : self\n      @parameters[key] = value.to_s\n\n      self\n    end\n\n    # Sets the `AVD::Violation::ConstraintViolationInterface#invalid_value` on the expected violation.\n    #\n    # Returns `self` for chaining.\n    def invalid_value(value : _) : self\n      @invalid_value = AVD::ValueContainer.new value\n\n      self\n    end\n\n    # Sets the `AVD::Violation::ConstraintViolationInterface#plural` on the expected violation.\n    #\n    # Returns `self` for chaining.\n    def plural(@plural : Int32) : self\n      self\n    end\n\n    # Sets the `AVD::Violation::ConstraintViolationInterface#code` on the expected violation.\n    #\n    # Returns `self` for chaining.\n    def code(@code : String?) : self\n      self\n    end\n\n    # Sets the `AVD::Violation::ConstraintViolationInterface#cause` on the expected violation.\n    #\n    # Returns `self` for chaining.\n    def cause(@cause : String?) : self\n      self\n    end\n\n    # Asserts that the violation added to the context equals the violation built via `self`.\n    def assert_violation(*, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n      expected_violations = [self.get_violation] of AVD::Violation::ConstraintViolationInterface\n\n      violations = @context.violations\n\n      violations.size.should eq(1), failure_message: \"Expected 1 violation, got #{violations.size}.\"\n\n      expected_violations.each_with_index do |expected_violation, idx|\n        actual_violation = violations[idx]\n\n        # This is derived from `AVD::Violation::ConstraintViolation#==(other : AVD::Violation::ConstraintViolationInterface)`\n        # but is broken out here to make knowing _what_ wasn't equal easier to identity within spec failures.\n        self.assert_equals \"message\", actual_violation.message, expected_violation.message, file: file, line: line\n        self.assert_equals \"message_template\", actual_violation.message_template, expected_violation.message_template, file: file, line: line\n        self.assert_equals \"parameters\", actual_violation.parameters, expected_violation.parameters, file: file, line: line\n        self.assert_equals \"root_container\", actual_violation.root_container, expected_violation.root_container, file: file, line: line\n        self.assert_equals \"property_path\", actual_violation.property_path, expected_violation.property_path, file: file, line: line\n        self.assert_equals \"invalid_value_container\", actual_violation.invalid_value_container, expected_violation.invalid_value_container, file: file, line: line\n        self.assert_equals \"plural\", actual_violation.plural, expected_violation.plural, file: file, line: line\n        self.assert_equals \"code\", actual_violation.code, expected_violation.code, file: file, line: line\n        self.assert_equals \"constraint\", actual_violation.constraint, expected_violation.constraint, file: file, line: line\n        self.assert_equals \"cause\", actual_violation.cause, expected_violation.cause, file: file, line: line\n      end\n    end\n\n    private def get_violation\n      AVD::Violation::ConstraintViolation.new(\n        @message,\n        @message,\n        @parameters,\n        @context.root_container,\n        @property_path,\n        @invalid_value,\n        @plural,\n        @code,\n        @constraint,\n        @cause\n      )\n    end\n\n    private def assert_equals(property : String, actual : _, expected : _, file : String, line : Int32) : Nil\n      actual.should eq(expected), failure_message: \"Expected #{property} to be: #{expected}, got: #{actual}\", file: file, line: line\n    end\n  end\n\n  # :nodoc:\n  class AssertingContextualValidator\n    include AVD::Validator::ContextualValidatorInterface\n\n    record Expectation,\n      value : String | Int32 | Nil,\n      groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil,\n      constraints : Proc(Array(AVD::Constraint) | AVD::Constraint | Nil, Nil),\n      violation : AVD::Violation::ConstraintViolationInterface? = nil\n\n    @context : AVD::ExecutionContextInterface\n\n    @expect_no_validate = false\n    @at_path_calls = -1\n    @expected_at_path = [] of String?\n    @validate_calls = -1\n    @expected_validate = [] of Expectation?\n\n    def initialize(@context : AVD::ExecutionContextInterface); end\n\n    def at_path(path : String) : AVD::Validator::ContextualValidatorInterface\n      @expect_no_validate.should be_false, failure_message: \"No validation calls have been expected.\"\n\n      unless expected_path = @expected_at_path[@at_path_calls += 1]?\n        fail \"Validation for property path '#{path}' was not expected.\"\n      end\n\n      @expected_at_path[@at_path_calls] = nil\n\n      path.should eq expected_path\n\n      self\n    end\n\n    def validate(value : _, constraints : Array(AVD::Constraint) | AVD::Constraint | Nil = nil, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n      @expect_no_validate.should be_false, failure_message: \"No validation calls have been expected.\"\n\n      unless expectation = @expected_validate[@validate_calls += 1]?\n        return self\n      end\n      @expected_validate[@validate_calls] = nil\n\n      value.should eq expectation.value\n      expectation.constraints.call constraints\n      expectation.groups.should eq groups\n\n      if v = expectation.violation\n        @context.add_violation v.message, v.parameters\n      end\n\n      self\n    end\n\n    def validate_property(object : AVD::Validatable, property_name : String, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n      self\n    end\n\n    def validate_property_value(object : AVD::Validatable, property_name : String, value : _, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n      self\n    end\n\n    def violations : AVD::Violation::ConstraintViolationListInterface\n      @context.violations\n    end\n\n    def expect_no_validate : Nil\n      @expect_no_validate = true\n    end\n\n    def expect_validation(\n      call : Int32,\n      property_path : String?,\n      value : _,\n      group : Array(String) | String | AVD::Constraints::GroupSequence | Nil,\n      violation : AVD::Violation::ConstraintViolationInterface? = nil,\n      &block : Array(AVD::Constraint) | AVD::Constraint | Nil -> Nil\n    )\n      if property_path\n        @expected_at_path.insert call, property_path\n      end\n\n      @expected_validate.insert call, Expectation.new value, group, block, violation\n    end\n  end\n\n  @group : String\n  @metadata : Nil = nil\n  @object : Nil = nil\n  @value : String | Array(String)\n  @root : String\n  @property_path : String\n  @constraint : AVD::Constraint\n  @context : AVD::ExecutionContext?\n  @validator : AVD::ConstraintValidatorInterface?\n  @expected_violations : Array(AVD::Violation::ConstraintViolationListInterface)\n  @call : Int32\n\n  protected def initialize\n    @group = \"my_group\"\n    @value = \"invalid_value\"\n    @root = \"root\"\n    @property_path = \"property.path\"\n\n    @constraint = AVD::Constraints::NotBlank.new\n    @expected_violations = Array(AVD::Violation::ConstraintViolationListInterface).new\n    @call = 0\n\n    ctx = self.create_context\n    validator = self.create_validator\n    validator.context = ctx\n\n    @context = ctx\n    @validator = validator\n  end\n\n  # Returns a new validator instance for the constraint being tested.\n  abstract def create_validator : AVD::ConstraintValidatorInterface\n\n  # Returns the class of the constraint being tested.\n  abstract def constraint_class : AVD::Constraint.class\n\n  # Returns a new constraint instance based on `#constraint_class` and the provided *args*.\n  def new_constraint(**args) : AVD::Constraint\n    self.constraint_class.new **args\n  end\n\n  # Asserts that no violations were added to the context.\n  def assert_no_violation(*, file : String = __FILE__, line : Int32 = __LINE__) : Nil\n    unless (violation_count = self.context.violations.size).zero?\n      fail \"0 violations expected but got #{violation_count}.\", file, line\n    end\n  end\n\n  # Asserts a violation with the provided *message* was added to the context.\n  def assert_violation(message : String) : Nil\n    self.build_violation(message).assert_violation\n  end\n\n  # Asserts a violation with the provided provided *message*, and *code* was added to the context.\n  def assert_violation(message : String, code : String) : Nil\n    self.build_violation(message, code).assert_violation\n  end\n\n  # Asserts a violation with the provided *message*, *code*, and *value* parameter was added to the context.\n  def assert_violation(message : String, code : String, value : _) : Nil\n    self.build_violation(message, code, value).assert_violation\n  end\n\n  # Returns an `AVD::Spec::ConstraintValidatorTestCase::Assertion` with the provided *message* preset.\n  def build_violation(message : String) : AVD::Spec::ConstraintValidatorTestCase::Assertion\n    Assertion.new self.context, message, @constraint\n  end\n\n  # Returns an `AVD::Spec::ConstraintValidatorTestCase::Assertion` with the provided *message*, and *code* preset.\n  def build_violation(message : String, code : String) : AVD::Spec::ConstraintValidatorTestCase::Assertion\n    self.build_violation(message).code(code)\n  end\n\n  # Returns an `AVD::Spec::ConstraintValidatorTestCase::Assertion` with the provided *message*, *code*, and *value* parameter preset.\n  def build_violation(message : String, code : String, value : _) : AVD::Spec::ConstraintValidatorTestCase::Assertion\n    self.build_violation(message).code(code).add_parameter(\"{{ value }}\", value)\n  end\n\n  # Asserts that a validation within a specific context occurs with the provided *property_path*, *value*, *constraints*, and optionally *groups*.\n  #\n  # See `CollectionValidatorTestCase` for an example.\n  def expect_validate_value_at(\n    idx : Int32,\n    property_path : String,\n    value : _,\n    constraints : Array(AVD::Constraint) | AVD::Constraint,\n    groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil,\n  )\n    raise \"BUG: Null context\" unless c = @context\n\n    contextual_validator = c.validator.in_context(c).as AssertingContextualValidator\n    contextual_validator.expect_validation idx, property_path, value, groups do |passed_constraints|\n      constraints.should eq passed_constraints\n    end\n  end\n\n  # Can be used to have a nested validator return the correct violations when used within another validator.\n  #\n  # Creates a separate validation context, validating the provided *value* against the provided *constraint*,\n  # causing the resulting violations to be returned from the inner validator as they would be in a non-test context.\n  #\n  # See `AVD::Constraints::ISIN::Validator`, and its related specs, for an example.\n  def expect_violation_at(idx : Int, value : _, constraint : AVD::Constraint) : AVD::Violation::ConstraintViolationListInterface\n    ctx = self.create_context\n\n    validator = constraint.validated_by.new\n    validator.context = ctx\n    validator.validate value, constraint\n\n    @expected_violations << ctx.violations\n\n    ctx.violations\n  end\n\n  # Overrides the value/node currently being validated.\n  def value=(value) : Nil\n    @value = value\n    self.context.set_node(@value, @object, @metadata, @property_path)\n  end\n\n  # Returns a reference to the context used for the current test.\n  def context : AVD::ExecutionContext\n    @context.not_nil!\n  end\n\n  # Returns the validator instance returned via `#create_validator`.\n  def validator : AVD::ConstraintValidatorInterface\n    @validator.not_nil!\n  end\n\n  private def create_context : AVD::ExecutionContext\n    validator = MockValidator.new do\n      (@expected_violations[@call]? || AVD::Violation::ConstraintViolationList.new).tap { @call += 1 }\n    end\n\n    ctx = AVD::ExecutionContext.new validator, @root\n    ctx.group = @group\n    ctx.set_node @value, @object, @metadata, @property_path\n    ctx.constraint = @constraint\n\n    contextual_validator = AssertingContextualValidator.new ctx\n    validator.contextual_validator = contextual_validator\n\n    ctx\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/spec/validator_test_case.cr",
    "content": "# :nodoc:\nabstract struct Athena::Validator::Spec::ValidatorTestCase < AVD::Spec::AbstractValidatorTestCase\n  getter! validator : AVD::Validator::ValidatorInterface\n\n  def initialize\n    super\n\n    @validator = self.create_validator @metadata_factory\n  end\n\n  abstract def create_validator(metadata_factory : AVD::Metadata::MetadataFactoryInterface) : AVD::Validator::ValidatorInterface\n\n  def validate(value, constraints = nil, groups = nil) : AVD::Violation::ConstraintViolationListInterface\n    self.validator.validate value, constraints, groups\n  end\n\n  def validate_property(object, property_name, groups = nil) : AVD::Violation::ConstraintViolationListInterface\n    self.validator.validate_property object, property_name, groups\n  end\n\n  def validate_property_value(object, property_name, value, groups = nil) : AVD::Violation::ConstraintViolationListInterface\n    self.validator.validate_property_value object, property_name, value, groups\n  end\n\n  def test_validate_constraint_without_group : Nil\n    self.validate(nil, AVD::Constraints::NotNil.new).size.should eq 1\n  end\n\n  def test_validate_empty_array_as_constraint : Nil\n    self.validate(nil, [] of AVD::Constraint).should be_empty\n  end\n\n  def test_validate_group_sequence_aborts_after_failed_group : Nil\n    object = Entity.new\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message1\"\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message2\"\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: AVD::Constraints::Callback::CallbackProc.new { }, groups: [\"group1\"]\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: [\"group2\"]\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: [\"group3\"]\n\n    violations = self.validate object, AVD::Constraints::Valid.new, AVD::Constraints::GroupSequence.new([\"group1\", \"group2\", \"group3\"])\n\n    violations.size.should eq 1\n    violations.first.message.should eq \"message1\"\n  end\n\n  def test_validate_group_sequence_includes_sub_objects : Nil\n    object = Entity.new\n    object.sub_object = SubEntity.new\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message1\"\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message2\"\n    end\n\n    @metadata.add_property_constraint \"sub_object\", AVD::Constraints::Valid.new\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: [\"group1\"]\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: [\"group2\"]\n\n    violations = self.validate object, AVD::Constraints::Valid.new, AVD::Constraints::GroupSequence.new([\"group1\", \"Athena::Validator::Spec::AbstractValidatorTestCase::Entity\"])\n\n    violations.size.should eq 1\n    violations.first.message.should eq \"message1\"\n  end\n\n  def test_validate_in_separate_context : Nil\n    object = Entity.new\n    object.sub_object = SubEntity.new\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |value, context|\n      value = value.get Entity\n\n      violations = context.validator.validate value.sub_object, AVD::Constraints::Valid.new, \"group\"\n\n      violations.size.should eq 1\n      violation = violations.first\n\n      violation.message.should eq \"message value\"\n      violation.message_template.should eq \"message {{ value }}\"\n      violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n      violation.property_path.should be_empty\n\n      violation.root.should eq value.sub_object\n      violation.invalid_value.should eq value.sub_object\n      violation.plural.should be_nil\n      violation.code.should be_nil\n\n      context.add_violation \"different violation\"\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      context.class_name.should eq SubEntity\n      context.property_name.should be_nil\n      context.property_path.should be_empty\n      context.group.should eq \"group\"\n      context.root.should eq object.sub_object\n      context.value.should eq object.sub_object\n      value.should eq object.sub_object\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: \"group\"\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: \"group\"\n\n    violations = self.validate object, AVD::Constraints::Valid.new, \"group\"\n\n    violations.size.should eq 1\n    violations.first.message.should eq \"different violation\"\n  end\n\n  def test_validate_in_context : Nil\n    object = Entity.new\n    object.sub_object = SubEntity.new\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |value, context|\n      previous_value = context.value\n      previous_object = context.object\n      previous_metadata = context.metadata\n      previous_path = context.property_path\n      previous_group = context.group\n\n      context\n        .validator\n        .in_context(context)\n        .at_path(\"subpath\")\n        .validate(value.get(Entity).sub_object)\n\n      # Context changes shouldn't leak from #validate.\n      previous_value.should eq context.value\n      previous_object.should eq context.object\n      previous_metadata.should eq context.metadata\n      previous_path.should eq context.property_path\n      previous_group.should eq context.group\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      context.class_name.should eq SubEntity\n      context.property_name.should be_nil\n      context.property_path.should eq \"subpath\"\n      context.group.should eq \"group\"\n      context.metadata.should eq @sub_object_metadata\n      context.root.should eq object\n      context.value.should eq object.sub_object\n      value.should eq object.sub_object\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: \"group\"\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: \"group\"\n\n    violations = self.validate object, AVD::Constraints::Valid.new, \"group\"\n\n    violations.size.should eq 1\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should eq \"subpath\"\n    violation.root.should eq object\n    violation.invalid_value.should eq object.sub_object\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_hash_in_context : Nil\n    object = Entity.new\n    object.sub_object = SubEntity.new\n\n    callback1 = AVD::Constraints::Callback::CallbackProc.new do |value, context|\n      previous_value = context.value\n      previous_object = context.object\n      previous_metadata = context.metadata\n      previous_path = context.property_path\n      previous_group = context.group\n\n      context\n        .validator\n        .in_context(context)\n        .at_path(\"subpath\")\n        .validate({\"key\" => value.get(Entity).sub_object})\n\n      # Context changes shouldn't leak from #validate.\n      previous_value.should eq context.value\n      previous_object.should eq context.object\n      previous_metadata.should eq context.metadata\n      previous_path.should eq context.property_path\n      previous_group.should eq context.group\n    end\n\n    callback2 = AVD::Constraints::Callback::CallbackProc.new do |value, context, _payload|\n      context.class_name.should eq SubEntity\n      context.property_name.should be_nil\n      context.property_path.should eq \"subpath[key]\"\n      context.group.should eq \"group\"\n      context.metadata.should eq @sub_object_metadata\n      context.root.should eq object\n      context.value.should eq object.sub_object\n      value.should eq object.sub_object\n\n      context.add_violation \"message {{ value }}\", {\"{{ value }}\" => \"value\"}\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback1, groups: \"group\"\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback2, groups: \"group\"\n\n    violations = self.validate object, AVD::Constraints::Valid.new, \"group\"\n\n    violations.size.should eq 1\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should eq \"subpath[key]\"\n    violation.root.should eq object\n    violation.invalid_value.should eq object.sub_object\n    violation.plural.should be_nil\n    violation.code.should be_nil\n  end\n\n  def test_validate_sub_object_with_cascade_disabled_by_default : Nil\n    object = Entity.new\n    object.sub_object = SubEntity.new\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, _context|\n      fail \"Callback should not have been invoked\"\n    end\n\n    @sub_object_metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: \"group\"\n\n    self.validate(object, AVD::Constraints::Valid.new, \"group\").should be_empty\n  end\n\n  def test_validate_customized_violation : Nil\n    object = Entity.new\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context\n        .build_violation(\"message {{ value }}\", \"CODE\", \"value\")\n        .plural(2)\n        .invalid_value(\"Invalid Value\")\n        .add\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback\n\n    violations = self.validate object\n\n    violations.size.should eq 1\n    violation = violations.first\n\n    violation.message.should eq \"message value\"\n    violation.message_template.should eq \"message {{ value }}\"\n    violation.parameters.should eq({\"{{ value }}\" => \"value\"})\n    violation.property_path.should be_empty\n    violation.root.should eq object\n    violation.invalid_value.should eq \"Invalid Value\"\n    violation.plural.should eq 2\n    violation.code.should eq \"CODE\"\n  end\n\n  def ptest_validate_no_duplicate_violations_if_class_constraint_is_in_multiple_groups : Nil\n    object = Entity.new\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message\"\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback, groups: [\"group1\", \"group2\"]\n\n    self.validate(object, AVD::Constraints::Valid.new, groups: [\"group1\", \"group2\"]).size.should eq 1\n  end\n\n  def ptest_validate_no_duplicate_violations_if_property_constraint_is_in_multiple_groups : Nil\n    object = Entity.new\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      context.add_violation \"message\"\n    end\n\n    @metadata.add_property_constraint \"first_name\", AVD::Constraints::Callback.new callback: callback, groups: [\"group1\", \"group2\"]\n\n    self.validate(object, AVD::Constraints::Valid.new, groups: [\"group1\", \"group2\"]).size.should eq 1\n  end\n\n  def test_validate_fails_non_object_array_and_no_constraints : Nil\n    expect_raises ArgumentError, \"Could not validate values of type 'String' automatically.  Please provide a constraint.\" do\n      self.validate \"Foo\"\n    end\n  end\n\n  def test_validate_access_current_object : Nil\n    called = false\n    object = Entity.new\n    object.first_name = \"Fred\"\n    object.data_hash = {\"first_name\" => \"Jon\"}\n\n    callback = AVD::Constraints::Callback::CallbackProc.new do |_value, context|\n      called = true\n      context.object.should eq object\n    end\n\n    @metadata.add_constraint AVD::Constraints::Callback.new callback: callback\n    @metadata.add_property_constraint \"first_name\", AVD::Constraints::Callback.new callback: callback\n    @metadata.add_property_constraints({\"data_hash\" => AVD::Constraints::EqualTo.new({\"first_name\" => \"Jon\"})})\n\n    self.validate object\n\n    called.should be_true\n  end\n\n  def test_validate_constraint_is_passed_to_violation : Nil\n    constraint = FailingConstraint.new\n\n    violations = self.validate \"foo\", constraint\n\n    violations.size.should eq 1\n    violations.first.constraint.should eq constraint\n  end\n\n  def test_validate_sub_object_is_not_validated_if_group_in_valid_constraint_is_not_validated : Nil\n    object = Entity.new\n    object.first_name = \"\"\n    sub_object = SubEntity.new\n    sub_object.value = \"\"\n    object.sub_object = sub_object\n\n    @metadata.add_property_constraint \"first_name\", AVD::Constraints::NotBlank.new groups: \"group1\"\n    @metadata.add_property_constraint \"sub_object\", AVD::Constraints::Valid.new groups: \"group1\"\n    @sub_object_metadata.add_property_constraint \"value\", AVD::Constraints::NotBlank.new\n\n    self.validate(object, nil, [] of String).should be_empty\n  end\n\n  def test_validate_sub_object_is_validated_if_group_in_valid_constraint_is_valided : Nil\n    object = Entity.new\n    object.first_name = \"\"\n    sub_object = SubEntity.new\n    sub_object.value = \"\"\n    object.sub_object = sub_object\n\n    @metadata.add_property_constraint \"first_name\", AVD::Constraints::NotBlank.new groups: \"group1\"\n    @metadata.add_property_constraint \"sub_object\", AVD::Constraints::Valid.new groups: \"group1\"\n    @sub_object_metadata.add_property_constraint \"value\", AVD::Constraints::NotBlank.new groups: \"group1\"\n\n    self.validate(object, nil, [\"default\", \"group1\"]).size.should eq 2\n  end\n\n  def test_validate_sub_object_is_valided_in_multiple_groups_if_group_in_valid_constraint_is_validated : Nil\n    object = Entity.new\n    object.first_name = nil\n\n    sub_object = SubEntity.new\n    sub_object.value = nil\n\n    object.sub_object = sub_object\n\n    @metadata.add_property_constraint \"first_name\", AVD::Constraints::NotBlank.new\n    @metadata.add_property_constraint \"sub_object\", AVD::Constraints::Valid.new groups: [\"group1\", \"group2\"]\n\n    @sub_object_metadata.add_property_constraint \"value\", AVD::Constraints::NotBlank.new groups: \"group1\"\n    @sub_object_metadata.add_property_constraint \"value\", AVD::Constraints::NotNil.new groups: \"group2\"\n\n    self.validate(object, nil, [\"default\", \"group1\", \"group2\"]).size.should eq 3\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/spec.cr",
    "content": "require \"athena-spec\"\n\nrequire \"./spec/abstract_validator_test_case\"\nrequire \"./spec/compound_constraint_test_case\"\nrequire \"./spec/constraint_validator_test_case\"\nrequire \"./spec/validator_test_case\"\n\n# A set of testing utilities/types to aid in testing `Athena::Validator` related types.\n#\n# ### Getting Started\n#\n# Require this module in your `spec_helper.cr` file.\n#\n# ```\n# # This also requires \"spec\" and \"athena-spec\".\n# require \"athena-validator/spec\"\n# ```\n#\n# Add `Athena::Spec` as a development dependency, then run a `shards install`.\n# See the individual types for more information.\nmodule Athena::Validator::Spec\n  # Extension of `AVD::Spec::ConstraintValidatorTestCase` used for testing `AVD::Constraints::AbstractComparison` based constraints.\n  #\n  # ### Example\n  #\n  # Using the spec from `AVD::Constraints::EqualTo`:\n  #\n  # ```\n  # # Makes for a bit less typing when needing to reference the constraint.\n  # private alias CONSTRAINT = AVD::Constraints::EqualTo\n  #\n  # # Define our test case inheriting from the abstract `ComparisonConstraintValidatorTestCase`.\n  # struct EqualToValidatorTest < AVD::Spec::ComparisonConstraintValidatorTestCase\n  #   # Returns a Tuple of Tuples representing valid comparisons.\n  #   # The first item  is the actual value and the second item is the expected value.\n  #   def valid_comparisons : Tuple\n  #     {\n  #       {3, 3},\n  #       {'a', 'a'},\n  #       {\"a\", \"a\"},\n  #       {Time.utc(2020, 4, 7), Time.utc(2020, 4, 7)},\n  #       {nil, false},\n  #     }\n  #   end\n  #\n  #   # Returns a Tuple of Tuples representing invalid comparisons.\n  #   # The first item  is the actual value and the second item is the expected value.\n  #   def invalid_comparisons : Tuple\n  #     {\n  #       {1, 3},\n  #       {'b', 'a'},\n  #       {\"b\", \"a\"},\n  #       {Time.utc(2020, 4, 8), Time.utc(2020, 4, 7)},\n  #     }\n  #   end\n  #\n  #   # The error code related to the current CONSTRAINT.\n  #   def error_code : String\n  #     CONSTRAINT::NOT_EQUAL_ERROR\n  #   end\n  #\n  #   # Implement some abstract defs to return the validator and constraint class.\n  #   def create_validator : AVD::ConstraintValidatorInterface\n  #     CONSTRAINT::Validator.new\n  #   end\n  #\n  #   def constraint_class : AVD::Constraint.class\n  #     CONSTRAINT\n  #   end\n  # end\n  # ```\n  abstract struct ComparisonConstraintValidatorTestCase < ConstraintValidatorTestCase\n    # A `Tuple` of tuples representing valid comparisons.\n    abstract def valid_comparisons : Tuple\n\n    # A `Tuple` of tuples representing invalid comparisons.\n    abstract def invalid_comparisons : Tuple\n\n    # The code for the current constraint.\n    abstract def error_code : String\n\n    @[DataProvider(\"valid_comparisons\")]\n    def test_valid_comparisons(actual, expected) : Nil\n      self.validator.validate actual, self.new_constraint value: expected\n      self.assert_no_violation\n    end\n\n    @[DataProvider(\"invalid_comparisons\")]\n    def test_invalid_comparisons(actual, expected : T) : Nil forall T\n      self.validator.validate actual, self.new_constraint value: expected, message: \"my_message\"\n\n      self\n        .build_violation(\"my_message\", self.error_code, actual)\n        .add_parameter(\"{{ compared_value }}\", expected)\n        .add_parameter(\"{{ compared_value_type }}\", T)\n        .assert_violation\n    end\n  end\n\n  # A spec implementation of `AVD::Validator::ContextualValidatorInterface`.\n  #\n  # Allows settings the violations that should be returned, defaulting to no violations.\n  class MockContextualValidator\n    include Athena::Validator::Validator::ContextualValidatorInterface\n\n    setter violations : AVD::Violation::ConstraintViolationListInterface\n\n    def initialize(@violations : AVD::Violation::ConstraintViolationListInterface = AVD::Violation::ConstraintViolationList.new); end\n\n    # :inherit:\n    def at_path(path : String) : AVD::Validator::ContextualValidatorInterface\n      self\n    end\n\n    # :inherit:\n    def validate(value : _, constraints : Array(AVD::Constraint) | AVD::Constraint | Nil = nil, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n      self\n    end\n\n    # :inherit:\n    def validate_property(object : AVD::Validatable, property_name : String, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n      self\n    end\n\n    # :inherit:\n    def validate_property_value(object : AVD::Validatable, property_name : String, value : _, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n      self\n    end\n\n    # :inherit:\n    def violations : AVD::Violation::ConstraintViolationListInterface\n      @violations\n    end\n  end\n\n  # A spec implementation of `AVD::Validator::ValidatorInterface`.\n  #\n  # Allows settings the violations that should be returned, defaulting to no violations.\n  # Also allows providing a block that is called for each validated value.\n  # E.g. to allow dynamically configuring the returned violations after it is instantiated.\n  class MockValidator\n    include Athena::Validator::Validator::ValidatorInterface\n\n    setter violations_callback : Proc(AVD::Violation::ConstraintViolationListInterface)\n    protected property! contextual_validator : AVD::Validator::ContextualValidatorInterface?\n\n    def self.new(violations : AVD::Violation::ConstraintViolationListInterface = AVD::Violation::ConstraintViolationList.new) : self\n      new &-> { violations.as AVD::Violation::ConstraintViolationListInterface }\n    end\n\n    def initialize(\n      &@violations_callback : -> AVD::Violation::ConstraintViolationListInterface\n    ); end\n\n    # :inherit:\n    def validate(value : _, constraints : Array(AVD::Constraint) | AVD::Constraint | Nil = nil, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface\n      @violations_callback.call\n    end\n\n    # :inherit:\n    def validate_property(object : AVD::Validatable, property_name : String, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface\n      @violations_callback.call\n    end\n\n    # :inherit:\n    def validate_property_value(object : AVD::Validatable, property_name : String, value : _, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface\n      @violations_callback.call\n    end\n\n    # :inherit:\n    def start_context(root = nil) : AVD::Validator::ContextualValidatorInterface\n      @contextual_validator || MockContextualValidator.new @violations_callback.call\n    end\n\n    # :inherit:\n    def in_context(context : AVD::ExecutionContextInterface) : AVD::Validator::ContextualValidatorInterface\n      @contextual_validator || MockContextualValidator.new @violations_callback.call\n    end\n  end\n\n  # A spec implementation of `AVD::Metadata::MetadataFactoryInterface`, supporting a fixed number of additional metadatas\n  struct MockMetadataFactory(T1, T2, T3, T4, T5)\n    include AVD::Metadata::MetadataFactoryInterface\n\n    @metadatas = Hash(AVD::Validatable::Class, AVD::Metadata::ClassMetadata(T1) |\n                                               AVD::Metadata::ClassMetadata(T2) |\n                                               AVD::Metadata::ClassMetadata(T3) |\n                                               AVD::Metadata::ClassMetadata(T4) |\n                                               AVD::Metadata::ClassMetadata(T5)).new\n\n    def metadata(object : AVD::Validatable) : AVD::Metadata::ClassMetadata\n      if metadata = @metadatas[object.class]?\n        return metadata\n      end\n\n      object.class.validation_class_metadata\n    end\n\n    def add_metadata(klass : AVD::Validatable::Class, metadata : AVD::Metadata::ClassMetadata) : Nil\n      @metadatas[klass] = metadata\n    end\n  end\n\n  # A constraint that always adds a violation.\n  class FailingConstraint < AVD::Constraint\n    def initialize(\n      message : String = \"Failed\",\n      groups : Array(String) | String | Nil = nil,\n      payload : Hash(String, String)? = nil,\n    )\n      super message, groups, payload\n    end\n\n    class Validator < AVD::ConstraintValidator\n      def validate(value : _, constraint : FailingConstraint) : Nil\n        self.context.add_violation constraint.message\n      end\n    end\n  end\n\n  # An `AVD::Validatable` entity using an `Array` based group sequence.\n  record EntitySequenceProvider, sequence : Array(String | Array(String)) do\n    include AVD::Validatable\n    include AVD::Constraints::GroupSequence::Provider\n\n    def group_sequence : Array(String | Array(String)) | AVD::Constraints::GroupSequence\n      @sequence || AVD::Constraints::GroupSequence.new [] of String\n    end\n  end\n\n  # An `AVD::Validatable` entity using an `AVD::Constraints::GroupSequence` based group sequence.\n  record EntityGroupSequenceProvider, sequence : AVD::Constraints::GroupSequence do\n    include AVD::Validatable\n    include AVD::Constraints::GroupSequence::Provider\n\n    def group_sequence : Array(String | Array(String)) | AVD::Constraints::GroupSequence\n      @sequence || Array(String | Array(String)).new\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/validatable.cr",
    "content": "# When included, denotes that a type (class or struct) can be validated via `Athena::Validator`.\n#\n# ### Example\n#\n# ```\n# class Example\n#   include AVD::Validatable\n#\n#   def initialize(@name : String); end\n#\n#   @[Assert::NotBlank]\n#   property name : String\n# end\n#\n# AVD.validator.validate Example.new(\"Jim\")\n# ```\nmodule Athena::Validator::Validatable\n  # :nodoc:\n  module Class; end\n\n  macro included\n    extend AVD::Validatable::Class\n\n    macro inherited\n      include AVD::Validatable\n    end\n\n    {% unless @type.abstract? %}\n      class_getter validation_class_metadata : AVD::Metadata::ClassMetadata(self) { AVD::Metadata::ClassMetadata(self).build }\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/validator/contextual_validator_interface.cr",
    "content": "# A validator that validates in a specific `AVD::ExecutionContextInterface` instance.\nmodule Athena::Validator::Validator::ContextualValidatorInterface\n  # Appends the provided *path* to the current `AVD::ExecutionContextInterface#property_path`.\n  abstract def at_path(path : String) : AVD::Validator::ContextualValidatorInterface\n\n  # Validates the provided *value*, optionally against the provided *constraints*, optionally using the provided *groups*.\n  # `AVD::Constraint::DEFAULT_GROUP` is assumed if no *groups* are provided.\n  abstract def validate(value : _, constraints : Array(AVD::Constraint) | AVD::Constraint | Nil = nil, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n\n  # Validates a property of the provided *object* against the constraints defined for that property, optionally using the provided *groups*.\n  # `AVD::Constraint::DEFAULT_GROUP` is assumed if no *groups* are provided.\n  abstract def validate_property(object : AVD::Validatable, property_name : String, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n\n  # Validates a value against the constraints defined on the property of the provided *object*.\n  # `AVD::Constraint::DEFAULT_GROUP` is assumed if no *groups* are provided.\n  abstract def validate_property_value(object : AVD::Validatable, property_name : String, value : _, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n\n  # Returns any violations that have been generated so far in the context of `self`.\n  abstract def violations : AVD::Violation::ConstraintViolationListInterface\nend\n"
  },
  {
    "path": "src/components/validator/src/validator/recursive_contextual_validator.cr",
    "content": "# A recursive implementation of `AVD::Validator::ContextualValidatorInterface`.\n#\n# See `Athena::Validator.validator`.\nclass Athena::Validator::Validator::RecursiveContextualValidator\n  private alias GroupsTypes = Array(String) | Array(String | AVD::Constraints::GroupSequence)\n\n  include Athena::Validator::Validator::ContextualValidatorInterface\n\n  @default_groups : Array(String)\n  @default_property_path : String\n\n  def initialize(\n    @context : AVD::ExecutionContextInterface,\n    @constraint_validator_factory : AVD::ConstraintValidatorFactoryInterface,\n    @metadata_factory : AVD::Metadata::MetadataFactoryInterface,\n  )\n    @default_groups = [(g = @context.group) ? g : Constraint::DEFAULT_GROUP]\n    @default_property_path = @context.property_path\n  end\n\n  # :inherit:\n  def at_path(path : String) : AVD::Validator::ContextualValidatorInterface\n    @default_property_path = @context.property_path path\n\n    self\n  end\n\n  # :inherit:\n  def validate(value : _, constraints : Array(AVD::Constraint) | AVD::Constraint | Nil = nil, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n    groups = self.normalize_groups groups\n\n    previous_value = @context.value\n    previous_object = @context.object\n    previous_metadata = @context.metadata\n    previous_path = @context.property_path\n    previous_group = @context.group\n    previous_constraint = @context.is_a?(AVD::ExecutionContext) ? @context.constraint : nil\n\n    # Validate the value against explicitly passed constraints\n    unless constraints.nil?\n      constraints = constraints.is_a?(Array) ? constraints : [constraints]\n\n      metadata = AVD::Metadata::Metadata.new\n      metadata.add_constraints constraints\n\n      self.validate_generic_node(\n        value,\n        previous_object,\n        metadata,\n        @default_property_path,\n        groups,\n        nil,\n        @context\n      )\n\n      @context.set_node previous_value, previous_object, previous_metadata, previous_path\n      @context.group = previous_group\n\n      unless previous_constraint.nil?\n        @context.constraint = previous_constraint\n      end\n\n      return self\n    end\n\n    case value\n    when AVD::Validatable\n      self.validate_object(\n        value,\n        @default_property_path,\n        groups,\n        @context\n      )\n\n      @context.set_node previous_value, previous_object, previous_metadata, previous_path\n      @context.group = previous_group\n\n      self\n    when Iterable, Hash\n      self.validate_each_object_in(\n        value,\n        @default_property_path,\n        groups,\n        @context\n      )\n\n      @context.set_node previous_value, previous_object, previous_metadata, previous_path\n      @context.group = previous_group\n\n      self\n    when Athena::HTTP::UploadedFile\n      # Won't result in violations, but is still supported explicitly.\n\n      self\n    else\n      raise AVD::Exception::InvalidArgument.new \"Could not validate values of type '#{value.class}' automatically.  Please provide a constraint.\"\n    end\n  end\n\n  # :inherit:\n  def validate_property(object : AVD::Validatable, property_name : String, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n    groups = self.normalize_groups groups\n\n    class_metadata = @metadata_factory.metadata object\n    property_metadatas = class_metadata.property_metadata property_name\n\n    property_path = AVD::PropertyPath.append @default_property_path, property_name\n\n    previous_value = @context.value\n    previous_object = @context.object\n    previous_metadata = @context.metadata\n    previous_path = @context.property_path\n    previous_group = @context.group\n\n    property_metadatas.each do |property_metadata|\n      property_value = property_metadata.value object\n\n      self.validate_generic_node(\n        property_value,\n        object,\n        property_metadata,\n        property_path,\n        groups,\n        nil,\n        @context\n      )\n    end\n\n    @context.set_node previous_value, previous_object, previous_metadata, previous_path\n    @context.group = previous_group\n\n    self\n  end\n\n  # :inherit:\n  def validate_property_value(object : AVD::Validatable, property_name : String, value : _, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Validator::ContextualValidatorInterface\n    groups = self.normalize_groups groups\n\n    class_metadata = @metadata_factory.metadata object\n    property_metadatas = class_metadata.property_metadata property_name\n\n    property_path = AVD::PropertyPath.append @default_property_path, property_name\n\n    previous_value = @context.value\n    previous_object = @context.object\n    previous_metadata = @context.metadata\n    previous_path = @context.property_path\n    previous_group = @context.group\n\n    property_metadatas.each do |property_metadata|\n      self.validate_generic_node(\n        value,\n        object,\n        property_metadata,\n        property_path,\n        groups,\n        nil,\n        @context\n      )\n    end\n\n    @context.set_node previous_value, previous_object, previous_metadata, previous_path\n    @context.group = previous_group\n\n    self\n  end\n\n  # :inherit:\n  def violations : AVD::Violation::ConstraintViolationListInterface\n    @context.violations\n  end\n\n  private def validate_each_object_in(\n    collection : Iterable,\n    property_path : String,\n    groups : GroupsTypes,\n    context : AVD::ExecutionContextInterface,\n  )\n    collection.each_with_index do |item, idx|\n      case item\n      when Iterable, Hash   then self.validate_each_object_in(item, \"#{property_path}[#{idx}]\", groups, context)\n      when AVD::Validatable then self.validate_object(item, \"#{property_path}[#{idx}]\", groups, context)\n      end\n    end\n  end\n\n  private def validate_each_object_in(\n    collection : Hash,\n    property_path : String,\n    groups : GroupsTypes,\n    context : AVD::ExecutionContextInterface,\n  )\n    collection.each do |key, value|\n      case value\n      when Iterable, Hash   then self.validate_each_object_in(value, \"#{property_path}[#{key}]\", groups, context)\n      when AVD::Validatable then self.validate_object(value, \"#{property_path}[#{key}]\", groups, context)\n      end\n    end\n  end\n\n  private def validate_generic_node(\n    value : _,\n    object : _,\n    metadata : AVD::Metadata::MetadataInterface?,\n    property_path : String,\n    groups : GroupsTypes,\n    cascaded_groups : Array(String)?,\n    context : AVD::ExecutionContextInterface,\n  )\n    context.set_node value, object, metadata, property_path\n\n    groups.each_with_index do |group, idx|\n      if group.is_a? AVD::Constraints::GroupSequence\n        self.step_through_group_sequence(\n          value,\n          object,\n          metadata,\n          property_path,\n          group,\n          nil,\n          context\n        )\n\n        groups.delete_at idx\n\n        next\n      end\n\n      self.validate_in_group value, metadata, group, context\n    end\n\n    return if groups.empty?\n    return if value.nil?\n\n    return unless metadata.cascading_strategy.cascade?\n\n    cascaded_groups = !cascaded_groups.nil? && cascaded_groups.size > 0 ? cascaded_groups : groups\n\n    case value\n    when Iterable\n      self.validate_each_object_in(\n        value,\n        property_path,\n        cascaded_groups,\n        context\n      )\n    when AVD::Validatable\n      self.validate_object(\n        value,\n        property_path,\n        cascaded_groups,\n        context\n      )\n    end\n  end\n\n  private def validate_object(object : AVD::Validatable, property_path : String, groups : GroupsTypes, context : AVD::ExecutionContextInterface) : Nil\n    self.validate_class_node(\n      object,\n      @metadata_factory.metadata(object),\n      property_path,\n      groups,\n      nil,\n      context\n    )\n  end\n\n  private def validate_class_node(\n    object : AVD::Validatable,\n    class_metadata : AVD::Metadata::ClassMetadata,\n    property_path : String,\n    groups : GroupsTypes,\n    cascaded_groups : Array(String)?,\n    context : AVD::ExecutionContextInterface,\n  ) : Nil\n    context.set_node object, object, class_metadata, property_path\n\n    groups.each_with_index do |group, idx|\n      # Handle cascading to the \"default\" group if a GroupSequence is used.\n      default_overridden = false\n\n      # Replace the \"default\" group by the group sequence if applicable.\n      if AVD::Constraint::DEFAULT_GROUP == group\n        if group_sequence = class_metadata.group_sequence\n          group = group_sequence\n          default_overridden = true\n        elsif object.is_a? AVD::Constraints::GroupSequence::Provider\n          group = object.group_sequence\n          default_overridden = true\n\n          unless group.is_a? AVD::Constraints::GroupSequence\n            group = AVD::Constraints::GroupSequence.new group\n          end\n        end\n      end\n\n      if group.is_a? AVD::Constraints::GroupSequence\n        self.step_through_group_sequence(\n          object,\n          object,\n          class_metadata,\n          property_path,\n          group,\n          default_overridden ? AVD::Constraint::DEFAULT_GROUP : nil,\n          context\n        )\n\n        groups.delete_at idx\n\n        next\n      end\n\n      # TODO: Can cache validated groups here if needed in the future\n      self.validate_in_group object, class_metadata, group, context\n    end\n\n    return if groups.empty?\n\n    class_metadata.constrained_properties.each do |property_name|\n      # A constraint can be applied to a property and getter of that property,\n      # thus resulting in two metadata objects being returned.\n      class_metadata.property_metadata(property_name).each do |property_metadata|\n        property_value = property_metadata.value object\n\n        self.validate_generic_node(\n          property_value,\n          object,\n          property_metadata,\n          AVD::PropertyPath.append(property_path, property_name),\n          groups,\n          cascaded_groups,\n          context\n        )\n      end\n    end\n\n    return unless object.is_a? Iterable\n\n    self.validate_each_object_in(\n      object,\n      property_path,\n      groups,\n      context\n    )\n  end\n\n  private def step_through_group_sequence(\n    value : _,\n    object : _,\n    metadata : AVD::Metadata::MetadataInterface?,\n    property_path : String,\n    group_sequence : AVD::Constraints::GroupSequence,\n    cascaded_groups : String?,\n    context : AVD::ExecutionContextInterface,\n  ) : Nil\n    violation_count = context.violations.size\n    cascaded_groups = cascaded_groups ? [cascaded_groups] : nil\n\n    group_sequence.groups.each do |group_in_sequence|\n      groups = group_in_sequence.is_a?(Array) ? group_in_sequence : [group_in_sequence]\n\n      if metadata.is_a? AVD::Metadata::ClassMetadata\n        self.validate_class_node(\n          value,\n          metadata,\n          property_path,\n          groups,\n          cascaded_groups,\n          context\n        )\n      else\n        self.validate_generic_node(\n          value,\n          object,\n          metadata,\n          property_path,\n          groups,\n          cascaded_groups,\n          context\n        )\n      end\n\n      # Don't validate future groups if a violation was generated\n      break if context.violations.size > violation_count\n    end\n  end\n\n  private def validate_in_group(value : _, metadata : AVD::Metadata::MetadataInterface, group : String, context : AVD::ExecutionContextInterface) : Nil\n    context.group = group\n\n    metadata.find_constraints(group).each do |constraint|\n      # TODO: Can cache validated groups here if needed in the future\n      context.constraint = constraint\n\n      validator = @constraint_validator_factory.validator constraint.validated_by\n      validator.context = context\n\n      validator.validate value, constraint\n    rescue ex : AVD::Exception::UnexpectedValueError\n      context.add_violation \"This value should be a valid: {{ type }}\", {\"{{ type }}\" => ex.supported_types}\n    end\n  end\n\n  private def normalize_groups(groups) : GroupsTypes\n    case groups\n    in Nil                                     then @default_groups\n    in String, AVD::Constraints::GroupSequence then [groups] of String | AVD::Constraints::GroupSequence\n    in Array                                   then groups\n    end\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/validator/recursive_validator.cr",
    "content": "require \"../constraint_validator_factory_interface\"\n\n# A recursive implementation of `AVD::Validator::ValidatorInterface`.\n#\n# See `Athena::Validator.validator`.\nclass Athena::Validator::Validator::RecursiveValidator\n  include Athena::Validator::Validator::ValidatorInterface\n\n  @validator_factory : AVD::ConstraintValidatorFactoryInterface\n  @metadata_factory : AVD::Metadata::MetadataFactoryInterface\n\n  def initialize(validator_factory : AVD::ConstraintValidatorFactoryInterface? = nil, metadata_factory : AVD::Metadata::MetadataFactoryInterface? = nil)\n    @validator_factory = validator_factory || AVD::ConstraintValidatorFactory.new\n    @metadata_factory = metadata_factory || AVD::Metadata::MetadataFactory.new\n  end\n\n  # :inherit:\n  def validate(value : _, constraints : Array(AVD::Constraint) | AVD::Constraint | Nil = nil, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface\n    start_context(value).validate(value, constraints, groups).violations\n  end\n\n  # :inherit:\n  def validate_property(object : AVD::Validatable, property_name : String, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface\n    start_context(object).validate_property(object, property_name, groups).violations\n  end\n\n  # :inherit:\n  def validate_property_value(object : AVD::Validatable, property_name : String, value : _, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface\n    start_context(object).validate_property_value(object, property_name, value, groups).violations\n  end\n\n  # :inherit:\n  def start_context(root = nil) : AVD::Validator::ContextualValidatorInterface\n    AVD::Validator::RecursiveContextualValidator.new create_context(root), @validator_factory, @metadata_factory\n  end\n\n  # :inherit:\n  def in_context(context : AVD::ExecutionContextInterface) : AVD::Validator::ContextualValidatorInterface\n    AVD::Validator::RecursiveContextualValidator.new context, @validator_factory, @metadata_factory\n  end\n\n  private def create_context(root = nil) : AVD::ExecutionContextInterface\n    AVD::ExecutionContext.new self, root\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/validator/validator_interface.cr",
    "content": "module Athena::Validator::Validator::ValidatorInterface\n  # Validates the provided *value*, optionally against the provided *constraints*, optionally using the provided *groups*.\n  # `AVD::Constraint::DEFAULT_GROUP` is assumed if no *groups* are provided.\n  abstract def validate(value : _, constraints : Array(AVD::Constraint) | AVD::Constraint | Nil = nil, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface\n\n  # Validates a property of the provided *object* against the constraints defined for that property, optionally using the provided *groups*.\n  # `AVD::Constraint::DEFAULT_GROUP` is assumed if no *groups* are provided.\n  abstract def validate_property(object : AVD::Validatable, property_name : String, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface\n\n  # Validates a value against the constraints defined on the property of the provided *object*.\n  # `AVD::Constraint::DEFAULT_GROUP` is assumed if no *groups* are provided.\n  abstract def validate_property_value(object : AVD::Validatable, property_name : String, value : _, groups : Array(String) | String | AVD::Constraints::GroupSequence | Nil = nil) : AVD::Violation::ConstraintViolationListInterface\n\n  # Creates a new `AVD::ExecutionContextInterface` and returns a new validator for that context.\n  #\n  # Violations generated by the returned validator can be accessed via `AVD::Validator::ContextualValidatorInterface#violations`.\n  abstract def start_context : AVD::Validator::ContextualValidatorInterface\n\n  # Returns a validator in the provided *context*.\n  #\n  # Violations generated by the returned validator are added to the provided *context*.\n  abstract def in_context(context : AVD::ExecutionContextInterface) : AVD::Validator::ContextualValidatorInterface\nend\n"
  },
  {
    "path": "src/components/validator/src/violation/constraint_violation.cr",
    "content": "require \"./constraint_violation_interface\"\n\n# Basic implementation of `AVD::Violation::ConstraintViolationInterface`.\nstruct Athena::Validator::Violation::ConstraintViolation\n  include Athena::Validator::Violation::ConstraintViolationInterface\n\n  protected getter invalid_value_container : AVD::Container\n  protected getter root_container : AVD::Container\n\n  # :inherit:\n  getter cause : String?\n\n  # :inherit:\n  getter code : String?\n\n  # :inherit:\n  getter! constraint : AVD::Constraint\n\n  # :inherit:\n  getter message : String\n\n  # :inherit:\n  getter message_template : String?\n\n  # :inherit:\n  getter parameters : Hash(String, String)\n\n  # :inherit:\n  getter plural : Int32?\n\n  # :inherit:\n  getter property_path : String\n\n  def initialize(\n    @message : String,\n    @message_template : String?,\n    @parameters : Hash(String, String),\n    root : _,\n    @property_path : String,\n    @invalid_value_container : AVD::Container,\n    @plural : Int32? = nil,\n    @code : String? = nil,\n    @constraint : AVD::Constraint? = nil,\n    @cause : String? = nil,\n  )\n    @root_container = root.is_a?(AVD::Container) ? root : AVD::ValueContainer.new(root)\n  end\n\n  # :inherit:\n  def invalid_value\n    @invalid_value_container.value\n  end\n\n  # :inherit:\n  def root\n    @root_container.value\n  end\n\n  # :inherit:\n  def to_json(builder : JSON::Builder) : Nil\n    builder.object do\n      builder.field \"property\", @property_path\n      builder.field \"message\", @message\n\n      if code = @code\n        builder.field \"code\", code\n      end\n    end\n  end\n\n  # :inherit:\n  def inspect(io : IO) : Nil\n    io << \"#<AVD::Violation property_path=\" << @property_path.inspect << \" message=\" << @message.inspect << \">\"\n  end\n\n  # :inherit:\n  def to_s(io : IO) : Nil\n    klass = case self.root\n            when Hash                         then \"Hash\"\n            when AVD::Validatable, Enumerable then \"Object(#{self.root.class})\"\n            else\n              self.root.to_s\n            end\n\n    klass += '.' if !@property_path.blank? && !@property_path.starts_with?('[') && !klass.blank?\n\n    if (c = code) && !c.blank?\n      code = \" (code: #{c})\"\n    end\n\n    io << klass\n    io << @property_path\n    io << ':' << '\\n' << '\\t'\n    io << @message\n    io << code\n    io << '\\n'\n  end\n\n  # Returns `true` if *other* is the same as `self`, otherwise `false`.\n  def ==(other : AVD::Violation::ConstraintViolationInterface) : Bool\n    @message == other.message &&\n      @message_template == other.message_template &&\n      @parameters == other.parameters &&\n      @root_container == other.root_container &&\n      @property_path == other.property_path &&\n      @invalid_value_container == other.invalid_value_container &&\n      @plural == other.plural &&\n      @code == other.code &&\n      @constraint == other.constraint? &&\n      @cause == other.cause\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/violation/constraint_violation_builder.cr",
    "content": "require \"./constraint_violation_builder_interface\"\n\n# Basic implementation of `AVD::Violation::ConstraintViolationBuilderInterface`.\nclass Athena::Validator::Violation::ConstraintViolationBuilder\n  include Athena::Validator::Violation::ConstraintViolationBuilderInterface\n\n  @plural : Int32?\n  @cause : String?\n\n  protected def initialize(\n    @violations : AVD::Violation::ConstraintViolationListInterface,\n    @constraint : AVD::Constraint?,\n    @message : String,\n    @parameters : Hash(String, String),\n    @root_container : AVD::Container,\n    @property_path : String,\n    @invalid_value : AVD::Container,\n  )\n  end\n\n  # :inherit:\n  def add : Nil\n    # Split and determine the message to use based on plural value\n    translated_message = if !(count = @plural).nil? && @message.includes? '|'\n                           parts = @message.split('|')\n                           # TODO: Support more robust translations\n                           count == 1 ? parts.first : parts[1]\n                         else\n                           @message\n                         end\n\n    rendered_message = translated_message.gsub(/(?:{{ \\w+ }})+/, @parameters)\n\n    @violations.add AVD::Violation::ConstraintViolation.new(\n      rendered_message,\n      @message,\n      @parameters,\n      @root_container,\n      @property_path,\n      @invalid_value,\n      @plural,\n      @code,\n      @constraint,\n      @cause\n    )\n  end\n\n  # :inherit:\n  def add_parameter(key : String, value : _) : AVD::Violation::ConstraintViolationBuilderInterface\n    @parameters[key] = value.to_s\n\n    self\n  end\n\n  # :inherit:\n  def at_path(path : String) : AVD::Violation::ConstraintViolationBuilderInterface\n    @property_path = AVD::PropertyPath.append @property_path, path\n\n    self\n  end\n\n  # :inherit:\n  def cause(@cause : String?) : AVD::Violation::ConstraintViolationBuilderInterface\n    self\n  end\n\n  # :inherit:\n  def code(@code : String?) : AVD::Violation::ConstraintViolationBuilderInterface\n    self\n  end\n\n  # :inherit:\n  def constraint(@constraint : AVD::Constraint?) : AVD::Violation::ConstraintViolationBuilderInterface\n    self\n  end\n\n  # :inherit:\n  def invalid_value(value : _) : AVD::Violation::ConstraintViolationBuilderInterface\n    @invalid_value = AVD::ValueContainer.new value\n\n    self\n  end\n\n  # :inherit:\n  def plural(number : Int32) : AVD::Violation::ConstraintViolationBuilderInterface\n    @plural = number\n    self\n  end\n\n  # :inherit:\n  def set_parameters(@parameters : Hash(String, String)) : AVD::Violation::ConstraintViolationBuilderInterface\n    self\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/violation/constraint_violation_builder_interface.cr",
    "content": "# A [Builder Pattern](https://en.wikipedia.org/wiki/Builder_pattern) type for building `AVD::Violation::ConstraintViolationInterface`s.\n#\n# Allows using the methods defined on `self` to construct the desired violation before adding it to the context.\nmodule Athena::Validator::Violation::ConstraintViolationBuilderInterface\n  # Adds the violation to the current `AVD::ExecutionContextInterface`.\n  abstract def add : Nil\n\n  # Adds a parameter with the provided *key* and *value* to the violations' `AVD::Violation::ConstraintViolationInterface#parameters`.\n  # The provided *value* is stringified via `#to_s` before being added to the parameters.\n  #\n  # Returns `self` for chaining.\n  abstract def add_parameter(key : String, value : _) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Sets the `AVD::Violation::ConstraintViolationInterface#property_path`.\n  #\n  # Returns `self` for chaining.\n  abstract def at_path(path : String) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Sets the `AVD::Violation::ConstraintViolationInterface#cause`\n  #\n  # Returns `self` for chaining.\n  abstract def cause(cause : String?) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Sets the `AVD::Violation::ConstraintViolationInterface#code`\n  #\n  # Returns `self` for chaining.\n  abstract def code(code : String?) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Sets the `AVD::Violation::ConstraintViolationInterface#constraint`\n  #\n  # Returns `self` for chaining.\n  abstract def constraint(constraint : AVD::Constraint?) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Sets the `AVD::Violation::ConstraintViolationInterface#invalid_value`\n  #\n  # Returns `self` for chaining.\n  abstract def invalid_value(value : _) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Sets `AVD::Violation::ConstraintViolationInterface#plural`\n  #\n  # Returns `self` for chaining.\n  abstract def plural(number : Int32) : AVD::Violation::ConstraintViolationBuilderInterface\n\n  # Overrides the entire `AVD::Violation::ConstraintViolationInterface#parameters` hash with the provided *parameters*.\n  #\n  # Returns `self` for chaining.\n  abstract def set_parameters(parameters : Hash(String, String)) : AVD::Violation::ConstraintViolationBuilderInterface\nend\n"
  },
  {
    "path": "src/components/validator/src/violation/constraint_violation_interface.cr",
    "content": "# Represents a violation of a constraint during validation.\n#\n# Each failed constraint that fails during validation; one or more violations are created.\n# The violations store the violation message, the path to the failing element,\n# and the root element originally passed to the validator.\nmodule Athena::Validator::Violation::ConstraintViolationInterface\n  # Returns the cause of the violation.\n  abstract def cause : String?\n\n  # Returns a unique machine readable error code representing `self.`\n  # All constraints of a specific \"type\" should have the same code.\n  abstract def code : String?\n\n  # Returns the `AVD::Constraint` whose validation caused the violation, if any.\n  abstract def constraint : AVD::Constraint?\n\n  # Returns the value that caused the violation.\n  abstract def invalid_value\n\n  # Returns the violation message.\n  abstract def message : String\n\n  # Returns the raw violation message.\n  #\n  # The message template contains placeholders for the parameters returned via `#parameters`.\n  abstract def message_template : String?\n\n  # Returns the parameters used to render the `#message_template`.\n  abstract def parameters : Hash(String, String)\n\n  # Returns a number used to pluralize the violation message.\n  #\n  # The returned value is used to determine the right plurlaization form.\n  abstract def plural : Int32?\n\n  # Returns the path from the root element to the violation.\n  abstract def property_path : String\n\n  # Returns the element originally passed to the validator.\n  abstract def root\n\n  # Returns a `JSON` representation of `self`.\n  abstract def to_json(builder : JSON::Builder) : Nil\n\n  # Returns a string representation of `self`.\n  abstract def to_s(io : IO) : Nil\nend\n"
  },
  {
    "path": "src/components/validator/src/violation/constraint_violation_list.cr",
    "content": "require \"./constraint_violation_list_interface\"\n\n# Basic implementation of `AVD::Violation::ConstraintViolationListInterface`.\nstruct Athena::Validator::Violation::ConstraintViolationList\n  include Athena::Validator::Violation::ConstraintViolationListInterface\n  include Indexable(Athena::Validator::Violation::ConstraintViolationInterface)\n\n  @violations : Array(AVD::Violation::ConstraintViolationInterface) = [] of AVD::Violation::ConstraintViolationInterface\n\n  def initialize(violations : Array(AVD::Violation::ConstraintViolationInterface) = [] of AVD::Violation::ConstraintViolationInterface)\n    violations.each do |violation|\n      add violation\n    end\n  end\n\n  # Returns a new `AVD::Violation::ConstraintViolationInterface` that consists only of violations with the provided *error_code*.\n  def find_by_code(error_code : String) : AVD::Violation::ConstraintViolationListInterface\n    self.class.new @violations.select &.code.==(error_code)\n  end\n\n  # :inherit:\n  def add(violation : AVD::Violation::ConstraintViolationInterface) : Nil\n    @violations << violation\n  end\n\n  # :inherit:\n  def add(violations : AVD::Violation::ConstraintViolationListInterface) : Nil\n    @violations.concat violations\n  end\n\n  # :inherit:\n  def has?(index : Int) : Bool\n    !@violations[index]?.nil?\n  end\n\n  # :inherit:\n  def remove(index : Int) : Nil\n    @violations.delete_at index\n  end\n\n  # :inherit:\n  def set(index : Int, violation : AVD::Violation::ConstraintViolationInterface) : Nil\n    @violations[index] = violation\n  end\n\n  # :inherit:\n  def size : Int\n    @violations.size\n  end\n\n  # :inherit:\n  def to_json(builder : JSON::Builder) : Nil\n    builder.array do\n      @violations.each do |violation|\n        violation.to_json builder\n      end\n    end\n  end\n\n  # :inherit:\n  def inspect(io : IO) : Nil\n    io << \"#<AVD::ViolationList size=\" << @violations.size << \">\"\n  end\n\n  # :inherit:\n  def to_s(io : IO) : Nil\n    @violations.each do |violation|\n      violation.to_s io\n    end\n  end\n\n  # :nodoc:\n  @[AlwaysInline]\n  def unsafe_fetch(index : Int) : AVD::Violation::ConstraintViolationInterface\n    @violations[index]\n  end\nend\n"
  },
  {
    "path": "src/components/validator/src/violation/constraint_violation_list_interface.cr",
    "content": "# A wrapper type around an `Array(AVD::ConstraintViolationInterface)`.\nmodule Athena::Validator::Violation::ConstraintViolationListInterface\n  # Adds the provided *violation* to `self`.\n  abstract def add(violation : AVD::Violation::ConstraintViolationInterface) : Nil\n\n  # Adds each of the provided *violations* to `self`.\n  abstract def add(violations : AVD::Violation::ConstraintViolationListInterface) : Nil\n\n  # Returns `true` if a violation exists at the provided *index*, otherwise `false`.\n  abstract def has?(index : Int) : Bool\n\n  # Sets the provided *violation* at the provided *index*.\n  abstract def set(index : Int, violation : AVD::Violation::ConstraintViolationInterface) : Nil\n\n  # Returns the number of violations in `self`.\n  abstract def size : Int\n\n  # Returns the violation at the provided *index*.\n  abstract def remove(index : Int) : Nil\n\n  # Returns a `JSON` representation of `self`.\n  abstract def to_json(builder : JSON::Builder) : Nil\n\n  # Returns a string representation of `self`.\n  abstract def to_s(io : IO) : Nil\nend\n"
  }
]