typero 0.3.6 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/lib/adapters/sequel.rb +69 -0
- data/lib/typero.rb +11 -162
- data/lib/typero/func.rb +14 -0
- data/lib/typero/params.rb +84 -0
- data/lib/typero/type/type.rb +82 -0
- data/lib/typero/type/{boolean.rb → types/boolean.rb} +6 -0
- data/lib/typero/type/types/currency.rb +19 -0
- data/lib/typero/type/types/date.rb +10 -0
- data/lib/typero/type/types/datetime.rb +10 -0
- data/lib/typero/type/types/email.rb +21 -0
- data/lib/typero/type/types/float.rb +21 -0
- data/lib/typero/type/types/hash.rb +22 -0
- data/lib/typero/type/types/image.rb +28 -0
- data/lib/typero/type/types/integer.rb +21 -0
- data/lib/typero/type/{label.rb → types/label.rb} +7 -1
- data/lib/typero/type/types/oib.rb +42 -0
- data/lib/typero/type/types/point.rb +29 -0
- data/lib/typero/type/types/string.rb +23 -0
- data/lib/typero/type/types/text.rb +12 -0
- data/lib/typero/type/types/url.rb +18 -0
- data/lib/typero/typero.rb +177 -0
- metadata +27 -19
- data/lib/typero/type.rb +0 -28
- data/lib/typero/type/array.rb +0 -32
- data/lib/typero/type/email.rb +0 -14
- data/lib/typero/type/float.rb +0 -13
- data/lib/typero/type/hash.rb +0 -10
- data/lib/typero/type/integer.rb +0 -11
- data/lib/typero/type/oib.rb +0 -29
- data/lib/typero/type/string.rb +0 -12
- data/lib/typero/type/url.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1b145b43e7736b2c9f673b6685388a39ee83fcba0bbcbc0ad2fcf8af2c1f692
|
4
|
+
data.tar.gz: dd730a85ab31d2b181b875d7052e3b109a263caba0618d869e262e3004e6d52f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86b7d2a84fccf9d800ae9c121773d3ae960b6f2c32100466836d6f07056ad2f2affa4fdbe8df8c91ea5cc208d7e006db993ddbce86c30b9652077b87f22cd4c5
|
7
|
+
data.tar.gz: 95a83f6b58874de8b27edb08811ea9543ebe7036550364d535f1a73042af90d77e36a891ccfcab9db36817572e6a400c203356cc54fb929358730bfc8425999e
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.8.1
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequel::Plugins::TyperoAttributes
|
4
|
+
module ClassMethods
|
5
|
+
def typero
|
6
|
+
Typero.new self
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
# calling typero! on any object will validate all fields
|
12
|
+
def typero! field_name=nil
|
13
|
+
return unless Typero.defined?(self.class)
|
14
|
+
|
15
|
+
typero = self.class.typero
|
16
|
+
|
17
|
+
typero.validate(self) do |name, err|
|
18
|
+
errors.add(name, err) unless (errors.on(name) || []).include?(err)
|
19
|
+
end
|
20
|
+
|
21
|
+
# this are rules unique to database, so we check them here
|
22
|
+
typero.rules.each do |field, rule|
|
23
|
+
self[field] ||= {} if rule[:type] == 'hash'
|
24
|
+
|
25
|
+
# check uniqe fields
|
26
|
+
if rule[:unique]
|
27
|
+
id = self[:id] || 0
|
28
|
+
value = self[field]
|
29
|
+
|
30
|
+
# we only check if field is changed
|
31
|
+
if value.present? && column_changed?(field) && self.class.xwhere('LOWER(%s)=LOWER(?) and id<>?' % field, value, id).first
|
32
|
+
error = rule[:unique].class == TrueClass ? %[Value '"#{value}"' for #{field} allready exists] : rule[:unique]
|
33
|
+
errors.add(field, error) unless (errors.on(field) || []).include?(error)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# check protected fields
|
38
|
+
if rule[:protected] && self[:id]
|
39
|
+
if column_changed?(field)
|
40
|
+
error = rule[:protected].class == TrueClass ? "value once defined can't be overwritten." : rule[:protected]
|
41
|
+
errors.add(field, error) unless (errors.on(field) || []).include?(error)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# check single field if single field given
|
47
|
+
if field_name
|
48
|
+
raise ArgumentError.new 'Field :%s not found in %s' % [field_name, self] unless self[field_name]
|
49
|
+
return unless errors.on(field_name)
|
50
|
+
|
51
|
+
errors.on(field_name).join(', ')
|
52
|
+
end
|
53
|
+
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate
|
58
|
+
typero!
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module DatasetMethods
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Sequel::Model.plugin :typero_attributes
|
69
|
+
|
data/lib/typero.rb
CHANGED
@@ -1,164 +1,13 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
class Typero
|
12
|
-
attr_reader :rules
|
13
|
-
|
14
|
-
VERSION = File.read File.expand_path '../.version', File.dirname(__FILE__)
|
15
|
-
|
16
|
-
class << self
|
17
|
-
# validate single value in type
|
18
|
-
def validate value, type, opts={}
|
19
|
-
field = type.to_s.tableize.singularize.to_sym
|
20
|
-
|
21
|
-
# we need to have pointer to hash, so value can be changed (coerced) if needed
|
22
|
-
h = { field => value }
|
23
|
-
|
24
|
-
rule = new
|
25
|
-
rule.set field, type, opts
|
26
|
-
|
27
|
-
if error = rule.validate(h)[field]
|
28
|
-
block_given? ? yield(error) : raise(TypeError.new(error))
|
29
|
-
end
|
30
|
-
|
31
|
-
h[field]
|
32
|
-
end
|
33
|
-
|
34
|
-
# Typero.set(:label, 'Foo bar') -> "foo-bar"
|
35
|
-
def set type, value, opts={}
|
36
|
-
klass = 'Typero::%sType' % type.to_s.gsub(/[^\w]/,'').classify
|
37
|
-
check = klass.constantize.new value, opts
|
38
|
-
check.set
|
39
|
-
end
|
40
|
-
|
41
|
-
def cache
|
42
|
-
@@cache ||= {}
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
###
|
47
|
-
|
48
|
-
# accepts dsl block to
|
49
|
-
def initialize hash={}, &block
|
50
|
-
@rules = {}
|
51
|
-
hash.each { |k, v| set(k, v) }
|
52
|
-
instance_exec &block if block
|
53
|
-
end
|
54
|
-
|
55
|
-
# convert
|
56
|
-
# integer :age
|
57
|
-
# set :age, type: :integer
|
58
|
-
# email :email
|
59
|
-
# set :email, [:emails]
|
60
|
-
# email [:emails]
|
61
|
-
def method_missing name, *args, &block
|
62
|
-
field = args.shift
|
63
|
-
|
64
|
-
if field.class == Array
|
65
|
-
field = field.first
|
66
|
-
name = [name]
|
67
|
-
end
|
68
|
-
|
69
|
-
set field, type=name, *args
|
70
|
-
end
|
71
|
-
|
72
|
-
# coerce opts values
|
73
|
-
def parse_option opts
|
74
|
-
opts[:type] ||= 'string'
|
75
|
-
opts[:req] = opts.delete(:required) unless opts[:required].nil?
|
76
|
-
|
77
|
-
if opts[:type].is_a?(Array)
|
78
|
-
opts[:array_type] = opts[:type][0] if opts[:type][0]
|
79
|
-
opts[:type] = 'array'
|
80
|
-
end
|
81
|
-
|
82
|
-
opts[:type] = opts[:type].to_s.downcase
|
83
|
-
|
84
|
-
allowed_names = [:req, :uniq, :protected, :type, :min, :max, :array_type, :default, :downcase, :desc]
|
85
|
-
opts.keys.each do |key|
|
86
|
-
raise ArgumentError.new('%s is not allowed as typero option' % key) unless allowed_names.index(key)
|
87
|
-
end
|
88
|
-
|
89
|
-
opts
|
90
|
-
end
|
91
|
-
|
92
|
-
# used in dsl to define value
|
93
|
-
def set field, type=String, opts={}
|
94
|
-
opts = type.is_a?(Hash) ? type : opts.merge(type: type)
|
95
|
-
|
96
|
-
opts[:type] ||= :string
|
97
|
-
klass = 'Typero::%sType' % opts[:type].to_s.gsub(/[^\w]/,'').classify
|
98
|
-
klass.constantize
|
99
|
-
|
100
|
-
@rules[field] = parse_option opts
|
101
|
-
end
|
102
|
-
|
103
|
-
def safe_type type
|
104
|
-
type.to_s.gsub(/[^\w]/,'').classify
|
105
|
-
end
|
106
|
-
|
107
|
-
# adds error to array or prefixes with field name
|
108
|
-
def add_error field, msg
|
109
|
-
if @errors[field]
|
110
|
-
@errors[field] += ', %s' % msg
|
111
|
-
else
|
112
|
-
field_name = field.to_s.sub(/_id$/,'').humanize
|
113
|
-
@errors[field] = '%s %s' % [field_name, msg]
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# validates any instance object or object with hash variable interface
|
118
|
-
# it also coarces values
|
119
|
-
def validate instance
|
120
|
-
@errors = {}
|
121
|
-
|
122
|
-
@rules.each do |field, opts|
|
123
|
-
# set value to default if value is blank and default given
|
124
|
-
instance[field] = opts[:default] if opts[:default] && instance[field].blank?
|
125
|
-
|
126
|
-
# get field value
|
127
|
-
value = instance[field]
|
128
|
-
|
129
|
-
if value.present?
|
130
|
-
klass = 'Typero::%sType' % safe_type(opts[:type])
|
131
|
-
check = klass.constantize.new value, opts
|
132
|
-
check.value = check.default if check.value.nil?
|
133
|
-
|
134
|
-
unless check.value.nil?
|
135
|
-
begin
|
136
|
-
check.set
|
137
|
-
check.validate
|
138
|
-
instance[field] = check.value
|
139
|
-
rescue TypeError => e
|
140
|
-
add_error field, e.message
|
141
|
-
end
|
142
|
-
end
|
143
|
-
elsif opts[:req]
|
144
|
-
msg = opts[:req].class == TrueClass ? 'is required' : opts[:req]
|
145
|
-
add_error field, msg
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
if @errors.keys.length > 0 && block_given?
|
150
|
-
@errors.each { |k,v| yield(k, v) }
|
151
|
-
end
|
152
|
-
|
153
|
-
@errors
|
154
|
-
end
|
155
|
-
|
156
|
-
def valid? instance
|
157
|
-
errors = validate instance
|
158
|
-
errors.keys.length == 0
|
159
|
-
end
|
1
|
+
# base libs
|
2
|
+
require_relative 'typero/typero'
|
3
|
+
require_relative 'typero/func'
|
4
|
+
require_relative 'typero/params'
|
5
|
+
require_relative 'typero/type/type'
|
6
|
+
|
7
|
+
# checker types
|
8
|
+
Dir['%s/typero/type/types/*.rb' % __dir__].each do |file|
|
9
|
+
require file
|
160
10
|
end
|
161
11
|
|
162
|
-
|
163
|
-
|
164
|
-
Dir['%s/typero/type/*.rb' % File.dirname(__FILE__)].each { |file| require file }
|
12
|
+
# load Sequel adapter is Sequel is available
|
13
|
+
require_relative './adapters/sequel' if defined?(Sequel)
|
data/lib/typero/func.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
class Typero
|
2
|
+
SCHEMAS = {}
|
3
|
+
end
|
4
|
+
|
5
|
+
def Typero name=nil, &block
|
6
|
+
if block_given?
|
7
|
+
Typero.new(&block).tap do |schema|
|
8
|
+
Typero::SCHEMAS[name] = schema if name
|
9
|
+
end
|
10
|
+
else
|
11
|
+
raise ArgumentErorr.new('Schema nema not given') unless name
|
12
|
+
Typero::SCHEMAS[name] || raise(ArgumentErorr.new('Typero schema "%s" not defined' % name))
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# Base class for schema validation
|
2
|
+
|
3
|
+
class Typero
|
4
|
+
class Params
|
5
|
+
ALLOWED = %i(name min max default allowed delimiter max_count req required type array meta desc description duplicates unique)
|
6
|
+
|
7
|
+
attr_reader :rules, :db_rules
|
8
|
+
|
9
|
+
def initialize &block
|
10
|
+
@db_rules = []
|
11
|
+
@rules = {}
|
12
|
+
instance_exec &block
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# used in dsl to define schema field options
|
18
|
+
def set field, *args
|
19
|
+
raise "Field name not given (Typero)" unless field
|
20
|
+
|
21
|
+
if args.first.is_a?(Hash)
|
22
|
+
opts = args.first || {}
|
23
|
+
else
|
24
|
+
opts = args[1] || {}
|
25
|
+
opts[:type] ||= args[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
opts[:type] ||= :string
|
29
|
+
opts[:required] = true unless opts[:required].is_a?(FalseClass) || opts[:req].is_a?(FalseClass)
|
30
|
+
|
31
|
+
field = field.to_s
|
32
|
+
|
33
|
+
# name? - opional name
|
34
|
+
if field.include?('?')
|
35
|
+
field = field.sub('?', '')
|
36
|
+
opts[:required] = false
|
37
|
+
end
|
38
|
+
|
39
|
+
# array that allows duplicates
|
40
|
+
if opts[:type].is_a?(Array)
|
41
|
+
opts[:type] = opts[:type].first
|
42
|
+
opts[:array] = true
|
43
|
+
end
|
44
|
+
|
45
|
+
# no duplicates array
|
46
|
+
if opts[:type].is_a?(Set)
|
47
|
+
opts[:type] = opts[:type].to_a.first
|
48
|
+
opts[:array] = true
|
49
|
+
end
|
50
|
+
|
51
|
+
opts[:type] ||= 'string'
|
52
|
+
opts[:type] = opts[:type].to_s.downcase
|
53
|
+
|
54
|
+
opts[:description] = opts.delete(:desc) unless opts[:desc].nil?
|
55
|
+
|
56
|
+
# chek alloed params, all optional should go in meta
|
57
|
+
result = opts.keys - ALLOWED
|
58
|
+
raise ArgumentError.new('Unallowed Type params found: %s, allowed: %s' % [result.join(', '), ALLOWED]) if result.length > 0
|
59
|
+
|
60
|
+
field = field.to_sym
|
61
|
+
|
62
|
+
db :add_index, field if opts.delete(:index)
|
63
|
+
|
64
|
+
klass = Typero::Type.load opts[:type]
|
65
|
+
@rules[field] = opts
|
66
|
+
end
|
67
|
+
|
68
|
+
# pass values for db_schema only
|
69
|
+
# db :timestamps
|
70
|
+
# db :add_index, :code -> t.add_index :code
|
71
|
+
def db *args
|
72
|
+
@db_rules.push args
|
73
|
+
end
|
74
|
+
|
75
|
+
# set :age, type: :integer -> integer :age
|
76
|
+
# email :email
|
77
|
+
#
|
78
|
+
# set :emails, Array[:email]
|
79
|
+
# email Array[:emails]
|
80
|
+
def method_missing field, *args, &block
|
81
|
+
set field, *args
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Master class
|
2
|
+
|
3
|
+
class Typero
|
4
|
+
class Type
|
5
|
+
ERRORS = {
|
6
|
+
en: {
|
7
|
+
# errors shared between various types
|
8
|
+
min_length_error: 'min lenght is %s, you have %s',
|
9
|
+
max_length_error: 'max lenght is %s, you have %s',
|
10
|
+
min_value_error: 'min is %s, got %s',
|
11
|
+
max_value_error: 'max is %s, got %s',
|
12
|
+
unallowed_characters_error: 'is having unallowed characters'
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
OPTS = {}
|
17
|
+
|
18
|
+
attr_accessor :opts
|
19
|
+
attr_accessor :value
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def load name
|
23
|
+
klass = 'Typero::%sType' % name.to_s.gsub(/[^\w]/,'').classify
|
24
|
+
|
25
|
+
if const_defined? klass
|
26
|
+
klass.constantize
|
27
|
+
else
|
28
|
+
raise ArgumentError, 'Typero type "%s" is not defined (%s)' % [name, klass]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def error locale, key, message
|
33
|
+
locale = locale.to_sym
|
34
|
+
ERRORS[locale] ||= {}
|
35
|
+
ERRORS[locale][key.to_sym] = message
|
36
|
+
end
|
37
|
+
|
38
|
+
def opts key, desc
|
39
|
+
OPTS[self] ||= {}
|
40
|
+
OPTS[self][key] = desc
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
###
|
45
|
+
|
46
|
+
def initialize value, opts={}
|
47
|
+
@value = value
|
48
|
+
@opts = opts
|
49
|
+
end
|
50
|
+
|
51
|
+
# default validation for any type
|
52
|
+
def validate
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def default
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# get error from option or the default one
|
63
|
+
def error_for name, *args
|
64
|
+
locale =
|
65
|
+
if defined?(Lux)
|
66
|
+
Lux.current.locale.to_s
|
67
|
+
elsif defined?(I18n)
|
68
|
+
I18n.locale
|
69
|
+
end
|
70
|
+
|
71
|
+
locale = :en if locale.to_s == ''
|
72
|
+
pointer = ERRORS[locale.to_sym] || ERRORS[:en]
|
73
|
+
error = @opts.dig(:meta, locale, name) || @opts.dig(:meta, name) || pointer[name]
|
74
|
+
error = error % args if args.first
|
75
|
+
|
76
|
+
raise 'Type error :%s not defined' % name unless error
|
77
|
+
raise TypeError.new(error)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# you should not use this filed for currency calculations
|
2
|
+
|
3
|
+
require_relative './float'
|
4
|
+
|
5
|
+
class Typero::CurrencyType < Typero::FloatType
|
6
|
+
|
7
|
+
def set
|
8
|
+
@value = @value.to_f.round(2)
|
9
|
+
end
|
10
|
+
|
11
|
+
def db_field
|
12
|
+
opts = {}
|
13
|
+
opts[:precision] = 8
|
14
|
+
opts[:scale] = 2
|
15
|
+
[:decimal, opts]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Typero::EmailType < Typero::Type
|
2
|
+
error :en, :not_8_chars_error, 'is not having at least 8 characters'
|
3
|
+
error :en, :missing_monkey_error, 'is missing @'
|
4
|
+
|
5
|
+
def set
|
6
|
+
@value = @value.downcase.gsub(/\s+/,'+')
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate
|
10
|
+
error_for(:not_8_chars_error) unless @value.to_s.length > 7
|
11
|
+
error_for(:missing_monkey_error) unless @value.include?('@')
|
12
|
+
end
|
13
|
+
|
14
|
+
def db_field
|
15
|
+
opts = {}
|
16
|
+
opts[:limit] = @opts[:max] || 120
|
17
|
+
opts[:null] = false if @opts[:required]
|
18
|
+
[:string, opts]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Typero::FloatType < Typero::Type
|
2
|
+
opts :min, 'Minimum value'
|
3
|
+
opts :max, 'Maximun value'
|
4
|
+
|
5
|
+
def set
|
6
|
+
@value = @value.to_f
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate
|
10
|
+
error_for(:min_length_error, @opts[:min], @value) if @opts[:min] && value < @opts[:min]
|
11
|
+
error_for(:max_length_error, @opts[:max], @value) if @opts[:max] && value > @opts[:max]
|
12
|
+
end
|
13
|
+
|
14
|
+
def db_field
|
15
|
+
opts = {}
|
16
|
+
opts[:null] = false if @opts[:required]
|
17
|
+
[:float, opts]
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Typero::HashType < Typero::Type
|
2
|
+
error :en, :not_hash_type_error, 'value is not hash type'
|
3
|
+
|
4
|
+
def default
|
5
|
+
{}
|
6
|
+
end
|
7
|
+
|
8
|
+
def set
|
9
|
+
@value = @value.to_h
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate
|
13
|
+
error_for(:not_hash_type_error) unless @value.is_a?(Hash)
|
14
|
+
end
|
15
|
+
|
16
|
+
def db_field
|
17
|
+
[:jsonb, {
|
18
|
+
null: false
|
19
|
+
}]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Typero::ImageType < Typero::Type
|
2
|
+
FORMATS = %w[jpg jpeg gif png svg webp]
|
3
|
+
|
4
|
+
error :en, :image_not_starting_error, 'URL is not starting with http'
|
5
|
+
error :en, :image_not_image_format, 'URL is not ending with %s' % FORMATS.join(', ')
|
6
|
+
|
7
|
+
opts :strict, 'Force image to have known extension (%s)' % FORMATS.join(', ')
|
8
|
+
|
9
|
+
def set
|
10
|
+
@value = 'https://%s' % @value unless @value.include?('://')
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate
|
14
|
+
error_for(:image_not_starting_error) unless @value =~ /^https?:\/\/./
|
15
|
+
|
16
|
+
if opts[:strict]
|
17
|
+
ext = @value.split('.').last.downcase
|
18
|
+
error_for(:image_not_image_format) unless FORMATS.include?(ext)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def db_field
|
23
|
+
opts = {}
|
24
|
+
opts[:null] = false if @opts[:required]
|
25
|
+
[:string, opts]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Typero::IntegerType < Typero::Type
|
2
|
+
opts :min, 'Minimum value'
|
3
|
+
opts :max, 'Maximun value'
|
4
|
+
|
5
|
+
def set
|
6
|
+
@value = @value.to_i
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate
|
10
|
+
error_for(:min_value_error, @opts[:min], @value) if @opts[:min] && @value < @opts[:min]
|
11
|
+
error_for(:max_value_error, @opts[:max], @value) if @opts[:max] && @value > @opts[:max]
|
12
|
+
end
|
13
|
+
|
14
|
+
def db_field
|
15
|
+
opts = {}
|
16
|
+
opts[:null] = false if @opts[:required]
|
17
|
+
opts[:default] = @opts[:default]
|
18
|
+
[:integer, opts]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -4,6 +4,12 @@ class Typero::LabelType < Typero::Type
|
|
4
4
|
end
|
5
5
|
|
6
6
|
def validate
|
7
|
-
|
7
|
+
error_for(:unallowed_characters_error) unless @value =~ /^[\w\-]+$/
|
8
|
+
end
|
9
|
+
|
10
|
+
def db_field
|
11
|
+
opts = {}
|
12
|
+
opts[:limit] = 30
|
13
|
+
[:string, opts]
|
8
14
|
end
|
9
15
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class Typero::OibType < Typero::Type
|
2
|
+
error :en, :not_an_oib_error, 'not in an OIB format'
|
3
|
+
|
4
|
+
def set
|
5
|
+
@value = check?(@value) ? @value.to_i : nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def validate
|
9
|
+
error_for(:not_an_oib_error) unless check?(@value)
|
10
|
+
end
|
11
|
+
|
12
|
+
def db_field
|
13
|
+
opts = {}
|
14
|
+
opts[:null] = false if @opts[:required]
|
15
|
+
opts[:limit] = 11
|
16
|
+
[:string, opts]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# http://domagoj.eu/oib/
|
22
|
+
def check? oib
|
23
|
+
oib = oib.to_s
|
24
|
+
|
25
|
+
return false unless oib.match(/^[0-9]{11}$/)
|
26
|
+
|
27
|
+
control_sum = (0..9).inject(10) do |middle, position|
|
28
|
+
middle += oib.at(position).to_i
|
29
|
+
middle %= 10
|
30
|
+
middle = 10 if middle == 0
|
31
|
+
middle *= 2
|
32
|
+
middle %= 11
|
33
|
+
end
|
34
|
+
|
35
|
+
control_sum = 11 - control_sum
|
36
|
+
control_sum = 0 if control_sum == 10
|
37
|
+
|
38
|
+
return control_sum == oib.at(10).to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# for postgres - select st_asewkt(lon_lat) || st_astext(lon_lat)
|
2
|
+
# point = @object.class.xselect("ST_AsText(#{field}) as #{field}").where(id: @object.id).first[field.to_sym]
|
3
|
+
|
4
|
+
class Typero::PointType < Typero::Type
|
5
|
+
def set
|
6
|
+
if @value.present?
|
7
|
+
if @value.include?('/@')
|
8
|
+
point = @value.split('/@', 2).last.split(',')
|
9
|
+
@value = [point[0], point[1]].join(',')
|
10
|
+
end
|
11
|
+
|
12
|
+
if !@value.include?('POINT') && @value.include?(',')
|
13
|
+
point = @value.sub(/,\s*/, ' ')
|
14
|
+
@value = 'SRID=4326;POINT(%s)' % point
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate
|
20
|
+
if @value && @value.include?(',') && !@value =~ /^SRID=4326;POINT\(/
|
21
|
+
error_for(:unallowed_characters_error)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def db_field
|
26
|
+
[:geography, {}]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Typero::StringType < Typero::Type
|
2
|
+
opts :min, 'Minimun string length'
|
3
|
+
opts :max, 'Maximun string length'
|
4
|
+
|
5
|
+
def set
|
6
|
+
@value = @value.to_s unless @value.is_a?(String)
|
7
|
+
@value = @value.downcase if @opts[:downcase]
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate
|
11
|
+
error_for(:min_length_error, @opts[:min], @value.length) if @opts[:min] && @value.length < @opts[:min]
|
12
|
+
error_for(:max_length_error, @opts[:max], @value.length) if @opts[:max] && @value.length > @opts[:max]
|
13
|
+
end
|
14
|
+
|
15
|
+
def db_field
|
16
|
+
opts = {}
|
17
|
+
opts[:limit] = @opts[:max] || 255
|
18
|
+
opts[:null] = false if @opts[:required]
|
19
|
+
opts[:default] = @opts[:default]
|
20
|
+
[:string, opts]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Typero::UrlType < Typero::Type
|
2
|
+
error :en, :url_not_starting_error, 'URL is not starting with http'
|
3
|
+
|
4
|
+
def set
|
5
|
+
@value = 'http://%s' % @value unless @value.include?('://')
|
6
|
+
end
|
7
|
+
|
8
|
+
def validate
|
9
|
+
error_for(:url_not_starting_error) unless @value =~ /^https?:\/\/./
|
10
|
+
end
|
11
|
+
|
12
|
+
def db_field
|
13
|
+
opts = {}
|
14
|
+
opts[:null] = false if @opts[:required]
|
15
|
+
[:string, opts]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# rules = Typero.new do
|
2
|
+
# set :name, String, req: true
|
3
|
+
# set :email, :email, req: true
|
4
|
+
# set :skills, [:email], min: 2
|
5
|
+
# end
|
6
|
+
#
|
7
|
+
# or
|
8
|
+
#
|
9
|
+
# rules = Typero.new do
|
10
|
+
# string :name, req: true
|
11
|
+
# email :email, req: true
|
12
|
+
# email [:skills], min: 2
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# errors = rules.validate @object
|
16
|
+
# rules.valid?
|
17
|
+
# rules.validate(@object) { |errors| ... }
|
18
|
+
|
19
|
+
class Typero
|
20
|
+
VERSION = File.read File.expand_path "../../.version", File.dirname(__FILE__)
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# check and coerce value
|
24
|
+
# Typero.set(:label, 'Foo bar') -> "foo-bar"
|
25
|
+
def set type, value, opts = {}, &block
|
26
|
+
check = Typero::Type.load(type).new value, opts
|
27
|
+
check.set
|
28
|
+
check.validate
|
29
|
+
check.value
|
30
|
+
rescue TypeError => error
|
31
|
+
if block
|
32
|
+
block.call error
|
33
|
+
false
|
34
|
+
else
|
35
|
+
raise error
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
###
|
41
|
+
|
42
|
+
# accepts dsl block to
|
43
|
+
def initialize &block
|
44
|
+
raise "Params not defined" unless block_given?
|
45
|
+
@schema = Params.new &block
|
46
|
+
end
|
47
|
+
|
48
|
+
# validates any instance object with hash variable interface
|
49
|
+
# it also coarces values
|
50
|
+
def validate object
|
51
|
+
@object = object
|
52
|
+
@errors = {}
|
53
|
+
|
54
|
+
@schema.rules.each do |field, opts|
|
55
|
+
# set value to default if value is blank and default given
|
56
|
+
@object[field] = opts[:default] if opts[:default] && @object[field].blank?
|
57
|
+
|
58
|
+
# get field value
|
59
|
+
value = @object[field]
|
60
|
+
|
61
|
+
if opts[:array]
|
62
|
+
unless value.is_a?(Array)
|
63
|
+
opts[:delimiter] ||= /\s*,\s*/
|
64
|
+
value = value.to_s.split(opts[:delimiter])
|
65
|
+
end
|
66
|
+
|
67
|
+
value = value
|
68
|
+
.map { |el| check_filed_value field, el, opts }
|
69
|
+
.map { |el| el.to_s == '' ? nil : el }
|
70
|
+
.compact
|
71
|
+
|
72
|
+
value = Set.new(value).to_a unless opts[:duplicates]
|
73
|
+
|
74
|
+
opts[:max_count] ||= 100
|
75
|
+
add_error(field, 'Max number of array elements is %d, you have %d' % [opts[:max_count], value.length], opts) if value.length > opts[:max_count]
|
76
|
+
|
77
|
+
check_required field, value.first, opts
|
78
|
+
else
|
79
|
+
value = check_filed_value field, value, opts
|
80
|
+
check_required field, value, opts
|
81
|
+
end
|
82
|
+
|
83
|
+
# if value is not list of allowed values, raise error
|
84
|
+
if opts[:allowed] && !opts[:values].include?(value)
|
85
|
+
add_error field, 'Value "%s" is not allowed' % value, opts
|
86
|
+
end
|
87
|
+
|
88
|
+
# present empty string values as nil
|
89
|
+
@object[field] = value.to_s.sub(/\s+/, '') == '' ? nil : value
|
90
|
+
end
|
91
|
+
|
92
|
+
if @errors.keys.length > 0 && block_given?
|
93
|
+
@errors.each { |k, v| yield(k, v) }
|
94
|
+
end
|
95
|
+
|
96
|
+
@errors
|
97
|
+
end
|
98
|
+
|
99
|
+
def valid? object
|
100
|
+
errors = validate object
|
101
|
+
errors.keys.length == 0
|
102
|
+
end
|
103
|
+
|
104
|
+
# returns field, db_type, db_opts
|
105
|
+
def db_schema
|
106
|
+
out = @schema.rules.inject([]) do |total, (field, opts)|
|
107
|
+
# get db filed schema
|
108
|
+
type, opts = Typero::Type.load(opts[:type]).new(nil, opts).db_field
|
109
|
+
|
110
|
+
# add array true to field it ont defined in schema
|
111
|
+
schema_opts = @schema.rules[field]
|
112
|
+
opts[:array] = true if schema_opts[:array]
|
113
|
+
|
114
|
+
total << [type, field, opts]
|
115
|
+
end
|
116
|
+
|
117
|
+
out += @schema.db_rules
|
118
|
+
|
119
|
+
out
|
120
|
+
end
|
121
|
+
|
122
|
+
# iterate trough all the ruels via block interface
|
123
|
+
# schema.rules do |field, opts|
|
124
|
+
# schema.rules(:url) do |field, opts|
|
125
|
+
def rules(filter = nil, &block)
|
126
|
+
return @schema.rules unless filter
|
127
|
+
out = @schema.rules
|
128
|
+
out = out.select { |k, v| v[:type].to_s == filter.to_s || v[:array_type].to_s == filter.to_s } if filter
|
129
|
+
return out unless block_given?
|
130
|
+
|
131
|
+
out.each { |k, v| yield k, v }
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# adds error to array or prefixes with field name
|
137
|
+
def add_error field, msg, opts
|
138
|
+
if @errors[field]
|
139
|
+
@errors[field] += ", %s" % msg
|
140
|
+
else
|
141
|
+
if msg && msg[0, 1].downcase == msg[0, 1]
|
142
|
+
field_name = opts[:name] || field.to_s.sub(/_id$/, "").humanize
|
143
|
+
msg = "%s %s" % [field_name, msg]
|
144
|
+
end
|
145
|
+
|
146
|
+
@errors[field] = msg
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def safe_type type
|
151
|
+
type.to_s.gsub(/[^\w]/, "").classify
|
152
|
+
end
|
153
|
+
|
154
|
+
def check_required field, value, opts
|
155
|
+
return if !opts[:required] || value
|
156
|
+
msg = opts[:required].class == TrueClass ? "is required" : opts[:required]
|
157
|
+
add_error field, msg, opts
|
158
|
+
end
|
159
|
+
|
160
|
+
def check_filed_value field, value, opts
|
161
|
+
return unless value
|
162
|
+
|
163
|
+
klass = "Typero::%sType" % safe_type(opts[:type])
|
164
|
+
check = klass.constantize.new value, opts
|
165
|
+
check.value = check.default if check.value.nil?
|
166
|
+
|
167
|
+
unless check.value.nil?
|
168
|
+
begin
|
169
|
+
check.set
|
170
|
+
check.validate
|
171
|
+
check.value
|
172
|
+
rescue TypeError => e
|
173
|
+
add_error field, e.message, opts
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typero
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dino Reic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-05-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fast_blank
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '0'
|
27
27
|
description: Simple and fast ruby type system. Enforce types as Array, Email, Boolean
|
28
28
|
for ruby class instances
|
29
29
|
email: reic.dino@gmail.com
|
@@ -32,18 +32,27 @@ extensions: []
|
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
34
|
- "./.version"
|
35
|
+
- "./lib/adapters/sequel.rb"
|
35
36
|
- "./lib/typero.rb"
|
36
|
-
- "./lib/typero/
|
37
|
-
- "./lib/typero/
|
38
|
-
- "./lib/typero/type/
|
39
|
-
- "./lib/typero/type/
|
40
|
-
- "./lib/typero/type/
|
41
|
-
- "./lib/typero/type/
|
42
|
-
- "./lib/typero/type/
|
43
|
-
- "./lib/typero/type/
|
44
|
-
- "./lib/typero/type/
|
45
|
-
- "./lib/typero/type/
|
46
|
-
- "./lib/typero/type/
|
37
|
+
- "./lib/typero/func.rb"
|
38
|
+
- "./lib/typero/params.rb"
|
39
|
+
- "./lib/typero/type/type.rb"
|
40
|
+
- "./lib/typero/type/types/boolean.rb"
|
41
|
+
- "./lib/typero/type/types/currency.rb"
|
42
|
+
- "./lib/typero/type/types/date.rb"
|
43
|
+
- "./lib/typero/type/types/datetime.rb"
|
44
|
+
- "./lib/typero/type/types/email.rb"
|
45
|
+
- "./lib/typero/type/types/float.rb"
|
46
|
+
- "./lib/typero/type/types/hash.rb"
|
47
|
+
- "./lib/typero/type/types/image.rb"
|
48
|
+
- "./lib/typero/type/types/integer.rb"
|
49
|
+
- "./lib/typero/type/types/label.rb"
|
50
|
+
- "./lib/typero/type/types/oib.rb"
|
51
|
+
- "./lib/typero/type/types/point.rb"
|
52
|
+
- "./lib/typero/type/types/string.rb"
|
53
|
+
- "./lib/typero/type/types/text.rb"
|
54
|
+
- "./lib/typero/type/types/url.rb"
|
55
|
+
- "./lib/typero/typero.rb"
|
47
56
|
homepage: https://github.com/dux/typero
|
48
57
|
licenses:
|
49
58
|
- MIT
|
@@ -63,8 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
72
|
- !ruby/object:Gem::Version
|
64
73
|
version: '0'
|
65
74
|
requirements: []
|
66
|
-
|
67
|
-
rubygems_version: 2.7.5
|
75
|
+
rubygems_version: 3.0.6
|
68
76
|
signing_key:
|
69
77
|
specification_version: 4
|
70
78
|
summary: Ruby type system
|
data/lib/typero/type.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
class Typero::Type
|
2
|
-
attr_accessor :opts
|
3
|
-
attr_accessor :value
|
4
|
-
|
5
|
-
def initialize(value, opts={})
|
6
|
-
@value = value
|
7
|
-
@opts = opts
|
8
|
-
end
|
9
|
-
|
10
|
-
# default validation for any type
|
11
|
-
def validate(what)
|
12
|
-
true
|
13
|
-
end
|
14
|
-
|
15
|
-
def get
|
16
|
-
@value
|
17
|
-
end
|
18
|
-
|
19
|
-
def set
|
20
|
-
@value
|
21
|
-
end
|
22
|
-
|
23
|
-
def default
|
24
|
-
nil
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
|
data/lib/typero/type/array.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
class Typero::ArrayType < Typero::Type
|
2
|
-
def default
|
3
|
-
[]
|
4
|
-
end
|
5
|
-
|
6
|
-
def set
|
7
|
-
unless @value.class.to_s.index('Array')
|
8
|
-
@value = @value.to_s.sub(/^\{/,'').sub(/\}$/,'').split(/\s*,\s*/)
|
9
|
-
end
|
10
|
-
|
11
|
-
@value.uniq!
|
12
|
-
@value.compact!
|
13
|
-
|
14
|
-
if type = @opts[:array_type]
|
15
|
-
@value.map! { |el|
|
16
|
-
Typero.validate(el, type) { |msg|
|
17
|
-
raise TypeError.new "'%s' %s (value in list)" % [el, msg]
|
18
|
-
}
|
19
|
-
}
|
20
|
-
end
|
21
|
-
|
22
|
-
# this converts Sequel::Postgres::PGArray to Array and fixes many problems
|
23
|
-
@value = @value.to_a if @value.class != Array
|
24
|
-
end
|
25
|
-
|
26
|
-
def validate
|
27
|
-
raise TypeError, 'Min array lenght is %s elements' % @opts[:min] if @opts[:min] && @value.length < @opts[:min]
|
28
|
-
raise TypeError, 'Max array lenght is %s elements' % @opts[:max] if @opts[:max] && @value.length > @opts[:max]
|
29
|
-
true
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
data/lib/typero/type/email.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
class Typero::EmailType < Typero::Type
|
2
|
-
|
3
|
-
def set
|
4
|
-
@value = @value.downcase.gsub(/\s+/,'+')
|
5
|
-
end
|
6
|
-
|
7
|
-
def validate
|
8
|
-
raise TypeError, 'is not having at least 8 characters' unless @value.to_s.length > 7
|
9
|
-
raise TypeError, 'is missing @' unless @value.include?('@')
|
10
|
-
# raise TypeError, "[#{@value}] is in wrong format" unless @value =~ /^[\+\w\-\.]+\@[\w\-\.]+$/i
|
11
|
-
end
|
12
|
-
|
13
|
-
end
|
14
|
-
|
data/lib/typero/type/float.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
class Typero::FloatType < Typero::Type
|
2
|
-
|
3
|
-
def set
|
4
|
-
@value = @value.to_f
|
5
|
-
end
|
6
|
-
|
7
|
-
def validate
|
8
|
-
raise TypeError, "min lenght is #{@opts[:min]}" if @opts[:min] && value < @opts[:min]
|
9
|
-
raise TypeError, "max lenght is #{@opts[:max]}" if @opts[:max] && value > @opts[:max]
|
10
|
-
end
|
11
|
-
|
12
|
-
end
|
13
|
-
|
data/lib/typero/type/hash.rb
DELETED
data/lib/typero/type/integer.rb
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
class Typero::IntegerType < Typero::Type
|
2
|
-
def set
|
3
|
-
@value = @value.to_i
|
4
|
-
end
|
5
|
-
|
6
|
-
def validate
|
7
|
-
raise TypeError, 'min is %s, got %s' % [@opts[:min], @value] if @opts[:min] && @value < @opts[:min]
|
8
|
-
raise TypeError, 'max is %s, got %s' % [@opts[:max], @value] if @opts[:max] && @value > @opts[:max]
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
data/lib/typero/type/oib.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
class Typero::OibType < Typero::Type
|
2
|
-
def check? oib
|
3
|
-
oib = oib.to_s
|
4
|
-
|
5
|
-
return false if (oib =~ /\d{11}/) != 0
|
6
|
-
return false if oib.length != 11
|
7
|
-
|
8
|
-
a = 10
|
9
|
-
(0..9).each do |i|
|
10
|
-
a = (a + oib[i,1].to_i) % 10
|
11
|
-
a = 10 if a == 0
|
12
|
-
a = (a * 2) % 11
|
13
|
-
end
|
14
|
-
|
15
|
-
kontrolna = 11 - a
|
16
|
-
kontrolna = 0 if kontrolna == 10
|
17
|
-
|
18
|
-
kontrolna == oib[10,1].to_i
|
19
|
-
end
|
20
|
-
|
21
|
-
def set
|
22
|
-
@value = check?(@value) ? @value.to_i : nil
|
23
|
-
end
|
24
|
-
|
25
|
-
def validate
|
26
|
-
raise TypeError.new('Not an OIB') unless check?(@value)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
data/lib/typero/type/string.rb
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
class Typero::StringType < Typero::Type
|
2
|
-
def set
|
3
|
-
@value = @value.to_s unless @value.is_a?(String)
|
4
|
-
@value = @value.downcase if @opts[:downcase]
|
5
|
-
end
|
6
|
-
|
7
|
-
def validate
|
8
|
-
raise TypeError, 'min lenght is %s, you have %s' % [@opts[:min], @value.length] if @opts[:min] && @value.length < @opts[:min]
|
9
|
-
raise TypeError, 'max lenght is %s, you have %s' % [@opts[:max], @value.length] if @opts[:max] && @value.length > @opts[:max]
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
data/lib/typero/type/url.rb
DELETED