protocol-media 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2b4e5ba22b3b99614f42ca660be8745ea4d8b16a83dc8b97728461a5b4601d6
4
- data.tar.gz: d9eea7f70012140ce42dee2501b48281b17476a5207e017ecd3a44891fac74fa
3
+ metadata.gz: 41cc93afe36dc9af35b5ca77721b8859543b270627a3cb9f7f6cc2df2334d83e
4
+ data.tar.gz: 306bf200c34635b7eba643945121d6150054759ffa7ffa9ce5f08bf0668aa3b0
5
5
  SHA512:
6
- metadata.gz: bb8adf6b0a4aa0df12f48cac05123671dc00bbdb53ee46825cd8e26676020fb4685302c99e4c1967e68ca6b2ab36d706b7f0b884a3e48c6d80d124ed48e43810
7
- data.tar.gz: '0961e8287d2d249e37ffdf576fe17d27da6727070cacaa4694b8c84b6c0ef3f8b98a4f88b8cfca5f93758b8058b1522cc4cc728653d085985d5355fad9106df1'
6
+ metadata.gz: 7ab4060ead68e7b62f6486e298155e23060019bf7eb4bc9d2524efcb5cfe65e0a018945d46305730359b0c7e6bca158b44632ce5b7ca06ffd6c3c0bc2b8d327c
7
+ data.tar.gz: 84ccb4d5c42a6431cdc7da7d5ba6d1c776581bb561f0bde140d002c7cc5549f13304023c5d55bf4385c3e1de5a17be86c9d0aefcd2f708969aef3476772faa39
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "type"
7
+
8
+ module Protocol
9
+ module Media
10
+ # An immutable map from concrete media types to objects for range-based lookup.
11
+ class Map
12
+ # Incrementally constructs an immutable media map.
13
+ class Builder
14
+ # Initialize an empty media map builder.
15
+ def initialize
16
+ @index = {}
17
+ end
18
+
19
+ # Associate a concrete media type with an object.
20
+ #
21
+ # @parameter media_type [String | Object] The concrete media type or compatible object.
22
+ # @parameter object [Object] The object associated with the media type.
23
+ def []=(media_type, object)
24
+ media_type = Type.for(media_type)
25
+
26
+ # Preserve the first registration as the default for wildcard queries:
27
+ @index["*/*"] ||= object
28
+ @index["#{media_type.type}/*"] ||= object
29
+ @index[media_type.name] = object
30
+ end
31
+
32
+ # Compile the current registrations into an immutable map.
33
+ # @returns [Map] The immutable media map.
34
+ def build
35
+ return Map.new(@index).freeze
36
+ end
37
+ end
38
+
39
+ # Construct an immutable map using a builder.
40
+ # @yields {|builder| ...} The mutable builder.
41
+ # @returns [Map] The immutable media map.
42
+ def self.build
43
+ builder = Builder.new
44
+
45
+ if block_given?
46
+ yield builder
47
+ end
48
+
49
+ return builder.build
50
+ end
51
+
52
+ # Convert keyed entries into a media map.
53
+ #
54
+ # Existing maps are returned unchanged, including their frozen state.
55
+ #
56
+ # @parameter entries [Map | Enumerable] The existing map or keyed media range entries.
57
+ # @returns [Map] The existing map or a newly constructed immutable map.
58
+ def self.for(entries)
59
+ return entries if entries.instance_of?(self)
60
+
61
+ return build do |builder|
62
+ entries.each do |media_type, object|
63
+ builder[media_type] = object
64
+ end
65
+ end
66
+ end
67
+
68
+ # Initialize a new media map with a given index.
69
+ #
70
+ # The index is retained without copying or freezing it.
71
+ #
72
+ # @parameter index [Hash] The keyed media range entries.
73
+ def initialize(index)
74
+ @index = index
75
+ end
76
+
77
+ # Freeze the media map and its index.
78
+ # @returns [self]
79
+ def freeze
80
+ return self if frozen?
81
+
82
+ @index.freeze
83
+
84
+ return super
85
+ end
86
+
87
+ # Find the object matching a media type or range.
88
+ #
89
+ # @parameter media_range [String | Object] The media type or compatible range.
90
+ # @returns [Object | nil] The matching object, if one exists.
91
+ def [](media_range)
92
+ return lookup(Range.for(media_range))
93
+ end
94
+
95
+ # Find the first object matching an ordered sequence of media ranges.
96
+ #
97
+ # @parameter media_ranges [Enumerable] The media types or ranges in preference order.
98
+ # @returns [Array(Object, Range | String) | nil] The matching object and original range, if one exists.
99
+ def for(media_ranges)
100
+ media_ranges.each do |media_range|
101
+ if object = lookup(Range.for(media_range))
102
+ return [object, media_range]
103
+ end
104
+ end
105
+
106
+ return nil
107
+ end
108
+
109
+ private
110
+
111
+ def lookup(media_range)
112
+ media_range = Range.build(media_range.type, media_range.subtype)
113
+ return @index[media_range.name]
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "version"
7
+
8
+ require "strscan"
9
+
10
+ module Protocol
11
+ module Media
12
+ # A media range and its parameters.
13
+ class Range
14
+ TOKEN = /[!#$%&'*+\-.^_`|~0-9A-Z]+/i
15
+ QUOTED_STRING = /"(?:.(?!(?<!\\)"))*.?"/
16
+ MEDIA_TYPE = /(?<type>#{TOKEN})\/(?<subtype>#{TOKEN})/
17
+ PARAMETER = /\s*;\s*(?<key>#{TOKEN})=((?<value>#{TOKEN})|(?<quoted_value>#{QUOTED_STRING}))/
18
+
19
+ # Build a normalized media range.
20
+ #
21
+ # @parameter type [String] The top-level type.
22
+ # @parameter subtype [String] The subtype.
23
+ # @parameter parameters [Hash] The media range parameters.
24
+ # @returns [Range] The normalized media range.
25
+ def self.build(type, subtype = "*", parameters = {})
26
+ type = type.downcase
27
+ subtype = subtype.downcase
28
+
29
+ # A wildcard type requires a wildcard subtype:
30
+ if type == "*"
31
+ if subtype != "*"
32
+ raise ArgumentError, "Invalid wildcards in media range: #{type}/#{subtype}"
33
+ end
34
+ end
35
+
36
+ return new(type, subtype, parameters)
37
+ end
38
+
39
+ # Parse strings into media ranges while preserving compatible objects.
40
+ #
41
+ # @parameter value [String | Object] A media range string or compatible object.
42
+ # @returns [Range | Object] The parsed range or original object.
43
+ def self.for(value)
44
+ if value.is_a?(String)
45
+ parse(value)
46
+ else
47
+ value
48
+ end
49
+ end
50
+
51
+ # Parse a media range.
52
+ #
53
+ # @parameter text [String] The media range, including any parameters.
54
+ # @parameter normalize_whitespace [Boolean] Whether to normalize whitespace in quoted parameter values.
55
+ # @returns [Range] The parsed media range.
56
+ def self.parse(text, normalize_whitespace = true)
57
+ scanner = StringScanner.new(text)
58
+
59
+ unless scanner.scan(MEDIA_TYPE)
60
+ raise ArgumentError, "Invalid media range: #{text.inspect}"
61
+ end
62
+
63
+ type = scanner[:type]
64
+ subtype = scanner[:subtype]
65
+ parameters = parse_parameters(scanner, normalize_whitespace)
66
+
67
+ unless scanner.eos?
68
+ raise ArgumentError, "Invalid media range: #{text.inspect}"
69
+ end
70
+
71
+ return build(type, subtype, parameters)
72
+ end
73
+
74
+ # Parse media type parameters from the scanner.
75
+ #
76
+ # @parameter scanner [StringScanner] The scanner positioned after the type and subtype.
77
+ # @parameter normalize_whitespace [Boolean] Whether to normalize whitespace in quoted values.
78
+ # @returns [Hash] The parsed parameters.
79
+ def self.parse_parameters(scanner, normalize_whitespace = true)
80
+ parameters = {}
81
+
82
+ while scanner.scan(PARAMETER)
83
+ key = scanner[:key]
84
+
85
+ if value = scanner[:value]
86
+ parameters[key] = value
87
+ elsif quoted_value = scanner[:quoted_value]
88
+ parameters[key] = unquote(quoted_value, normalize_whitespace)
89
+ end
90
+ end
91
+
92
+ parameters
93
+ end
94
+
95
+ # @attribute [String] The top-level type, e.g. `text` or `*`.
96
+ attr_reader :type
97
+
98
+ # @attribute [String] The subtype, e.g. `plain` or `*`.
99
+ attr_reader :subtype
100
+
101
+ # @attribute [Hash] The media range parameters.
102
+ attr_reader :parameters
103
+
104
+ # @parameter type [String] The top-level type.
105
+ # @parameter subtype [String] The subtype.
106
+ # @parameter parameters [Hash] The media range parameters.
107
+ def initialize(type, subtype = "*", parameters = {})
108
+ @type = type
109
+ @subtype = subtype
110
+ @parameters = parameters
111
+ end
112
+
113
+ # The complete media range name.
114
+ #
115
+ # @returns [String]
116
+ def name
117
+ "#{@type}/#{@subtype}"
118
+ end
119
+
120
+ alias mime_type name
121
+
122
+ # Convert the media range and parameters to a string.
123
+ #
124
+ # @returns [String] The serialized media range.
125
+ def to_s
126
+ name + @parameters.collect do |key, value|
127
+ "; #{key}=#{quote(value.to_s)}"
128
+ end.join
129
+ end
130
+
131
+ alias to_str to_s
132
+
133
+ # Freeze the media range and its direct values.
134
+ # @returns [self]
135
+ def freeze
136
+ return self if frozen?
137
+
138
+ @type.freeze
139
+ @subtype.freeze
140
+ @parameters.freeze
141
+
142
+ return super
143
+ end
144
+
145
+ # Whether this range matches the given media type or range.
146
+ #
147
+ # @parameter media_range [Range] The media type or range to match.
148
+ # @returns [Boolean] Whether the type and subtype are compatible.
149
+ def match?(media_range)
150
+ # Both type components must be compatible:
151
+ unless component_match?(@type, media_range.type)
152
+ return false
153
+ end
154
+
155
+ # Both subtype components must be compatible:
156
+ unless component_match?(@subtype, media_range.subtype)
157
+ return false
158
+ end
159
+
160
+ return true
161
+ end
162
+
163
+ alias === match?
164
+
165
+ # Compare this media range with another media range.
166
+ #
167
+ # @parameter other [Object] The object to compare.
168
+ # @returns [Boolean] Whether the values are equal.
169
+ def eql?(other)
170
+ other.instance_of?(self.class) && @type.eql?(other.type) && @subtype.eql?(other.subtype) && @parameters.eql?(other.parameters)
171
+ end
172
+
173
+ alias == eql?
174
+
175
+ # Generate a hash key consistent with {#eql?}.
176
+ #
177
+ # @returns [Integer] The hash value.
178
+ def hash
179
+ [self.class, @type, @subtype, @parameters].hash
180
+ end
181
+
182
+ private
183
+
184
+ def component_match?(component, other_component)
185
+ # A wildcard on either side matches any component:
186
+ if component == "*"
187
+ return true
188
+ end
189
+
190
+ if other_component == "*"
191
+ return true
192
+ end
193
+
194
+ return component == other_component
195
+ end
196
+
197
+ def self.unquote(value, normalize_whitespace)
198
+ value = value[1...-1]
199
+ value.gsub!(/\\(.)/, '\\1')
200
+ value.gsub!(/[\r\n]+\s+/, " ") if normalize_whitespace
201
+ value
202
+ end
203
+
204
+ def quote(value)
205
+ if value.match?(/\A#{TOKEN}\z/)
206
+ value
207
+ else
208
+ "\"#{value.gsub(/["\\]/, "\\\\\\0")}\""
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "range"
7
+
8
+ require "set"
9
+
10
+ module Protocol
11
+ module Media
12
+ # An immutable set of compatible media ranges.
13
+ class Set
14
+ include Enumerable
15
+
16
+ # An immutable set which matches any media range.
17
+ class Any
18
+ include Enumerable
19
+
20
+ RANGE = Range.new("*", "*").freeze
21
+ private_constant :RANGE
22
+
23
+ # Whether the set contains a compatible media type or range.
24
+ # @parameter media_range [String | Object] The media type or range to validate.
25
+ # @returns [Boolean]
26
+ def include?(media_range)
27
+ media_range = Range.for(media_range)
28
+
29
+ Range.build(media_range.type, media_range.subtype)
30
+
31
+ return true
32
+ end
33
+
34
+ alias === include?
35
+ alias match? include?
36
+
37
+ # Enumerate the universal media range.
38
+ # @yields {|media_range| ...} The universal media range.
39
+ # @returns [Enumerator | self]
40
+ def each
41
+ return to_enum unless block_given?
42
+
43
+ yield RANGE
44
+
45
+ return self
46
+ end
47
+
48
+ # The number of membership ranges.
49
+ # @returns [Integer]
50
+ def size
51
+ return 1
52
+ end
53
+
54
+ # Whether there are no membership ranges.
55
+ # @returns [Boolean]
56
+ def empty?
57
+ return false
58
+ end
59
+ end
60
+
61
+ ANY = Any.new.freeze
62
+
63
+ private_constant :Any, :ANY
64
+
65
+ # Incrementally constructs an immutable media set.
66
+ class Builder
67
+ # Initialize an empty media set builder.
68
+ def initialize
69
+ @types = {}
70
+ end
71
+
72
+ # Add a media range to the set under construction.
73
+ # @parameter media_range [String | Object] The media range or compatible object.
74
+ # @returns [self]
75
+ def add(media_range)
76
+ if @types.instance_of?(Any)
77
+ return self
78
+ end
79
+
80
+ media_range = Range.for(media_range)
81
+ media_range = Range.build(media_range.type, media_range.subtype)
82
+ type = media_range.type.freeze
83
+ subtype = media_range.subtype.freeze
84
+
85
+ if type == "*"
86
+ @types = ANY
87
+ elsif subtype == "*"
88
+ @types[type] = ANY
89
+ elsif subtypes = @types[type]
90
+ unless subtypes.instance_of?(Any)
91
+ subtypes.add(subtype)
92
+ end
93
+ else
94
+ @types[type] = ::Set.new([subtype])
95
+ end
96
+
97
+ return self
98
+ end
99
+
100
+ alias << add
101
+
102
+ # Compile the current ranges into an immutable set.
103
+ # @returns [Set] The immutable media set.
104
+ def build
105
+ if @types.instance_of?(Any)
106
+ return @types
107
+ end
108
+
109
+ return Set.new(@types).freeze
110
+ end
111
+ end
112
+
113
+ # Construct an immutable set using a builder.
114
+ # @yields {|builder| ...} The mutable builder.
115
+ # @returns [Set] The immutable media set.
116
+ def self.build
117
+ builder = Builder.new
118
+
119
+ if block_given?
120
+ yield builder
121
+ end
122
+
123
+ return builder.build
124
+ end
125
+
126
+ # Convert a sequence of ranges into a media set.
127
+ #
128
+ # Existing sets are returned unchanged, including their frozen state.
129
+ #
130
+ # @parameter media_ranges [Set | Enumerable] The existing set or media ranges.
131
+ # @returns [Set] The existing set or a newly constructed immutable set.
132
+ def self.for(media_ranges)
133
+ # Preserve an existing media set:
134
+ if media_ranges.instance_of?(self)
135
+ return media_ranges
136
+ end
137
+
138
+ # Preserve the universal media set:
139
+ if media_ranges.instance_of?(Any)
140
+ return media_ranges
141
+ end
142
+
143
+ return build do |builder|
144
+ media_ranges.each do |media_range|
145
+ builder << media_range
146
+ end
147
+ end
148
+ end
149
+
150
+ # Initialize a new media set with the given type index.
151
+ #
152
+ # The index is retained without copying or freezing it.
153
+ #
154
+ # @parameter types [Hash] The indexed media ranges.
155
+ def initialize(types)
156
+ @types = types
157
+ end
158
+
159
+ # Freeze the media set and its index.
160
+ # @returns [self]
161
+ def freeze
162
+ return self if frozen?
163
+
164
+ @types.each_value(&:freeze)
165
+ @types.freeze
166
+
167
+ return super
168
+ end
169
+
170
+ # Whether the set contains a compatible media type or range.
171
+ # @parameter media_range [String | Object] The media type or range to match.
172
+ # @returns [Boolean]
173
+ def include?(media_range)
174
+ media_range = normalize(media_range)
175
+ type = media_range.type
176
+ subtype = media_range.subtype
177
+
178
+ # A wildcard type matches any non-empty set:
179
+ if type == "*"
180
+ return !@types.empty?
181
+ end
182
+
183
+ subtypes = @types[type]
184
+
185
+ # An unknown type cannot match:
186
+ unless subtypes
187
+ return false
188
+ end
189
+
190
+ # A wildcard subtype matches any known type:
191
+ if subtype == "*"
192
+ return true
193
+ end
194
+
195
+ # A type-wide wildcard matches every subtype:
196
+ if subtypes.instance_of?(Any)
197
+ return true
198
+ end
199
+
200
+ return subtypes.include?(subtype)
201
+ end
202
+
203
+ alias === include?
204
+ alias match? include?
205
+
206
+ # Enumerate the canonical media ranges which define membership.
207
+ # @yields {|media_range| ...} Each canonical media range.
208
+ # @returns [Enumerator | self]
209
+ def each
210
+ return to_enum unless block_given?
211
+
212
+ @types.each do |type, subtypes|
213
+ if subtypes.instance_of?(Any)
214
+ yield Range.new(type, "*")
215
+ else
216
+ subtypes.each do |subtype|
217
+ yield Range.new(type, subtype)
218
+ end
219
+ end
220
+ end
221
+
222
+ return self
223
+ end
224
+
225
+ # The number of canonical membership ranges.
226
+ # @returns [Integer]
227
+ def size
228
+ return @types.sum do |_type, subtypes|
229
+ subtypes.size
230
+ end
231
+ end
232
+
233
+ # Whether the set contains no media ranges.
234
+ # @returns [Boolean]
235
+ def empty?
236
+ return @types.empty?
237
+ end
238
+
239
+ private
240
+
241
+ def normalize(media_range)
242
+ media_range = Range.for(media_range)
243
+ return Range.build(media_range.type, media_range.subtype)
244
+ end
245
+ end
246
+ end
247
+ end
@@ -3,130 +3,42 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2026, by Samuel Williams.
5
5
 
6
- require_relative "version"
7
-
8
- require "strscan"
6
+ require_relative "range"
9
7
 
10
8
  module Protocol
11
9
  module Media
12
10
  # A concrete media type and its parameters.
13
- class Type
14
- TOKEN = /[!#$%&'*+\-.^_`|~0-9A-Z]+/i
15
- QUOTED_STRING = /"(?:.(?!(?<!\\)"))*.?"/
16
- MEDIA_TYPE = /(?<type>#{TOKEN})\/(?<subtype>#{TOKEN})/
17
- PARAMETER = /\s*;\s*(?<key>#{TOKEN})=((?<value>#{TOKEN})|(?<quoted_value>#{QUOTED_STRING}))/
18
-
19
- # Parse a concrete media type.
11
+ class Type < Range
12
+ # Parse strings and normalize compatible media type objects.
20
13
  #
21
- # @parameter text [String] The media type, including any parameters.
22
- # @parameter normalize_whitespace [Boolean] Whether to normalize whitespace in quoted parameter values.
23
- # @returns [Type] The parsed media type.
24
- def self.parse(text, normalize_whitespace = true)
25
- scanner = StringScanner.new(text)
26
-
27
- unless scanner.scan(MEDIA_TYPE)
28
- raise ArgumentError, "Invalid media type: #{text.inspect}"
14
+ # @parameter value [String | Object] A media type string or compatible object.
15
+ # @returns [Type] The normalized concrete media type.
16
+ def self.for(value)
17
+ if value.is_a?(String)
18
+ return parse(value)
29
19
  end
30
20
 
31
- type = scanner[:type]
32
- subtype = scanner[:subtype]
33
- parameters = parse_parameters(scanner, normalize_whitespace)
34
-
35
- unless scanner.eos?
36
- raise ArgumentError, "Invalid media type: #{text.inspect}"
37
- end
38
-
39
- new(type, subtype, parameters)
21
+ return build(value.type, value.subtype, value.parameters)
40
22
  end
41
23
 
42
- # Parse media type parameters from the scanner.
24
+ # Build a normalized concrete media type.
43
25
  #
44
- # @parameter scanner [StringScanner] The scanner positioned after the type and subtype.
45
- # @parameter normalize_whitespace [Boolean] Whether to normalize whitespace in quoted values.
46
- # @returns [Hash] The parsed parameters.
47
- def self.parse_parameters(scanner, normalize_whitespace = true)
48
- parameters = {}
49
-
50
- while scanner.scan(PARAMETER)
51
- key = scanner[:key]
52
-
53
- if value = scanner[:value]
54
- parameters[key] = value
55
- elsif quoted_value = scanner[:quoted_value]
56
- parameters[key] = unquote(quoted_value, normalize_whitespace)
57
- end
58
- end
59
-
60
- parameters
61
- end
62
-
63
- # @attribute [String] The top-level type, e.g. `text`.
64
- attr_reader :type
65
-
66
- # @attribute [String] The subtype, e.g. `plain`.
67
- attr_reader :subtype
68
-
69
- # @attribute [Hash] The media type parameters.
70
- attr_reader :parameters
71
-
72
26
  # @parameter type [String] The top-level type.
73
27
  # @parameter subtype [String] The subtype.
74
28
  # @parameter parameters [Hash] The media type parameters.
75
- def initialize(type, subtype, parameters = {})
76
- if type.include?("*") || subtype.include?("*")
29
+ # @returns [Type] The normalized concrete media type.
30
+ def self.build(type, subtype, parameters = {})
31
+ # Concrete media types cannot use a wildcard type:
32
+ if type == "*"
77
33
  raise ArgumentError, "Media types cannot contain wildcards: #{type}/#{subtype}"
78
34
  end
79
35
 
80
- @type = type.downcase
81
- @subtype = subtype.downcase
82
- @parameters = parameters
83
- end
84
-
85
- # The complete media type name.
86
- #
87
- # @returns [String]
88
- def name
89
- "#{@type}/#{@subtype}"
90
- end
91
-
92
- alias mime_type name
93
-
94
- # Convert the media type and parameters to a string.
95
- def to_s
96
- name + @parameters.collect do |key, value|
97
- "; #{key}=#{quote(value.to_s)}"
98
- end.join
99
- end
100
-
101
- alias to_str to_s
102
-
103
- # Compare this media type with another media type.
104
- def ==(other)
105
- other.is_a?(Type) && @type == other.type && @subtype == other.subtype && @parameters == other.parameters
106
- end
107
-
108
- alias eql? ==
109
-
110
- # Generate a hash key consistent with {#eql?}.
111
- def hash
112
- [@type, @subtype, @parameters].hash
113
- end
114
-
115
- private
116
-
117
- def self.unquote(value, normalize_whitespace)
118
- value = value[1...-1]
119
- value.gsub!(/\\(.)/, '\\1')
120
- value.gsub!(/[\r\n]+\s+/, " ") if normalize_whitespace
121
- value
122
- end
123
-
124
- def quote(value)
125
- if value.match?(/\A#{TOKEN}\z/)
126
- value
127
- else
128
- "\"#{value.gsub(/["\\]/, "\\\\\\0")}\""
36
+ # Concrete media types cannot use a wildcard subtype:
37
+ if subtype == "*"
38
+ raise ArgumentError, "Media types cannot contain wildcards: #{type}/#{subtype}"
129
39
  end
40
+
41
+ return super
130
42
  end
131
43
  end
132
44
  end
@@ -7,6 +7,6 @@
7
7
  module Protocol
8
8
  # Models media types used by internet protocols and data formats.
9
9
  module Media
10
- VERSION = "0.0.1"
10
+ VERSION = "0.2.0"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  Provides a small representation of media types which can be shared by protocol implementations and registry backends.
4
4
 
5
- ``` ruby
6
- require "protocol/media/type"
5
+ ## Usage
7
6
 
8
- type = Protocol::Media::Type.parse("text/plain; charset=utf-8")
9
- type.type # => "text"
10
- type.subtype # => "plain"
11
- type.parameters # => {"charset" => "utf-8"}
12
- ```
7
+ Please see the [project documentation](https://socketry.github.io/protocol-media/) for more details.
13
8
 
14
- Registry data and indexed lookup are provided separately by `protocol-media-data`.
9
+ - [Getting Started](https://socketry.github.io/protocol-media/guides/getting-started/index) - This guide explains how to represent, compare, and map media types with `protocol-media`.
10
+
11
+ - [Content Negotiation](https://socketry.github.io/protocol-media/guides/content-negotiation/index) - This guide explains how to combine `protocol-media` with an HTTP parser to select an application representation.
12
+
13
+ ## See Also
14
+
15
+ - [Protocol::Media::Registry](https://github.com/socketry/protocol-media-registry) provides registry data and indexed lookup by media type or file extension.
data/releases.md ADDED
@@ -0,0 +1,6 @@
1
+ # Releases
2
+
3
+ ## Unreleased
4
+
5
+ - Introduce `Protocol::Media::Set` for compatible media range membership.
6
+ - Make `Protocol::Media::Map` and `Protocol::Media::Set` immutable, with builders for incremental construction and indexed lookups.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-media
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -13,10 +13,14 @@ executables: []
13
13
  extensions: []
14
14
  extra_rdoc_files: []
15
15
  files:
16
+ - lib/protocol/media/map.rb
17
+ - lib/protocol/media/range.rb
18
+ - lib/protocol/media/set.rb
16
19
  - lib/protocol/media/type.rb
17
20
  - lib/protocol/media/version.rb
18
21
  - license.md
19
22
  - readme.md
23
+ - releases.md
20
24
  homepage: https://github.com/socketry/protocol-media
21
25
  licenses:
22
26
  - MIT