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 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: []