starry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Starry
2
+ VERSION = '0.1.0'
3
+ 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: []