diff --git a/lib/protocol/http/media_types.rb b/lib/protocol/http/media_types.rb new file mode 100644 index 0000000..2809dd2 --- /dev/null +++ b/lib/protocol/http/media_types.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2016-2024, by Samuel Williams. + +module Protocol + module HTTP + class MediaTypes + WILDCARD = "*/*".freeze + + def initialize + @map = {} + end + + def freeze + return self if frozen? + + @map.freeze + @map.each_value(&:freeze) + + return super + end + + # Given a list of content types (e.g. from browser_preferred_content_types), return the best converter. Media types can be an array of MediaRange or String values. + def for(media_ranges) + media_ranges.each do |media_range| + range_string = media_range.range_string + + if object = @map[range_string] + return object + end + end + + return nil + end + + def []= media_range, object + @map[media_range] = object + end + + def [] media_range + @map[media_range] + end + + # Add a converter to the collection. A converter can be anything that responds to #content_type. Objects will be considered in the order they are added, subsequent objects cannot override previously defined media types. `object` must respond to #split('/', 2) which should give the type and subtype. + def << object + media_range = object.media_range + + # We set the default if not specified already: + @map[WILDCARD] = object if @map.empty? + + type = media_range.type + if type != "*" + @map["#{type}/*"] ||= object + + subtype = media_range.subtype + if subtype != "*" + @map["#{type}/#{subtype}"] ||= object + end + end + + return self + end + end + end +end diff --git a/test/protocol/http/media_types.rb b/test/protocol/http/media_types.rb new file mode 100644 index 0000000..4ed94b7 --- /dev/null +++ b/test/protocol/http/media_types.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2016-2024, by Samuel Williams. + +require "protocol/http/accept/media_types" +require "protocol/http/accept/media_types/map" +require "protocol/http/accept/content_type" + +describe Protocol::HTTP::MediaTypes do + let(:converter) do + Struct.new(:content_type) do + def split(*args) + self.content_type.split(*args) + end + end + end + + let(:text_html_converter) {converter.new("text/html")} + + let(:text_plain_content_type) {Protocol::HTTP::Accept::ContentType.new("text", "plain", charset: "utf-8")} + let(:text_plain_converter) {converter.new(text_plain_content_type)} + + let(:map) {subject.new} + + it "should give the correct converter when specified completely" do + map << text_html_converter + map << text_plain_converter + + media_types = Protocol::HTTP::Accept::MediaTypes.parse("text/plain, text/*, */*") + expect(map.for(media_types).first).to be == text_plain_converter + + media_types = Protocol::HTTP::Accept::MediaTypes.parse("text/html, text/*, */*") + expect(map.for(media_types).first).to be == text_html_converter + end + + it "should match the wildcard subtype converter" do + map << text_html_converter + map << text_plain_converter + + media_types = Protocol::HTTP::Accept::MediaTypes.parse("text/*, */*") + expect(map.for(media_types).first).to be == text_html_converter + + media_types = Protocol::HTTP::Accept::MediaTypes.parse("*/*") + expect(map.for(media_types).first).to be == text_html_converter + end + + it "should fail to match if no media types match" do + map << text_plain_converter + + expect(map.for(["application/json"])).to be_nil + end + + it "should fail to match if no media types specified" do + expect(map.for(["text/*", "*/*"])).to be_nil + end + + it "should freeze converters" do + map << text_html_converter + + map.freeze + + expect(text_html_converter).to be(:frozen?) + end + + it "should assign and retrive media ranges" do + map["*/*"] = :test + + expect(map["*/*"]).to be == :test + end +end