kdl 0.1.0 → 1.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ require_relative './email/parser'
2
+
3
+ module KDL
4
+ module Types
5
+ class Email < Value
6
+ attr_reader :local, :domain
7
+
8
+ def initialize(value, local:, domain:, **kwargs)
9
+ super(value, **kwargs)
10
+ @local = local
11
+ @domain = domain
12
+ end
13
+
14
+ def self.call(value, type = 'email')
15
+ local, domain = Parser.new(value.value).parse
16
+
17
+ new(value.value, type: type, local: local, domain: domain)
18
+ end
19
+
20
+ end
21
+ MAPPING['email'] = Email
22
+
23
+ class IDNEmail < Email
24
+ attr_reader :unicode_domain
25
+
26
+ def initialize(value, unicode_domain:, **kwargs)
27
+ super(value, **kwargs)
28
+ @unicode_domain = unicode_domain
29
+ end
30
+
31
+ def self.call(value, type = 'email')
32
+ local, domain, unicode_domain = Email::Parser.new(value.value, idn: true).parse
33
+
34
+ new("#{local}@#{domain}", type: type, local: local, domain: domain, unicode_domain: unicode_domain)
35
+ end
36
+
37
+ def unicode_value
38
+ "#{local}@#{unicode_domain}"
39
+ end
40
+ end
41
+ MAPPING['idn-email'] = IDNEmail
42
+ end
43
+ end
@@ -0,0 +1,51 @@
1
+ require 'simpleidn'
2
+
3
+ module KDL
4
+ module Types
5
+ class Hostname < Value
6
+ class Validator
7
+ PART_RGX = /^[a-z0-9_][a-z0-9_\-]{0,62}$/i
8
+
9
+ attr_reader :string
10
+ alias ascii string
11
+ alias unicode string
12
+
13
+ def initialize(string)
14
+ @string = string
15
+ end
16
+
17
+ def valid?
18
+ return false if @string.length > 253
19
+
20
+ @string.split('.').all? { |x| valid_part?(x) }
21
+ end
22
+
23
+ private
24
+
25
+ def valid_part?(part)
26
+ return false if part.empty?
27
+ return false if part.start_with?('-') || part.end_with?('-')
28
+
29
+ part =~ PART_RGX
30
+ end
31
+ end
32
+ end
33
+
34
+ class IDNHostname < Hostname
35
+ class Validator < Hostname::Validator
36
+ attr_reader :unicode
37
+
38
+ def initialize(string)
39
+ is_ascii = string.split('.').any? { |x| x.start_with?('xn--') }
40
+ if is_ascii
41
+ super(string)
42
+ @unicode = SimpleIDN.to_unicode(string)
43
+ else
44
+ super(SimpleIDN.to_ascii(string))
45
+ @unicode = string
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,32 @@
1
+ require_relative './hostname/validator'
2
+
3
+ module KDL
4
+ module Types
5
+ class Hostname < Value
6
+ def self.call(value, type = 'hostname')
7
+ validator = Validator.new(value.value)
8
+ raise ArgumentError, "invalid hostname #{value}" unless validator.valid?
9
+
10
+ new(value.value, type: type)
11
+ end
12
+ end
13
+ MAPPING['hostname'] = Hostname
14
+
15
+ class IDNHostname < Hostname
16
+ attr_reader :unicode_value
17
+
18
+ def initialize(value, unicode_value:, **kwargs)
19
+ super(value, **kwargs)
20
+ @unicode_value = unicode_value
21
+ end
22
+
23
+ def self.call(value, type = 'idn-hostname')
24
+ validator = Validator.new(value.value)
25
+ raise ArgumentError, "invalid hostname #{value}" unless validator.valid?
26
+
27
+ new(validator.ascii, type: type, unicode_value: validator.unicode)
28
+ end
29
+ end
30
+ MAPPING['idn-hostname'] = IDNHostname
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ module KDL
2
+ module Types
3
+ class IP < Value
4
+ def self.call(value, type = ip_type)
5
+ return nil unless value.is_a? ::KDL::Value::String
6
+
7
+ ip = ::IPAddr.new(value.value)
8
+ raise ArgumentError, "invalid #{ip_type} address" unless valid_ip?(ip)
9
+
10
+ new(ip, type: type)
11
+ end
12
+
13
+ def self.valid_ip?(ip)
14
+ ip.__send__(:"#{ip_type}?")
15
+ end
16
+ end
17
+
18
+ class IPV4 < IP
19
+ def self.ip_type
20
+ 'ipv4'
21
+ end
22
+ end
23
+ MAPPING['ipv4'] = IPV4
24
+
25
+ class IPV6 < IP
26
+ def self.ip_type
27
+ 'ipv6'
28
+ end
29
+ end
30
+ MAPPING['ipv6'] = IPV6
31
+ end
32
+ end
@@ -0,0 +1,123 @@
1
+ module KDL
2
+ module Types
3
+ class IRLReference < Value
4
+ class Parser
5
+ RGX = /^(?:(?:([a-z][a-z0-9+.\-]+)):\/\/([^@]+@)?([^\/?#]+)?)?(\/?[^?#]*)?(?:\?([^#]*))?(?:#(.*))?$/i.freeze
6
+ PERCENT_RGX = /%[a-f0-9]{2}/i.freeze
7
+
8
+ RESERVED_URL_CHARS = %w[! # $ & ' ( ) * + , / : ; = ? @ \[ \] %]
9
+ UNRESERVED_URL_CHARS = %w[A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
10
+ a b c d e f g h i j k l m n o p q r s t u v w x y z
11
+ 0 1 2 3 4 5 6 7 8 9 - _ . ~].freeze
12
+ URL_CHARS = RESERVED_URL_CHARS + UNRESERVED_URL_CHARS
13
+
14
+ def initialize(string)
15
+ @string = string
16
+ end
17
+
18
+ def parse
19
+ scheme, auth, domain, path, search, hash = *parse_url
20
+
21
+ if @string.ascii_only?
22
+ unicode_path = Parser.decode(path)
23
+ unicode_search = Parser.decode(search)
24
+ unicode_hash = Parser.decode(hash)
25
+ else
26
+ unicode_path = path
27
+ path = Parser.encode(unicode_path)
28
+ unicode_search = search
29
+ search_params = unicode_search ? unicode_search.split('&').map { |x| x.split('=') } : nil
30
+ search = search_params ? search_params.map { |k, v| "#{Parser.encode(k)}=#{Parser.encode(v)}" }.join('&') : nil
31
+ unicode_hash = hash
32
+ hash = Parser.encode(hash)
33
+ end
34
+
35
+ if domain
36
+ validator = IDNHostname::Validator.new(domain)
37
+ domain = validator.ascii
38
+ unicode_domain = validator.unicode
39
+ else
40
+ unicode_domain = domain
41
+ end
42
+
43
+ unicode_value = Parser.build_uri_string(scheme, auth, unicode_domain, unicode_path, unicode_search, unicode_hash)
44
+ ascii_value = Parser.build_uri_string(scheme, auth, domain, path, search, hash)
45
+
46
+ [ascii_value,
47
+ { unicode_value: unicode_value,
48
+ unicode_domain: unicode_domain,
49
+ unicode_path: unicode_path,
50
+ unicode_search: unicode_search,
51
+ unicode_hash: unicode_hash }]
52
+ end
53
+
54
+ def parse_url
55
+ match = RGX.match(@string)
56
+ raise ArgumentError, "invalid IRL `#{@string}'" if match.nil?
57
+
58
+ _, *parts = *match
59
+ raise ArgumentError, "invalid IRL `#{@string}'" unless parts.all? { |part| Parser.valid_url_part?(part) }
60
+
61
+ parts
62
+ end
63
+
64
+ def self.valid_url_part?(string)
65
+ return true unless string
66
+
67
+ string.chars.all? do |char|
68
+ !char.ascii_only? || URL_CHARS.include?(char)
69
+ end
70
+ end
71
+
72
+ def self.encode(string)
73
+ return string unless string
74
+
75
+ string.chars
76
+ .map { |c| c.ascii_only? ? c : percent_encode(c) }
77
+ .join
78
+ .force_encoding('utf-8')
79
+ end
80
+
81
+ def self.decode(string)
82
+ return string unless string
83
+
84
+ string.gsub(PERCENT_RGX) do |match|
85
+ char = match[1, 2].to_i(16).chr
86
+ if RESERVED_URL_CHARS.include?(char)
87
+ match
88
+ else
89
+ char
90
+ end
91
+ end.force_encoding('utf-8')
92
+ end
93
+
94
+ def self.percent_encode(c)
95
+ c.bytes.map { |b| "%#{b.to_s(16)}" }.join.upcase
96
+ end
97
+
98
+ def self.build_uri_string(scheme, auth, domain, path, search, hash)
99
+ string = ''
100
+ string += "#{scheme}://" if scheme
101
+ string += auth if auth
102
+ string += domain if domain
103
+ string += path if path
104
+ string += "?#{search}" if search
105
+ string += "##{hash}" if hash
106
+ string
107
+ end
108
+ end
109
+ end
110
+
111
+ class IRL < IRLReference
112
+ class Parser < IRLReference::Parser
113
+ def parse_url
114
+ parts = super
115
+ scheme, * = parts
116
+ raise ArgumentError, "invalid IRL `#{@string}'" if scheme.nil? || scheme.empty?
117
+
118
+ parts
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,46 @@
1
+ require_relative './irl/parser'
2
+
3
+ module KDL
4
+ module Types
5
+ class IRLReference < Value
6
+ attr_reader :unicode_value,
7
+ :unicode_domain,
8
+ :unicode_path,
9
+ :unicode_search,
10
+ :unicode_hash
11
+
12
+ def initialize(value, unicode_value:, unicode_domain:, unicode_path:, unicode_search:, unicode_hash:, **kwargs)
13
+ super(value, **kwargs)
14
+ @unicode_value = unicode_value
15
+ @unicode_domain = unicode_domain
16
+ @unicode_path = unicode_path
17
+ @unicode_search = unicode_search
18
+ @unicode_hash = unicode_hash
19
+ end
20
+
21
+ def self.call(value, type = 'irl-reference')
22
+ return nil unless value.is_a? ::KDL::Value::String
23
+
24
+ ascii_value, params = parser(value.value).parse
25
+
26
+ new(URI.parse(ascii_value), type: type, **params)
27
+ end
28
+
29
+ def self.parser(string)
30
+ IRLReference::Parser.new(string)
31
+ end
32
+ end
33
+ MAPPING['irl-reference'] = IRLReference
34
+
35
+ class IRL < IRLReference
36
+ def self.call(value, type = 'irl')
37
+ super(value, type)
38
+ end
39
+
40
+ def self.parser(string)
41
+ IRL::Parser.new(string)
42
+ end
43
+ end
44
+ MAPPING['irl'] = IRL
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ module KDL
2
+ module Types
3
+ class Regex < Value
4
+ def self.call(value, type = 'regex')
5
+ return nil unless value.is_a? ::KDL::Value::String
6
+
7
+ regex = ::Regexp.new(value.value)
8
+ new(regex, type: type)
9
+ end
10
+ end
11
+ MAPPING['regex'] = Regex
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ module KDL
2
+ module Types
3
+ class URLReference < Value
4
+ def self.call(value, type = 'url-reference')
5
+ return nil unless value.is_a? ::KDL::Value::String
6
+
7
+ uri = parse_url(value.value)
8
+ new(uri, type: type)
9
+ end
10
+
11
+ def self.parse_url(string)
12
+ URI.parse(string)
13
+ end
14
+ end
15
+ MAPPING['url-reference'] = URLReference
16
+
17
+ class URL < URLReference
18
+ def self.call(value, type = 'url')
19
+ super(value, type)
20
+ end
21
+
22
+ def self.parse_url(string)
23
+ super.tap do |uri|
24
+ raise 'invalid URL' if uri.scheme.nil?
25
+ end
26
+ end
27
+ end
28
+ MAPPING['url'] = URL
29
+ end
30
+ end
@@ -0,0 +1,328 @@
1
+ module KDL
2
+ module Types
3
+ class URLTemplate < Value
4
+ UNRESERVED = /[a-zA-Z0-9\-._~]/.freeze
5
+ RESERVED = %r{[:/?#\[\]@!$&'()*+,;=]}.freeze
6
+
7
+ def self.call(value, type = 'url-template')
8
+ return nil unless value.is_a? ::KDL::Value::String
9
+
10
+ parts = Parser.parse(value.value)
11
+ new(parts, type: type)
12
+ end
13
+
14
+ def expand(variables)
15
+ result = value.map { |v| v.expand(variables) }.join
16
+ parser = IRLReference::Parser.new(result)
17
+ uri, * = parser.parse
18
+ URI(uri)
19
+ end
20
+
21
+ class Parser
22
+ def self.parse(string)
23
+ new(string).parse
24
+ end
25
+
26
+ def initialize(string)
27
+ @string = string
28
+ @index = 0
29
+ end
30
+
31
+ def parse
32
+ result = []
33
+ until (token = next_token).nil?
34
+ result << token
35
+ end
36
+ result
37
+ end
38
+
39
+ def next_token
40
+ buffer = ''
41
+ context = nil
42
+ expansion_type = nil
43
+ loop do
44
+ c = @string[@index]
45
+ case context
46
+ when nil
47
+ case c
48
+ when '{'
49
+ context = :expansion
50
+ buffer = ''
51
+ n = @string[@index + 1]
52
+ expansion_type = case n
53
+ when '+' then ReservedExpansion
54
+ when '#' then FragmentExpansion
55
+ when '.' then LabelExpansion
56
+ when '/' then PathExpantion
57
+ when ';' then ParameterExpansion
58
+ when '?' then QueryExpansion
59
+ when '&' then QueryContinuation
60
+ else StringExpansion
61
+ end
62
+ @index += (expansion_type == StringExpansion ? 1 : 2)
63
+ when nil then return nil
64
+ else
65
+ buffer = c
66
+ @index += 1
67
+ context = :literal
68
+ end
69
+ when :literal
70
+ case c
71
+ when '{', nil then return StringLiteral.new(buffer)
72
+ else
73
+ buffer << c
74
+ @index += 1
75
+ end
76
+ when :expansion
77
+ case c
78
+ when '}'
79
+ @index += 1
80
+ return parse_expansion(buffer, expansion_type)
81
+ when nil
82
+ raise ArgumentError, 'unterminated expansion'
83
+ else
84
+ buffer << c
85
+ @index += 1
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def parse_expansion(string, type)
92
+ variables = string.split(',').map do |str|
93
+ case str
94
+ when /(.*)\*$/
95
+ Variable.new(Regexp.last_match(1),
96
+ explode: true,
97
+ allow_reserved: type.allow_reserved?,
98
+ with_name: type.with_name?,
99
+ keep_empties: type.keep_empties?)
100
+ when /(.*):(\d+)/
101
+ Variable.new(Regexp.last_match(1),
102
+ limit: Regexp.last_match(2).to_i,
103
+ allow_reserved: type.allow_reserved?,
104
+ with_name: type.with_name?,
105
+ keep_empties: type.keep_empties?)
106
+ else
107
+ Variable.new(str,
108
+ allow_reserved: type.allow_reserved?,
109
+ with_name: type.with_name?,
110
+ keep_empties: type.keep_empties?)
111
+ end
112
+ end
113
+ type.new(variables)
114
+ end
115
+ end
116
+
117
+ class Variable
118
+ attr_reader :name
119
+
120
+ def initialize(name, limit: nil, explode: false, allow_reserved: false, with_name: false, keep_empties: false)
121
+ @name = name.to_sym
122
+ @limit = limit
123
+ @explode = explode
124
+ @allow_reserved = allow_reserved
125
+ @with_name = with_name
126
+ @keep_empties = keep_empties
127
+ end
128
+
129
+ def expand(value)
130
+ if @explode
131
+ case value
132
+ when Array
133
+ value.map { |v| prefix(encode(v)) }
134
+ when Hash
135
+ value.map { |k, v| prefix(encode(v), k) }
136
+ else
137
+ [prefix(encode(value))]
138
+ end
139
+ elsif @limit
140
+ [prefix(limit(value))].compact
141
+ else
142
+ [prefix(flatten(value))].compact
143
+ end
144
+ end
145
+
146
+ def limit(string)
147
+ return nil unless string
148
+
149
+ encode(string[0, @limit])
150
+ end
151
+
152
+ def flatten(value)
153
+ case value
154
+ when String
155
+ encode(value)
156
+ when Array, Hash
157
+ result = value.to_a
158
+ .flatten
159
+ .compact
160
+ .map { |v| encode(v) }
161
+ result.empty? ? nil : result.join(',')
162
+ end
163
+ end
164
+
165
+ def encode(string)
166
+ return nil unless string
167
+
168
+ string.to_s
169
+ .chars
170
+ .map do |c|
171
+ if UNRESERVED.match?(c) || (@allow_reserved && RESERVED.match?(c))
172
+ c
173
+ else
174
+ IRLReference::Parser.percent_encode(c)
175
+ end
176
+ end
177
+ .join
178
+ .force_encoding('utf-8')
179
+ end
180
+
181
+ def prefix(string, override = nil)
182
+ return nil unless string
183
+
184
+ key = override || @name
185
+
186
+ if @with_name || override
187
+ if string.empty? && !@keep_empties
188
+ encode(key)
189
+ else
190
+ "#{encode(key)}=#{string}"
191
+ end
192
+ else
193
+ string
194
+ end
195
+ end
196
+ end
197
+
198
+ class Part
199
+ def expand_variables(values)
200
+ @variables.reduce([]) do |list, variable|
201
+ expanded = variable.expand(values[variable.name])
202
+ expanded ? list + expanded : list
203
+ end
204
+ end
205
+
206
+ def separator
207
+ ','
208
+ end
209
+
210
+ def prefix
211
+ ''
212
+ end
213
+
214
+ def self.allow_reserved?
215
+ false
216
+ end
217
+
218
+ def self.with_name?
219
+ false
220
+ end
221
+
222
+ def self.keep_empties?
223
+ false
224
+ end
225
+ end
226
+
227
+ class StringLiteral < Part
228
+ def initialize(value)
229
+ super()
230
+ @value = value
231
+ end
232
+
233
+ def expand(*)
234
+ @value
235
+ end
236
+ end
237
+
238
+ class StringExpansion < Part
239
+ def initialize(variables)
240
+ super()
241
+ @variables = variables
242
+ end
243
+
244
+ def expand(values)
245
+ expanded = expand_variables(values)
246
+ return '' if expanded.empty?
247
+
248
+ prefix + expanded.join(separator)
249
+ end
250
+ end
251
+
252
+ class ReservedExpansion < StringExpansion
253
+ def self.allow_reserved?
254
+ true
255
+ end
256
+ end
257
+
258
+ class FragmentExpansion < StringExpansion
259
+ def prefix
260
+ '#'
261
+ end
262
+
263
+ def self.allow_reserved?
264
+ true
265
+ end
266
+ end
267
+
268
+ class LabelExpansion < StringExpansion
269
+ def prefix
270
+ '.'
271
+ end
272
+
273
+ def separator
274
+ '.'
275
+ end
276
+ end
277
+
278
+ class PathExpantion < StringExpansion
279
+ def prefix
280
+ '/'
281
+ end
282
+
283
+ def separator
284
+ '/'
285
+ end
286
+ end
287
+
288
+ class ParameterExpansion < StringExpansion
289
+ def prefix
290
+ ';'
291
+ end
292
+
293
+ def separator
294
+ ';'
295
+ end
296
+
297
+ def self.with_name?
298
+ true
299
+ end
300
+ end
301
+
302
+ class QueryExpansion < StringExpansion
303
+ def prefix
304
+ '?'
305
+ end
306
+
307
+ def separator
308
+ '&'
309
+ end
310
+
311
+ def self.with_name?
312
+ true
313
+ end
314
+
315
+ def self.keep_empties?
316
+ true
317
+ end
318
+ end
319
+
320
+ class QueryContinuation < QueryExpansion
321
+ def prefix
322
+ '&'
323
+ end
324
+ end
325
+ end
326
+ MAPPING['url-template'] = URLTemplate
327
+ end
328
+ end