starry 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 +7 -0
- data/lib/starry/inner_list.rb +26 -0
- data/lib/starry/item.rb +17 -0
- data/lib/starry/parser.rb +205 -0
- data/lib/starry/version.rb +3 -0
- data/lib/starry.rb +150 -0
- metadata +50 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b104249aac02869f4536ddd816a828005419c76b7dfd14222ba781599d9ffc94
|
4
|
+
data.tar.gz: a795783282e4123bc20a964e90c36cfa228d257b2fe8e25c0cb83970aa87d2df
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6502b2202ae627ebd6bdbb2a4590ff8c4fac70fcc8373a8d44d214617132dcab7e5a2e2ec7b3792503f4e3379f1d6ae489d80c545936d1c58ff3b438c23f62b2
|
7
|
+
data.tar.gz: 02ddf59cef3d6399c5deabf6c438b18685936727ed3cac0be5671cece355559d9620b246cb6fd1d757a0d7dbb2ba1af7a704c78325e8313945a08056df0d94b0
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
class Starry::InnerList
|
4
|
+
|
5
|
+
attr_accessor :value, :parameters
|
6
|
+
|
7
|
+
include Enumerable
|
8
|
+
extend Forwardable
|
9
|
+
def_delegator :value, :each
|
10
|
+
|
11
|
+
def initialize(value = [], parameters = {})
|
12
|
+
@value = value
|
13
|
+
@parameters = parameters
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
self.class == other.class && self.value == other.value && self.parameters == other.parameters
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
members = self.map do |item|
|
22
|
+
Starry.serialize_item(item)
|
23
|
+
end
|
24
|
+
"(#{ members.join(' ') })#{ Starry.serialize_parameters(parameters) }"
|
25
|
+
end
|
26
|
+
end
|
data/lib/starry/item.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Starry::Item
|
2
|
+
|
3
|
+
attr_accessor :value, :parameters
|
4
|
+
|
5
|
+
def initialize(value, parameters = {})
|
6
|
+
@value = value
|
7
|
+
@parameters = parameters
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
self.class == other.class && self.value == other.value && self.parameters == other.parameters
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"#{ Starry.serialize_bare_item(value) }#{ Starry.serialize_parameters(parameters) }"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
class Starry::Parser
|
6
|
+
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def initialize(input, symbolize_names: false)
|
10
|
+
@s = StringScanner.new(input)
|
11
|
+
@symbolize_names = symbolize_names
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse(type)
|
15
|
+
consume_sp
|
16
|
+
output = (
|
17
|
+
case type
|
18
|
+
when :list
|
19
|
+
parse_list
|
20
|
+
when :dictionary
|
21
|
+
parse_dictionary
|
22
|
+
when :item
|
23
|
+
parse_item
|
24
|
+
else
|
25
|
+
raise ArgumentError
|
26
|
+
end
|
27
|
+
)
|
28
|
+
consume_sp
|
29
|
+
unless @s.eos?
|
30
|
+
parse_error(:eos)
|
31
|
+
end
|
32
|
+
output
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_list
|
36
|
+
output = []
|
37
|
+
until eos?
|
38
|
+
output << parse_item_or_inner_list
|
39
|
+
consume_ows
|
40
|
+
return output if eos?
|
41
|
+
expect(',')
|
42
|
+
consume_ows
|
43
|
+
parse_error if eos?
|
44
|
+
end
|
45
|
+
output
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_item_or_inner_list
|
49
|
+
if check('(')
|
50
|
+
parse_inner_list
|
51
|
+
else
|
52
|
+
parse_item
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_inner_list
|
57
|
+
output = []
|
58
|
+
expect('(')
|
59
|
+
until eos?
|
60
|
+
consume_sp
|
61
|
+
if scan(')')
|
62
|
+
parameters = parse_parameters
|
63
|
+
return Starry::InnerList.new(output, parameters)
|
64
|
+
end
|
65
|
+
output << parse_item
|
66
|
+
unless check(/[ )]/)
|
67
|
+
parse_error([' ', ')'])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
parse_error(')')
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_dictionary
|
74
|
+
output = {}
|
75
|
+
until eos?
|
76
|
+
key = parse_key
|
77
|
+
if scan('=')
|
78
|
+
value = parse_item_or_inner_list
|
79
|
+
else
|
80
|
+
parameters = parse_parameters
|
81
|
+
value = Starry::Item.new(true, parameters)
|
82
|
+
end
|
83
|
+
output[key] = value
|
84
|
+
consume_ows
|
85
|
+
return output if eos?
|
86
|
+
expect(',')
|
87
|
+
consume_ows
|
88
|
+
parse_error if eos?
|
89
|
+
end
|
90
|
+
output
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse_item
|
94
|
+
value = parse_bare_item
|
95
|
+
parameters = parse_parameters
|
96
|
+
Starry::Item.new(value, parameters)
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_bare_item
|
100
|
+
case
|
101
|
+
when check(/[-0-9]/)
|
102
|
+
parse_integer_or_decimal
|
103
|
+
when check('"')
|
104
|
+
parse_string
|
105
|
+
when check(/[A-Za-z*]/)
|
106
|
+
parse_token
|
107
|
+
when check(':')
|
108
|
+
parse_byte_sequence
|
109
|
+
when check('?')
|
110
|
+
parse_boolean
|
111
|
+
else
|
112
|
+
parse_error
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def parse_parameters
|
117
|
+
output = {}
|
118
|
+
until eos?
|
119
|
+
break unless scan(';')
|
120
|
+
consume_sp
|
121
|
+
key = parse_key
|
122
|
+
if scan('=')
|
123
|
+
value = parse_bare_item
|
124
|
+
else
|
125
|
+
value = true
|
126
|
+
end
|
127
|
+
output[key] = value
|
128
|
+
end
|
129
|
+
output
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_key
|
133
|
+
parse_error unless check(/[a-z*]/)
|
134
|
+
output = scan(/[a-z0-9_\-.*]*/)
|
135
|
+
if @symbolize_names
|
136
|
+
output.to_sym
|
137
|
+
else
|
138
|
+
output
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def parse_integer_or_decimal
|
143
|
+
unless output = scan(/-?(\d+)(\.\d+)?/)
|
144
|
+
parse_error
|
145
|
+
end
|
146
|
+
if @s[2]
|
147
|
+
if @s[1].size > 12 || @s[2].size > 4
|
148
|
+
parse_error
|
149
|
+
end
|
150
|
+
output.to_f
|
151
|
+
else
|
152
|
+
if @s[1].size > 15
|
153
|
+
parse_error
|
154
|
+
end
|
155
|
+
output.to_i
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_string
|
160
|
+
expect('"')
|
161
|
+
output = scan(/([\u0020-\u0021\u0023-\u005b\u005d-\u007e]|\\[\\"])*/)
|
162
|
+
expect('"')
|
163
|
+
output.gsub(/\\([\\"])/, '\1')
|
164
|
+
end
|
165
|
+
|
166
|
+
def parse_token
|
167
|
+
check(/[A-Za-z*]/)
|
168
|
+
scan(/[!#$%&'*+\-.^_`|~0-9A-Za-z:\/]*/).to_sym
|
169
|
+
end
|
170
|
+
|
171
|
+
def parse_byte_sequence
|
172
|
+
expect(':')
|
173
|
+
output = scan(/[A-Za-z0-9+\/=]*/)
|
174
|
+
expect(':')
|
175
|
+
Base64.decode64(output)
|
176
|
+
end
|
177
|
+
|
178
|
+
def parse_boolean
|
179
|
+
unless scan(/\?([01])/)
|
180
|
+
parse_error
|
181
|
+
end
|
182
|
+
@s[1] == '1'
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def_delegators :@s, :check, :scan, :eos?
|
188
|
+
|
189
|
+
def expect(regexp)
|
190
|
+
scan(regexp) || parse_error(regexp)
|
191
|
+
end
|
192
|
+
|
193
|
+
def consume_sp
|
194
|
+
scan(/ */)
|
195
|
+
end
|
196
|
+
|
197
|
+
def consume_ows
|
198
|
+
scan(/[ \t]*/)
|
199
|
+
end
|
200
|
+
|
201
|
+
def parse_error(_ = nil)
|
202
|
+
raise Starry::ParseError
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
data/lib/starry.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Starry
|
4
|
+
|
5
|
+
class SerializeError < ::StandardError; end
|
6
|
+
class ParseError < ::StandardError; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def serialize(input)
|
11
|
+
case input
|
12
|
+
when {}, []
|
13
|
+
return nil
|
14
|
+
when Hash
|
15
|
+
serialize_dictionary(input)
|
16
|
+
when Enumerable
|
17
|
+
serialize_list(input)
|
18
|
+
else
|
19
|
+
serialize_item(input)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def serialize_list(input)
|
24
|
+
input.map do |item|
|
25
|
+
serialize_item_or_inner_list(item)
|
26
|
+
end.join(', ')
|
27
|
+
end
|
28
|
+
|
29
|
+
def serialize_parameters(input)
|
30
|
+
input.transform_keys(&:to_s).map do |key, value|
|
31
|
+
if value == true
|
32
|
+
";#{ serialize_key(key) }"
|
33
|
+
else
|
34
|
+
";#{ serialize_key(key) }=#{ serialize_bare_item(value) }"
|
35
|
+
end
|
36
|
+
end.join('')
|
37
|
+
end
|
38
|
+
|
39
|
+
def serialize_key(input)
|
40
|
+
unless input.match?(/\A[a-z*][a-z0-9_\-.*]*\z/)
|
41
|
+
raise SerializeError, "The given value #{ input.inspect } contains characters that are not allowed as a key for dictionary / parameters in HTTP Structured Field."
|
42
|
+
end
|
43
|
+
input
|
44
|
+
end
|
45
|
+
|
46
|
+
def serialize_dictionary(input)
|
47
|
+
input.transform_keys(&:to_s).map do |key, value|
|
48
|
+
if value == true
|
49
|
+
serialize_key(key)
|
50
|
+
elsif value.kind_of?(Item) && value.value == true
|
51
|
+
"#{ serialize_key(key) }#{ serialize_parameters(value.parameters) }"
|
52
|
+
else
|
53
|
+
"#{ serialize_key(key) }=#{ serialize_item_or_inner_list(value) }"
|
54
|
+
end
|
55
|
+
end.join(', ')
|
56
|
+
end
|
57
|
+
|
58
|
+
def serialize_item(input)
|
59
|
+
if input.kind_of?(Item)
|
60
|
+
input.to_s
|
61
|
+
else
|
62
|
+
serialize_bare_item(input)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def serialize_bare_item(input)
|
67
|
+
case input
|
68
|
+
when Integer
|
69
|
+
if input.abs >= 10 ** 15
|
70
|
+
raise SerializeError, "Integer value in HTTP Structured Field must have an absolute value less than 10 ** 15, but #{ input } given."
|
71
|
+
end
|
72
|
+
input.to_s
|
73
|
+
when Float
|
74
|
+
x = input.round(3, half: :even)
|
75
|
+
if x.abs >= 10 ** 12
|
76
|
+
raise SerializeError, "Numeric value in HTTP Structured Field must have an absolute value less than 10 ** 15, but #{ input } given."
|
77
|
+
end
|
78
|
+
x.to_s
|
79
|
+
when String
|
80
|
+
if input.encoding == Encoding::ASCII_8BIT
|
81
|
+
":#{ Base64.strict_encode64(input) }:"
|
82
|
+
else
|
83
|
+
unless input.match?(/\A[\u0020-\u007E]*\z/)
|
84
|
+
raise SerializeError, "String value in HTTP Structured Field must consist of only ASCII printable characters, but given value #{ input.inspect } does not meet that."
|
85
|
+
end
|
86
|
+
"\"#{ input.gsub(/\\|"/) { "\\#{ _1 }" } }\""
|
87
|
+
end
|
88
|
+
when Symbol
|
89
|
+
unless input.to_s.match?(/\A[A-Za-z*][!#$%&'*+\-.^_`|~0-9A-Za-z:\/]*\z/)
|
90
|
+
raise SerializeError, "The given value #{ input.inspect } contains characters that are not allowed as Token in HTTP Structured Field."
|
91
|
+
end
|
92
|
+
input.to_s
|
93
|
+
when true
|
94
|
+
'?1'
|
95
|
+
when false
|
96
|
+
'?0'
|
97
|
+
else
|
98
|
+
raise SerializeError, "The given value #{ input.inspect } cannnot be used as a bare item of HTTP Structured Field."
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private def serialize_item_or_inner_list(input)
|
103
|
+
case input
|
104
|
+
when InnerList
|
105
|
+
input.to_s
|
106
|
+
when Hash
|
107
|
+
raise SerializeError, "Hash cannnot be used as an item of HTTP Structured Field, but #{ input.inspect } given."
|
108
|
+
when Enumerable
|
109
|
+
InnerList.new(input).to_s
|
110
|
+
when Item
|
111
|
+
case input.value
|
112
|
+
when Hash
|
113
|
+
raise SerializeError, "Hash cannnot be used as an item of HTTP Structured Field, but #{ input.value.inspect } given."
|
114
|
+
when Enumerable
|
115
|
+
InnerList.new(input.value, input.parameters).to_s
|
116
|
+
else
|
117
|
+
input.to_s
|
118
|
+
end
|
119
|
+
else
|
120
|
+
serialize_bare_item(input)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse_list(input)
|
125
|
+
ensure_ascii_only(input)
|
126
|
+
Parser.new(input).parse(:list)
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse_dictionary(input)
|
130
|
+
ensure_ascii_only(input)
|
131
|
+
Parser.new(input).parse(:dictionary)
|
132
|
+
end
|
133
|
+
|
134
|
+
def parse_item(input)
|
135
|
+
ensure_ascii_only(input)
|
136
|
+
Parser.new(input).parse(:item)
|
137
|
+
end
|
138
|
+
|
139
|
+
private def ensure_ascii_only(input)
|
140
|
+
unless input.ascii_only?
|
141
|
+
raise ParseError, "Input string contains unexpected non-ASCII character."
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
require_relative 'starry/inner_list'
|
148
|
+
require_relative 'starry/item'
|
149
|
+
require_relative 'starry/parser'
|
150
|
+
require_relative 'starry/version'
|
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: starry
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Takemaro
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-10-07 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: info@takemaro.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/starry.rb
|
20
|
+
- lib/starry/inner_list.rb
|
21
|
+
- lib/starry/item.rb
|
22
|
+
- lib/starry/parser.rb
|
23
|
+
- lib/starry/version.rb
|
24
|
+
homepage: https://github.com/takemar/starry
|
25
|
+
licenses:
|
26
|
+
- MIT
|
27
|
+
metadata:
|
28
|
+
bug_tracker_uri: https://github.com/takemar/starry/issues
|
29
|
+
homepage_uri: https://github.com/takemar/starry
|
30
|
+
source_code_uri: https://github.com/takemar/starry/issues
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 2.7.0
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
requirements: []
|
46
|
+
rubygems_version: 3.4.10
|
47
|
+
signing_key:
|
48
|
+
specification_version: 4
|
49
|
+
summary: An implementation of HTTP Structured Field Values (RFC 8941)
|
50
|
+
test_files: []
|