kdl 0.1.1 → 1.0.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.
@@ -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