diff --git a/.gitignore b/.gitignore index 82f1ad5..0bbd4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/docs/ /lib/ /bin/ /.shards/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d54ee29..ff96272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,5 @@ # Changelog -## [0.1.5] - 2024-04-09 - -### Changed - -- Integrate website into monorepo ([#365](https://github.com/athena-framework/athena/pull/365)) (George Dietrich) - -## [0.1.4] - 2023-10-09 - -_Administrative release, no functional changes_ - -## [0.1.3] - 2023-02-18 - -### Changed - -- Update some links in preparation for Athena Framework `0.18.0` ([#261](https://github.com/athena-framework/athena/pull/261)) (George Dietrich) - ## [0.1.2] - 2022-05-14 _First release a part of the monorepo._ @@ -43,9 +27,6 @@ _First release a part of the monorepo._ _Initial release._ -[0.1.5]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.5 -[0.1.4]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.4 -[0.1.3]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.3 [0.1.2]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.2 [0.1.1]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.1 [0.1.0]: https://github.com/athena-framework/negotiation/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 9bbfb49..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contributing - -This 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. diff --git a/README.md b/README.md index 6a1e9e4..6342542 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,24 @@ Framework agnostic [content negotiation](https://tools.ietf.org/html/rfc7231#section-5.3) library based on [willdurand/Negotiation](https://github.com/willdurand/Negotiation). -## Getting Started +## Installation -Checkout the [Documentation](https://athenaframework.org/Negotiation). +1. Add the dependency to your `shard.yml`: + +```yaml +dependencies: + athena-negotiation: + github: athena-framework/negotiation + version: ~> 0.1.0 +``` + +2. Run `shards install` + +## Documentation + +If using the component on its own, checkout the [API documentation](https://athenaframework.org/Negotiation). +If using the component as part of Athena, also checkout the [external documentation](https://athenaframework.org/components/negotiation). ## Contributing -Read the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started. +[Report issues](https://github.com/athena-framework/athena/issues) and send [Pull Requests](https://github.com/athena-framework/athena/pulls) in the [main Athena repository](https://github.com/athena-framework/athena). diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 94b03ef..0000000 --- a/docs/README.md +++ /dev/null @@ -1,83 +0,0 @@ -The `Athena::Negotiation` component allows an application to support [content negotiation](https://tools.ietf.org/html/rfc7231#section-5.3). -The component has no dependencies and is framework agnostic; supporting various negotiators. - -## Installation - -First, install the component by adding the following to your `shard.yml`, then running `shards install`: - -```yaml -dependencies: - athena-negotiation: - github: athena-framework/negotiation - version: ~> 0.1.0 -``` - -## Usage - -The main type of [Athena::Negotiation][] is [ANG::AbstractNegotiator][] which is used to implement negotiators for each `Accept*` header. -`Athena::Negotiation` exposes class level getters for each negotiator; that return a lazily initialized singleton instance. -Each negotiator exposes two methods: [ANG::AbstractNegotiator#best][] and [ANG::AbstractNegotiator#ordered_elements][]. - -### Media Type - -```crystal -negotiator = ANG.negotiator - -accept_header = "text/html, application/xhtml+xml, application/xml;q=0.9" -priorities = ["text/html; charset=utf-8", "application/json", "application/xml;q=0.5"] - -accept = negotiator.best(accept_header, priorities).not_nil! - -accept.media_range # => "text/html" -accept.parameters # => {"charset" => "utf-8"} -``` - -The [ANG::Negotiator][] type returns an [ANG::Accept][], or `nil` if negotiating the best media type has failed. - -### Character Set - -```crystal -negotiator = ANG.charset_negotiator - -accept_header = "ISO-8859-1, utf-8; q=0.9" -priorities = ["iso-8859-1;q=0.3", "utf-8;q=0.9", "utf-16;q=1.0"] - -accept = negotiator.best(accept_header, priorities).not_nil! - -accept.charset # => "utf-8" -accept.quality # => 0.9 -``` - -The [ANG::CharsetNegotiator][] type returns an [ANG::AcceptCharset][], or `nil` if negotiating the best character set has failed. - -### Encoding - -```crystal -negotiator = ANG.encoding_negotiator - -accept_header = "gzip;q=1.0, identity; q=0.5, *;q=0" -priorities = ["gzip", "foo"] - -accept = negotiator.best(accept_header, priorities).not_nil! - -accept.coding # => "gzip" -``` - -The [ANG::EncodingNegotiator][] type returns an [ANG::AcceptEncoding][], or `nil` if negotiating the best encoding has failed. - -### Language - -```crystal -negotiator = ANG.language_negotiator - -accept_header = "en; q=0.1, fr; q=0.4, zh-Hans-CN; q=0.9, de; q=0.2" -priorities = ["de", "zh-Hans-CN", "en"] - -accept = negotiator.best(accept_header, priorities).not_nil! - -accept.language # => "zh" -accept.region # => "cn" -accept.script # => "hans" -``` - -The [ANG::LanguageNegotiator][] type returns an [ANG::AcceptLanguage][], or `nil` if negotiating the best language has failed. diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 08ced2d..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,31 +0,0 @@ -INHERIT: ../../../mkdocs-common.yml - -site_name: Negotiation -site_url: https://athenaframework.org/Negotiation/ -repo_url: https://github.com/athena-framework/negotiation - -nav: - - Introduction: README.md - - Back to Manual: project://. - - API: - - Aliases: aliases.md - - Top Level: top_level.md - - '*' - -plugins: - - search - - section-index - - literate-nav - - gen-files: - scripts: - - ../../../gen_doc_stubs.py - - mkdocstrings: - default_handler: crystal - custom_templates: ../../../docs/templates - handlers: - crystal: - crystal_docs_flags: - - ./docs/index.cr - - ./lib/athena-negotiation/src/athena-negotiation.cr - source_locations: - lib/athena-negotiation: https://github.com/athena-framework/negotiation/blob/v{shard_version}/{file}#L{line} diff --git a/shard.yml b/shard.yml index b15ac1f..3e40894 100644 --- a/shard.yml +++ b/shard.yml @@ -1,6 +1,6 @@ name: athena-negotiation -version: 0.1.5 +version: 0.1.2 crystal: ~> 1.4 @@ -14,4 +14,4 @@ description: | Framework agnostic content negotiation library. authors: - - George Dietrich + - George Dietrich diff --git a/spec/negotiator_spec.cr b/spec/negotiator_spec.cr index 353942d..e03e01d 100644 --- a/spec/negotiator_spec.cr +++ b/spec/negotiator_spec.cr @@ -71,9 +71,9 @@ struct NegotiatorTest < NegotiatorTestCase {"image/png, text/plain, audio/ogg", {"baz/asdf"}, nil}, {"image/png, text/plain, audio/ogg", {"audio/ogg"}, {"audio/ogg", nil}}, {"image/png, text/plain, audio/ogg", {"YO/SuP"}, nil}, - {"text/html; charset=utf-8, application/pdf", {"text/html; charset=utf-8"}, {"text/html", {"charset" => "utf-8"}}}, - {"text/html; charset=utf-8, application/pdf", {"text/html"}, nil}, - {"text/html, application/pdf", {"text/html; charset=utf-8"}, {"text/html", {"charset" => "utf-8"}}}, + {"text/html; charset=UTF-8, application/pdf", {"text/html; charset=UTF-8"}, {"text/html", {"charset" => "UTF-8"}}}, + {"text/html; charset=UTF-8, application/pdf", {"text/html"}, nil}, + {"text/html, application/pdf", {"text/html; charset=UTF-8"}, {"text/html", {"charset" => "UTF-8"}}}, # PHP"s PEAR HTTP2 assertions I took from the other lib. {php_pear_header, {"image/gif", "image/png", "application/xhtml+xml", "application/xml", "text/html", "image/jpeg", "text/plain"}, {"image/png", nil}}, @@ -86,7 +86,7 @@ struct NegotiatorTest < NegotiatorTestCase {php_pear_header, {"audio/midi"}, {"audio/midi", nil}}, {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", {"application/rss+xml"}, {"application/rss+xml", nil}}, - # Case sensitivity + # Case sensitiviy {"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"}}}, {"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"}}}, diff --git a/src/abstract_negotiator.cr b/src/abstract_negotiator.cr index 1bddf8c..0f67a51 100644 --- a/src/abstract_negotiator.cr +++ b/src/abstract_negotiator.cr @@ -26,9 +26,9 @@ abstract class Athena::Negotiation::AbstractNegotiator(HeaderType) raise ex if strict end - accepted_priorities = priorities.map { |p| HeaderType.new p } + accepted_priorties = priorities.map { |p| HeaderType.new p } - matches = self.find_matches accepted_headers, accepted_priorities + matches = self.find_matches accepted_headers, accepted_priorties specific_matches = matches.reduce({} of Int32 => ANG::AcceptMatch) do |acc, match| ANG::AcceptMatch.reduce acc, match @@ -38,7 +38,7 @@ abstract class Athena::Negotiation::AbstractNegotiator(HeaderType) match = specific_matches.shift? - match.nil? ? nil : accepted_priorities[match.index] + match.nil? ? nil : accepted_priorties[match.index] end # Returns an array of `HeaderType` that the provided *header* allows, ordered so that the `#best` match is first. diff --git a/src/accept.cr b/src/accept.cr index d69e91a..8a4b6df 100644 --- a/src/accept.cr +++ b/src/accept.cr @@ -3,11 +3,11 @@ require "./base_accept" # Represents an [Accept](https://tools.ietf.org/html/rfc7231#section-5.3.2) header media type. # # ``` -# accept = ANG::Accept.new "application/json; q = 0.75; charset = utf-8" +# accept = ANG::Accept.new "application/json; q = 0.75; charset = UTF-8" # -# accept.header # => "application/json; q = 0.75; charset = utf-8" -# accept.normalized_header # => "application/json; charset=utf-8" -# accept.parameters # => {"charset" => "utf-8"} +# accept.header # => "application/json; q = 0.75; charset = UTF-8" +# accept.normalized_header # => "application/json; charset=UTF-8" +# accept.parameters # => {"charset" => "UTF-8"} # accept.quality # => 0.75 # accept.type # => "application" # accept.sub_type # => "json" diff --git a/src/athena-negotiation.cr b/src/athena-negotiation.cr index 12ddf3a..22c8690 100644 --- a/src/athena-negotiation.cr +++ b/src/athena-negotiation.cr @@ -14,8 +14,95 @@ require "./exceptions/*" alias ANG = Athena::Negotiation # The `Athena::Negotiation` component allows an application to support [content negotiation](https://tools.ietf.org/html/rfc7231#section-5.3). +# The component has no dependencies and is framework agnostic; supporting various negotiators. +# +# ## Getting Started +# +# If using this component within the [Athena Framework][Athena::Framework], it is already installed and required for you. +# Checkout the [manual](/components/negotiation) for some additional information on how to use it within the framework. +# +# If using it outside of the framework, you will first need to add it as a dependency: +# +# ```yaml +# dependencies: +# athena-negotiation: +# github: athena-framework/negotiation +# version: ~> 0.1.0 +# ``` +# +# Then run `shards install`, being sure to require it via `require "athena-negotiation"`. +# +# ## Usage +# +# The main type of `Athena::Negotiation` is `ANG::AbstractNegotiator` which is used to implement negotiators for each `Accept*` header. +# `Athena::Negotiation` exposes class level getters for each negotiator; that return a lazily initialized singleton instance. +# Each negotiator exposes two methods: `ANG::AbstractNegotiator#best` and `ANG::AbstractNegotiator#ordered_elements`. +# +# ### Media Type +# +# ``` +# negotiator = ANG.negotiator +# +# accept_header = "text/html, application/xhtml+xml, application/xml;q=0.9" +# priorities = ["text/html; charset=UTF-8", "application/json", "application/xml;q=0.5"] +# +# accept = negotiator.best(accept_header, priorities).not_nil! +# +# accept.media_range # => "text/html" +# accept.parameters # => {"charset" => "UTF-8"} +# ``` +# +# The `ANG::Negotiator` type returns an `ANG::Accept`, or `nil` if negotiating the best media type has failed. +# +# ### Character Set +# +# ``` +# negotiator = ANG.charset_negotiator +# +# accept_header = "ISO-8859-1, UTF-8; q=0.9" +# priorities = ["iso-8859-1;q=0.3", "utf-8;q=0.9", "utf-16;q=1.0"] +# +# accept = negotiator.best(accept_header, priorities).not_nil! +# +# accept.charset # => "utf-8" +# accept.quality # => 0.9 +# ``` +# +# The `ANG::CharsetNegotiator` type returns an `ANG::AcceptCharset`, or `nil` if negotiating the best character set has failed. +# +# ### Encoding +# +# ``` +# negotiator = ANG.encoding_negotiator +# +# accept_header = "gzip;q=1.0, identity; q=0.5, *;q=0" +# priorities = ["gzip", "foo"] +# +# accept = negotiator.best(accept_header, priorities).not_nil! +# +# accept.coding # => "gzip" +# ``` +# +# The `ANG::EncodingNegotiator` type returns an `ANG::AcceptEncoding`, or `nil` if negotiating the best encoding has failed. +# +# ### Language +# +# ``` +# negotiator = ANG.language_negotiator +# +# accept_header = "en; q=0.1, fr; q=0.4, zh-Hans-CN; q=0.9, de; q=0.2" +# priorities = ["de", "zh-Hans-CN", "en"] +# +# accept = negotiator.best(accept_header, priorities).not_nil! +# +# accept.language # => "zh" +# accept.region # => "cn" +# accept.script # => "hans" +# ``` +# +# The `ANG::LanguageNegotiator` type returns an `ANG::AcceptLanguage`, or `nil` if negotiating the best language has failed. module Athena::Negotiation - VERSION = "0.1.5" + VERSION = "0.1.2" # Returns a lazily initialized `ANG::Negotiator` singleton instance. class_getter(negotiator) { ANG::Negotiator.new } diff --git a/src/base_accept.cr b/src/base_accept.cr index b9e02b8..f11d50e 100644 --- a/src/base_accept.cr +++ b/src/base_accept.cr @@ -10,7 +10,7 @@ abstract struct Athena::Negotiation::BaseAccept getter normalized_header : String # Returns any extension parameters included in the header `self` represents. - # E.x. `charset=utf-8` or `version=2`. + # E.x. `charset=UTF-8` or `version=2`. getter parameters : Hash(String, String) = Hash(String, String).new # Returns the [quality value](https://tools.ietf.org/html/rfc7231#section-5.3.1) of the header `self` represents. diff --git a/src/language_negotiator.cr b/src/language_negotiator.cr index e82d1c9..4d0304c 100644 --- a/src/language_negotiator.cr +++ b/src/language_negotiator.cr @@ -12,7 +12,7 @@ class Athena::Negotiation::LanguageNegotiator < Athena::Negotiation::AbstractNeg base_equal = accept_base.downcase == priority_base.downcase sub_equal = accept_sub.try &.downcase == priority_sub.try &.downcase - if (accept_base == "*" || base_equal) && (accept_sub.nil? || sub_equal) + if ((accept_base == "*" || base_equal) && (accept_sub.nil? || sub_equal)) score = 10 * (base_equal ? 1 : 0) + (sub_equal ? 1 : 0) return ANG::AcceptMatch.new accept.quality * priority.quality, score, index diff --git a/src/negotiator.cr b/src/negotiator.cr index 53cd001..18b3042 100644 --- a/src/negotiator.cr +++ b/src/negotiator.cr @@ -12,7 +12,7 @@ class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator( accept_sub_type = accept.sub_type priority_sub_type = priority.sub_type - intersection = accept.parameters.each_with_object({} of String => String) do |(k, v), params| + intercection = accept.parameters.each_with_object({} of String => String) do |(k, v), params| priority.parameters.tap do |pp| params[k] = v if pp.has_key?(k) && pp[k] == v end @@ -21,10 +21,12 @@ class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator( type_equals = accept_type.downcase == priority_type.downcase sub_type_equals = accept_sub_type.downcase == priority_sub_type.downcase - if (accept_type == "*" || type_equals) && - (accept_sub_type == "*" || sub_type_equals) && - intersection.size == accept.parameters.size - score = 100 * (type_equals ? 1 : 0) + 10 * (sub_type_equals ? 1 : 0) + intersection.size + if ( + (accept_type == "*" || type_equals) && + (accept_sub_type == "*" || sub_type_equals) && + intercection.size == accept.parameters.size + ) + score = 100 * (type_equals ? 1 : 0) + 10 * (sub_type_equals ? 1 : 0) + intercection.size return ANG::AcceptMatch.new accept.quality * priority.quality, score, index end @@ -34,18 +36,22 @@ class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator( accept_sub_type, accept_plus = self.split_sub_type accept_sub_type priority_sub_type, priority_plus = self.split_sub_type priority_sub_type - if !(accept_type == "*" || type_equals) || - !(accept_sub_type == "*" || priority_sub_type == "*" || accept_plus == "*" || priority_plus == "*") + if ( + !(accept_type == "*" || type_equals) || + !(accept_sub_type == "*" || priority_sub_type == "*" || accept_plus == "*" || priority_plus == "*") + ) return nil end sub_type_equals = accept_sub_type.downcase == priority_sub_type.downcase plus_equals = accept_plus.downcase == priority_plus.downcase - if (accept_sub_type == "*" || priority_sub_type == "*" || sub_type_equals) && - (accept_plus == "*" || priority_plus == '*' || plus_equals) && - intersection.size == accept.parameters.size - score = 100 * (type_equals ? 1 : 0) + 10 * (sub_type_equals ? 1 : 0) + (plus_equals ? 1 : 0) + intersection.size + if ( + (accept_sub_type == "*" || priority_sub_type == "*" || sub_type_equals) && + (accept_plus == "*" || priority_plus == '*' || plus_equals) && + intercection.size == accept.parameters.size + ) + score = 100 * (type_equals ? 1 : 0) + 10 * (sub_type_equals ? 1 : 0) + (plus_equals ? 1 : 0) + intercection.size return ANG::AcceptMatch.new accept.quality * priority.quality, score, index end