net-http-structured_field_values 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 50d5376e269e9e8cf277cb12a3dcf3fbf22d7f047b0a72654bce858e5e60994a
4
+ data.tar.gz: 3bdc2d923d5d9aeb32238fe9ce07ea12d9a570f5e47b85077ce0d950d20715b5
5
+ SHA512:
6
+ metadata.gz: 6862b163faa570c59554c2174a3d5b10b0477de62909f57e7c5887d58c80a65dbc8fabdf19eaa45c007327dfdb53b6dbfc5b6dfd16e6e3b4a4c815b5a3706c74
7
+ data.tar.gz: 173d2bb39bce5245778b3916dc3e8d9c975d8400ad4f096b4c3ed81a1970d7fe78e50699499f6f81d1f6938e57055fbcfa7a02a97a633bf60a9ffd24f81b2756
@@ -0,0 +1,27 @@
1
+ name: Check Actions
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - '.github/workflows/**.yml'
9
+ pull_request:
10
+ paths:
11
+ - '.github/workflows/**.yml'
12
+
13
+ jobs:
14
+ lint:
15
+ runs-on: ubuntu-latest
16
+ defaults:
17
+ run:
18
+ shell: bash
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+
24
+ - name: Install and Run Actionlint
25
+ run: |
26
+ bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
27
+ ./actionlint -color
@@ -0,0 +1,61 @@
1
+ name: Check Project
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths-ignore:
8
+ - '**.md'
9
+
10
+ pull_request:
11
+ paths-ignore:
12
+ - '**.md'
13
+
14
+ jobs:
15
+ rspec:
16
+ runs-on: ubuntu-latest
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - name: Set up Ruby
24
+ uses: ruby/setup-ruby@v1
25
+ with:
26
+ ruby-version: .ruby-version
27
+ bundler-cache: true
28
+
29
+ - name: Run RSpec
30
+ run: bundle exec rspec
31
+
32
+ - name: Upload coverage report
33
+ uses: codecov/codecov-action@v3
34
+
35
+ rubocop:
36
+ runs-on: ubuntu-latest
37
+
38
+ permissions:
39
+ contents: read
40
+ pull-requests: write
41
+
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ with:
45
+ fetch-depth: 0
46
+
47
+ - name: Set up Ruby
48
+ uses: ruby/setup-ruby@v1
49
+ with:
50
+ ruby-version: .ruby-version
51
+ bundler-cache: true
52
+
53
+ - name: Run RuboCop
54
+ uses: reviewdog/action-rubocop@v2
55
+ with:
56
+ rubocop_version: gemfile
57
+ rubocop_extensions: rubocop-performance:gemfile rubocop-rake:gemfile rubocop-rspec:gemfile
58
+ github_token: ${{ secrets.github_token }}
59
+ reporter: github-pr-review
60
+ fail_on_error: true
61
+ filter_mode: nofilter
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,25 @@
1
+ require:
2
+ - rubocop-performance
3
+ - rubocop-rake
4
+ - rubocop-rspec
5
+
6
+ AllCops:
7
+ NewCops: enable
8
+
9
+ Metrics:
10
+ Enabled: false
11
+
12
+ RSpec/MultipleExpectations:
13
+ Enabled: false
14
+
15
+ Style/NumericLiterals:
16
+ Enabled: false
17
+
18
+ Style/TrailingCommaInArguments:
19
+ EnforcedStyleForMultiline: consistent_comma
20
+
21
+ Style/TrailingCommaInArrayLiteral:
22
+ EnforcedStyleForMultiline: consistent_comma
23
+
24
+ Style/TrailingCommaInHashLiteral:
25
+ EnforcedStyleForMultiline: consistent_comma
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.2
data/Gemfile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in net-http-structured_field_values.gemspec
8
+ gemspec
9
+
10
+ group :development, :test do
11
+ gem 'bundler', '~> 2.4'
12
+ gem 'rake', '~> 13.0'
13
+
14
+ gem 'rubocop', '~> 1.56'
15
+ gem 'rubocop-performance', '~> 1.19'
16
+ gem 'rubocop-rake', '~> 0.4'
17
+ gem 'rubocop-rspec', '~> 2.1'
18
+ end
19
+
20
+ group :test do
21
+ gem 'rspec', '~> 3.12'
22
+
23
+ gem 'simplecov', '~> 0.22'
24
+ gem 'simplecov-cobertura', '~> 2.1'
25
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,92 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ net-http-structured_field_values (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ base64 (0.1.1)
11
+ diff-lcs (1.5.0)
12
+ docile (1.4.0)
13
+ json (2.6.3)
14
+ language_server-protocol (3.17.0.3)
15
+ parallel (1.23.0)
16
+ parser (3.2.2.3)
17
+ ast (~> 2.4.1)
18
+ racc
19
+ racc (1.7.1)
20
+ rainbow (3.1.1)
21
+ rake (13.0.6)
22
+ regexp_parser (2.8.1)
23
+ rexml (3.2.6)
24
+ rspec (3.12.0)
25
+ rspec-core (~> 3.12.0)
26
+ rspec-expectations (~> 3.12.0)
27
+ rspec-mocks (~> 3.12.0)
28
+ rspec-core (3.12.2)
29
+ rspec-support (~> 3.12.0)
30
+ rspec-expectations (3.12.3)
31
+ diff-lcs (>= 1.2.0, < 2.0)
32
+ rspec-support (~> 3.12.0)
33
+ rspec-mocks (3.12.6)
34
+ diff-lcs (>= 1.2.0, < 2.0)
35
+ rspec-support (~> 3.12.0)
36
+ rspec-support (3.12.1)
37
+ rubocop (1.56.3)
38
+ base64 (~> 0.1.1)
39
+ json (~> 2.3)
40
+ language_server-protocol (>= 3.17.0)
41
+ parallel (~> 1.10)
42
+ parser (>= 3.2.2.3)
43
+ rainbow (>= 2.2.2, < 4.0)
44
+ regexp_parser (>= 1.8, < 3.0)
45
+ rexml (>= 3.2.5, < 4.0)
46
+ rubocop-ast (>= 1.28.1, < 2.0)
47
+ ruby-progressbar (~> 1.7)
48
+ unicode-display_width (>= 2.4.0, < 3.0)
49
+ rubocop-ast (1.29.0)
50
+ parser (>= 3.2.1.0)
51
+ rubocop-capybara (2.18.0)
52
+ rubocop (~> 1.41)
53
+ rubocop-factory_bot (2.23.1)
54
+ rubocop (~> 1.33)
55
+ rubocop-performance (1.19.1)
56
+ rubocop (>= 1.7.0, < 2.0)
57
+ rubocop-ast (>= 0.4.0)
58
+ rubocop-rake (0.6.0)
59
+ rubocop (~> 1.0)
60
+ rubocop-rspec (2.24.0)
61
+ rubocop (~> 1.33)
62
+ rubocop-capybara (~> 2.17)
63
+ rubocop-factory_bot (~> 2.22)
64
+ ruby-progressbar (1.13.0)
65
+ simplecov (0.22.0)
66
+ docile (~> 1.1)
67
+ simplecov-html (~> 0.11)
68
+ simplecov_json_formatter (~> 0.1)
69
+ simplecov-cobertura (2.1.0)
70
+ rexml
71
+ simplecov (~> 0.19)
72
+ simplecov-html (0.12.3)
73
+ simplecov_json_formatter (0.1.4)
74
+ unicode-display_width (2.4.2)
75
+
76
+ PLATFORMS
77
+ ruby
78
+
79
+ DEPENDENCIES
80
+ bundler (~> 2.4)
81
+ net-http-structured_field_values!
82
+ rake (~> 13.0)
83
+ rspec (~> 3.12)
84
+ rubocop (~> 1.56)
85
+ rubocop-performance (~> 1.19)
86
+ rubocop-rake (~> 0.4)
87
+ rubocop-rspec (~> 2.1)
88
+ simplecov (~> 0.22)
89
+ simplecov-cobertura (~> 2.1)
90
+
91
+ BUNDLED WITH
92
+ 2.4.10
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 kyori19
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Net::Http::StructuredFieldValues
2
+
3
+ A Ruby implementation of [RFC 8941 - Structured Field Values for HTTP](https://datatracker.ietf.org/doc/html/rfc8941).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'net-http-structured_field_values'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install net-http-structured_field_values
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Development
26
+
27
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
+
29
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
+
31
+ ## Contributing
32
+
33
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kyori19/net-http-structured_field_values.
34
+
35
+ ## License
36
+
37
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'net/http/structured_field_values'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module HTTP
5
+ module StructuredFieldValues
6
+ # ParameterizedValue represents a value with parameters.
7
+ class ParameterizedValue
8
+ attr_reader :value, :parameters
9
+
10
+ def initialize(value, parameters)
11
+ @value = value
12
+ @parameters = parameters
13
+ end
14
+
15
+ def ==(other)
16
+ value == other.value && parameters == other.parameters
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'strscan'
5
+
6
+ require 'net/http/structured_field_values/parameterized_value'
7
+
8
+ # Disable some cops which is not compatible with StringScanner.
9
+ # rubocop:disable Lint/MissingCopEnableDirective
10
+ # rubocop:disable Performance/StringInclude
11
+ # rubocop:disable Style/CaseLikeIf
12
+ # rubocop:enable Lint/MissingCopEnableDirective
13
+
14
+ module Net
15
+ module HTTP
16
+ module StructuredFieldValues
17
+ # RFC8941 compliant parser which parses HTTP fields into Ruby objects.
18
+ #
19
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2}
20
+ class Parser
21
+ TOP_LEVEL_TYPES = %w[list dictionary item].freeze
22
+ private_constant :TOP_LEVEL_TYPES
23
+
24
+ # @param [String] input input bytes to be parsed
25
+ def initialize(input)
26
+ @scanner = StringScanner.new(input.encode(Encoding::ASCII))
27
+ remove_leading_spaces
28
+ rescue Encoding::UndefinedConversionError
29
+ raise ParseError, 'Unexpected input'
30
+ end
31
+
32
+ # @param [String] type type of the field to be parsed,
33
+ # must be one of 'list', 'dictionary' or 'item'
34
+ def parse_as(type)
35
+ raise ArgumentError, "Invalid type: #{type}" unless TOP_LEVEL_TYPES.include?(type)
36
+
37
+ send("parse_as_#{type}").tap do
38
+ remove_leading_spaces
39
+ raise ParseError, 'Unexpected input' unless scanner.eos?
40
+ end
41
+ end
42
+
43
+ TOP_LEVEL_TYPES.each do |type|
44
+ define_singleton_method("parse_as_#{type}") do |input|
45
+ new(input).parse_as(type)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :scanner
52
+
53
+ def remove_leading_spaces
54
+ scanner.skip(/ +/)
55
+ end
56
+
57
+ def remove_leading_whitespaces
58
+ scanner.skip(/[ \t]+/)
59
+ end
60
+
61
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.1}
62
+ # @return [Array<ParameterizedValue>]
63
+ def parse_as_list
64
+ return [] if scanner.eos?
65
+
66
+ [].tap do |result|
67
+ loop do
68
+ result << parse_as_item_or_inner_list
69
+ remove_leading_whitespaces
70
+ break if scanner.eos?
71
+
72
+ raise ParseError, 'Expected ","' unless scanner.skip(/,/)
73
+
74
+ remove_leading_whitespaces
75
+ raise ParseError, 'Expected next item' if scanner.eos?
76
+ end
77
+ end
78
+ end
79
+
80
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.1.1}
81
+ # @return [ParameterizedValue]
82
+ def parse_as_item_or_inner_list
83
+ if scanner.match?(/\(/)
84
+ parse_as_inner_list
85
+ else
86
+ parse_as_item
87
+ end
88
+ end
89
+
90
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.1.2}
91
+ # @return [ParameterizedValue<Array>]
92
+ def parse_as_inner_list
93
+ raise ParseError, 'Expected "("' unless scanner.skip(/\(/)
94
+
95
+ ParameterizedValue.new(
96
+ [].tap do |result|
97
+ loop do
98
+ remove_leading_spaces
99
+ break if scanner.skip(/\)/)
100
+
101
+ result << parse_as_item
102
+ raise ParseError, 'Expected space or ")"' unless scanner.match?(/[ )]/)
103
+ end
104
+ end,
105
+ parse_as_parameters,
106
+ )
107
+ end
108
+
109
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.2}
110
+ # @return [Hash]
111
+ def parse_as_dictionary
112
+ return {} if scanner.eos?
113
+
114
+ {}.tap do |result|
115
+ loop do
116
+ result[parse_as_key] = if scanner.skip(/=/)
117
+ parse_as_item_or_inner_list
118
+ else
119
+ ParameterizedValue.new(true, parse_as_parameters)
120
+ end
121
+ remove_leading_whitespaces
122
+ break if scanner.eos?
123
+
124
+ raise ParseError, 'Expected ","' unless scanner.skip(/,/)
125
+
126
+ remove_leading_whitespaces
127
+ raise ParseError, 'Expected next hash key' if scanner.eos?
128
+ end
129
+ end
130
+ end
131
+
132
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.3}
133
+ # @return [ParameterizedValue<Integer,Float,String,Boolean>]
134
+ def parse_as_item
135
+ ParameterizedValue.new(parse_as_bare_item, parse_as_parameters)
136
+ end
137
+
138
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.3.1}
139
+ # @return [Integer,Float,String,Symbol,Boolean]
140
+ def parse_as_bare_item
141
+ raise ParseError, 'Unexpected input' if scanner.eos?
142
+
143
+ if scanner.match?(/[-\d]/)
144
+ parse_as_integer_or_decimal
145
+ elsif scanner.match?(/"/)
146
+ parse_as_string
147
+ elsif scanner.match?(/[a-zA-Z*]/)
148
+ parse_as_token
149
+ elsif scanner.match?(/:/)
150
+ parse_as_byte_sequence
151
+ elsif scanner.match?(/\?/)
152
+ parse_as_boolean
153
+ else
154
+ raise ParseError, 'Unexpected input'
155
+ end
156
+ end
157
+
158
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.3.2}
159
+ # @return [Hash]
160
+ def parse_as_parameters
161
+ {}.tap do |result|
162
+ while scanner.skip(/;/)
163
+ remove_leading_spaces
164
+ result[parse_as_key] = if scanner.skip(/=/)
165
+ parse_as_bare_item
166
+ else
167
+ true
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.3.3}
174
+ # @return [String]
175
+ def parse_as_key
176
+ raise ParseError, 'Unexpected input' unless scanner.match?(/[a-z*]/)
177
+
178
+ scanner.scan(/[a-z\d_\-.*]+/)
179
+ end
180
+
181
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.4}
182
+ # @return [Integer,Float]
183
+ def parse_as_integer_or_decimal
184
+ sign = scanner.skip(/-/) ? -1 : 1
185
+ raise ParseError, 'Unexpected input' unless scanner.match?(/\d/)
186
+
187
+ str = scanner.scan(/\d+/)
188
+ num = if scanner.skip(/\./)
189
+ raise ParseError, 'Integer part of decimal number is too long' if str.length > 12
190
+ raise ParseError, 'Unexpected input' unless scanner.match?(/\d/)
191
+
192
+ frac = scanner.scan(/\d+/)
193
+ raise ParseError, 'Fractional part of decimal number is too long' if frac.length > 3
194
+
195
+ "#{str}.#{frac}".to_f
196
+ else
197
+ raise ParseError, 'Integer number is too long' if str.length > 15
198
+
199
+ str.to_i
200
+ end
201
+
202
+ num * sign
203
+ end
204
+
205
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.5}
206
+ # @return [String]
207
+ def parse_as_string
208
+ raise ParseError, 'Unexpected input' unless scanner.skip(/"/)
209
+
210
+ (+'').tap do |result|
211
+ loop do
212
+ if scanner.eos?
213
+ raise ParseError, 'Unexpected input'
214
+ elsif scanner.skip(/"/)
215
+ break
216
+ elsif scanner.skip(/\\/)
217
+ byte = scanner.scan(/["\\]/)
218
+ raise ParseError, 'Unexpected input' unless byte
219
+
220
+ result << byte
221
+ elsif scanner.match?(/[ -~]/)
222
+ result << scanner.scan(/[ !#-\[\]-~]+/)
223
+ else
224
+ raise ParseError, 'Expected space or visible ASCII character'
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.6}
231
+ # @return [Symbol]
232
+ def parse_as_token
233
+ raise ParseError, 'Unexpected input' unless scanner.match?(/[a-zA-Z*]/)
234
+
235
+ scanner.scan(/[!#-'*+\--:A-Z^-z|~]+/).to_sym
236
+ end
237
+
238
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.7}
239
+ # @return [String]
240
+ def parse_as_byte_sequence
241
+ raise ParseError, 'Unexpected input' unless scanner.skip(/:/)
242
+
243
+ str = scanner.scan(%r{[a-zA-Z\d+/=]+})
244
+ raise ParseError, 'Unexpected input' unless scanner.skip(/:/)
245
+
246
+ Base64.decode64(str || '')
247
+ end
248
+
249
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.2.8}
250
+ # @return [Boolean]
251
+ def parse_as_boolean
252
+ raise ParseError, 'Unexpected input' unless scanner.skip(/\?/)
253
+
254
+ case scanner.get_byte
255
+ when '0'
256
+ false
257
+ when '1'
258
+ true
259
+ else
260
+ raise ParseError, 'Unexpected input'
261
+ end
262
+ end
263
+
264
+ class ParseError < StandardError; end
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module HTTP
5
+ module StructuredFieldValues
6
+ # RFC8941 compliant serializer which serializes Ruby objects into HTTP fields.
7
+ #
8
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1}
9
+ class Serializer
10
+ def initialize
11
+ @result = +''
12
+ end
13
+
14
+ # @return [String]
15
+ def serialize(obj)
16
+ case obj
17
+ when Array
18
+ serialize_list(obj)
19
+ when Hash
20
+ serialize_dictionary(obj)
21
+ else
22
+ value, params = unpack_parameterized_value(obj)
23
+ serialize_item(value, params)
24
+ end
25
+
26
+ result.encode(Encoding::ASCII)
27
+ end
28
+
29
+ # @return [String]
30
+ def self.serialize(obj)
31
+ new.serialize(obj)
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :result
37
+
38
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.1}
39
+ #
40
+ # @param [Array] arr
41
+ def serialize_list(arr)
42
+ return if arr.empty?
43
+
44
+ loop do
45
+ value, params = unpack_parameterized_value(arr.shift)
46
+ case value
47
+ when Array
48
+ serialize_inner_list(value, params)
49
+ else
50
+ serialize_item(value, params)
51
+ end
52
+
53
+ break if arr.empty?
54
+
55
+ result << ', '
56
+ end
57
+ end
58
+
59
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.1.1}
60
+ #
61
+ # @param [Array] arr
62
+ # @param [Hash] params
63
+ def serialize_inner_list(arr, params)
64
+ result << '('
65
+
66
+ unless arr.empty?
67
+ loop do
68
+ serialize_item(*unpack_parameterized_value(arr.shift))
69
+
70
+ break if arr.empty?
71
+
72
+ result << ' '
73
+ end
74
+ end
75
+
76
+ result << ')'
77
+
78
+ serialize_parameters(params)
79
+ end
80
+
81
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.1.2}
82
+ #
83
+ # @param [Hash] params
84
+ def serialize_parameters(params)
85
+ params.each do |key, value|
86
+ result << ';'
87
+
88
+ serialize_key(key)
89
+ next if value == true
90
+
91
+ result << '='
92
+ serialize_bare_item(value)
93
+ end
94
+ end
95
+
96
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.1.3}
97
+ #
98
+ # @param [String] key
99
+ def serialize_key(key)
100
+ key = key.encode(Encoding::ASCII)
101
+ raise SerializationError, 'Invalid key' unless key.match?(/\A[a-z*][a-z\d_\-*]*\z/)
102
+
103
+ result << key
104
+ end
105
+
106
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.2}
107
+ #
108
+ # @param [Hash] dict
109
+ def serialize_dictionary(dict)
110
+ return if dict.empty?
111
+
112
+ loop do
113
+ key, obj = dict.shift
114
+ serialize_key(key)
115
+
116
+ value, params = unpack_parameterized_value(obj)
117
+ if value == true
118
+ serialize_parameters(params)
119
+ else
120
+ result << '='
121
+ case value
122
+ when Array
123
+ serialize_inner_list(value, params)
124
+ else
125
+ serialize_item(value, params)
126
+ end
127
+ end
128
+
129
+ break if dict.empty?
130
+
131
+ result << ', '
132
+ end
133
+ end
134
+
135
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.3}
136
+ #
137
+ # @param [Object] item
138
+ # @param [Hash] params
139
+ def serialize_item(item, params)
140
+ serialize_bare_item(item)
141
+ serialize_parameters(params)
142
+ end
143
+
144
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.3.1}
145
+ def serialize_bare_item(item)
146
+ case item
147
+ when Integer
148
+ serialize_integer(item)
149
+ when Float
150
+ serialize_decimal(item)
151
+ when String
152
+ case item.encoding
153
+ when Encoding::BINARY
154
+ serialize_byte_sequence(item)
155
+ else
156
+ serialize_string(item)
157
+ end
158
+ when Symbol
159
+ serialize_token(item)
160
+ when TrueClass, FalseClass
161
+ serialize_boolean(item)
162
+ else
163
+ raise SerializationError, "Unexpected item: #{item.inspect}"
164
+ end
165
+ end
166
+
167
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.4}
168
+ #
169
+ # @param [Integer] int
170
+ def serialize_integer(int)
171
+ unless (-999_999_999_999_999..999_999_999_999_999).cover?(int)
172
+ raise SerializationError, 'integers must be in the range of -999,999,999,999,999 to 999,999,999,999,999'
173
+ end
174
+
175
+ result << int.to_s
176
+ end
177
+
178
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.5}
179
+ #
180
+ # @param [Float] decimal
181
+ def serialize_decimal(decimal)
182
+ decimal = decimal.round(3, half: :even)
183
+ str = decimal.to_s
184
+ i = str.index('.')
185
+
186
+ raise SerializationError, 'integer part of decimals must be less than 13 chars' if i.nil? || i > 12
187
+
188
+ result << str
189
+ end
190
+
191
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.6}
192
+ #
193
+ # @param [String] str
194
+ def serialize_string(str)
195
+ result << '"'
196
+
197
+ s = StringScanner.new(str.encode(Encoding::ASCII))
198
+ loop do
199
+ if (part = s.scan(/[ !#-\[\]-~]+/))
200
+ result << part
201
+ end
202
+
203
+ break if s.eos?
204
+
205
+ raise SerializationError, 'Invalid string' unless (byte = s.scan(/["\\]/))
206
+
207
+ result << '\\'
208
+ result << byte
209
+ end
210
+
211
+ result << '"'
212
+ rescue EncodingError
213
+ raise SerializationError, 'Invalid string'
214
+ end
215
+
216
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.7}
217
+ #
218
+ # @param [Symbol] token
219
+ def serialize_token(token)
220
+ str = token.to_s
221
+
222
+ raise SerializationError, 'Invalid token' unless str.match?(/\A[a-zA-Z*][!#-'*+\--:A-Z^-z|~]*\z/)
223
+
224
+ result << str
225
+ end
226
+
227
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.8}
228
+ #
229
+ # @param [String] bytes
230
+ def serialize_byte_sequence(bytes)
231
+ result << ':'
232
+ result << Base64.strict_encode64(bytes)
233
+ result << ':'
234
+ end
235
+
236
+ # @see {https://www.rfc-editor.org/rfc/rfc8941#section-4.1.9}
237
+ #
238
+ # @param [Boolean] bool
239
+ def serialize_boolean(bool)
240
+ result << '?'
241
+ result << (bool ? '1' : '0')
242
+ end
243
+
244
+ def unpack_parameterized_value(obj)
245
+ case obj
246
+ when ParameterizedValue
247
+ [obj.value, obj.parameters]
248
+ else
249
+ [obj, {}]
250
+ end
251
+ end
252
+
253
+ class SerializationError < StandardError; end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Http
5
+ module StructuredFieldValues
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http/structured_field_values/parameterized_value'
4
+ require 'net/http/structured_field_values/parser'
5
+ require 'net/http/structured_field_values/version'
6
+
7
+ module Net
8
+ module Http
9
+ # A Ruby implementation of RFC 8941 - Structured Field Values for HTTP.
10
+ #
11
+ # @see https://datatracker.ietf.org/doc/html/rfc8941
12
+ module StructuredFieldValues
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'net/http/structured_field_values/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'net-http-structured_field_values'
9
+ spec.version = Net::Http::StructuredFieldValues::VERSION
10
+ spec.authors = ['kyori19']
11
+ spec.email = ['kyori@accelf.net']
12
+
13
+ spec.summary = 'A Ruby implementation of RFC 8941 - Structured Field Values for HTTP'
14
+ spec.homepage = 'https://github.com/kyori19/net-http-structured_field_values'
15
+ spec.license = 'MIT'
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
21
+ spec.metadata['rubygems_mfa_required'] = 'true'
22
+
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = 'https://github.com/kyori19/net-http-structured_field_values'
25
+ else
26
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
27
+ 'public gem pushes.'
28
+ end
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
33
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ end
35
+ spec.bindir = 'exe'
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ['lib']
38
+
39
+ spec.required_ruby_version = '>= 3.2'
40
+ end
data/renovate.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:base"
5
+ ]
6
+ }
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: net-http-structured_field_values
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - kyori19
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-09-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - kyori@accelf.net
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".github/workflows/check-actions.yml"
21
+ - ".github/workflows/check-project.yml"
22
+ - ".gitignore"
23
+ - ".rspec"
24
+ - ".rubocop.yml"
25
+ - ".ruby-version"
26
+ - Gemfile
27
+ - Gemfile.lock
28
+ - LICENSE.txt
29
+ - README.md
30
+ - Rakefile
31
+ - bin/console
32
+ - bin/setup
33
+ - lib/net/http/structured_field_values.rb
34
+ - lib/net/http/structured_field_values/parameterized_value.rb
35
+ - lib/net/http/structured_field_values/parser.rb
36
+ - lib/net/http/structured_field_values/serializer.rb
37
+ - lib/net/http/structured_field_values/version.rb
38
+ - net-http-structured_field_values.gemspec
39
+ - renovate.json
40
+ homepage: https://github.com/kyori19/net-http-structured_field_values
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ allowed_push_host: https://rubygems.org
45
+ rubygems_mfa_required: 'true'
46
+ homepage_uri: https://github.com/kyori19/net-http-structured_field_values
47
+ source_code_uri: https://github.com/kyori19/net-http-structured_field_values
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '3.2'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.4.10
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: A Ruby implementation of RFC 8941 - Structured Field Values for HTTP
67
+ test_files: []