typero 0.9.6 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54d18d3e66cf73fd0d6f6c42f319cb744fb3cc3f5e11e5e4a24b961c36792a34
4
- data.tar.gz: 65f87c041f1a498c82d707d897d93eab7294a0d373a4d9aca5ec9aefe5da27b7
3
+ metadata.gz: 988ca72b1b08538ccf6bf49e2fcd449a97b4b403bc8306e6d9c1242df6ae0a54
4
+ data.tar.gz: dd90e591bc0b9790e60656e8f6a342b033ab2adca4d80b80a6a1e702878f895e
5
5
  SHA512:
6
- metadata.gz: 1a63e1a470f0e21162f3c11e40fe4977b4432bb30d3f154330840f344b1dc91c49dc453213139b8e5299f4afe587ae3bf6427fa0aabedd0cc0910188e6463ca5
7
- data.tar.gz: 0dd2e24597fa25a0543b851fa3218fc69e129fa671c3ed52edf6110994876c14e3ceb88002ebfec130782722b51dee184cf30dbb0012384da9eba7654d8db24c
6
+ metadata.gz: 8940ebb5034757bb9c4a285b0e6e74bc65131ab5bdebdd9154b0643843836c5cab7c2a98b988361181bea0cf38d461f40662bf9dda926d074bf7b5756f1d6312
7
+ data.tar.gz: e77d16947abb2762bc33ed5c5ba855da56924f2187bb8d31c067d614561cd3f44a07730f8fb858882baa0876ef55da9529a495695454a58bf5c2fe205b81e1f7
data/.version CHANGED
@@ -1 +1 @@
1
- 0.9.6
1
+ 0.10.0
@@ -2,8 +2,16 @@
2
2
 
3
3
  module Sequel::Plugins::Typero
4
4
  module ClassMethods
5
- def typero
6
- Typero.schema self
5
+ def schema name = nil, &block
6
+ name ||= self
7
+ name = name.to_s.underscore.singularize
8
+ value = Typero.schema name, type: :model, &block
9
+
10
+ if ENV['DB_MIGRATE'] == 'true' && defined?(AutoMigrate)
11
+ AutoMigrate.apply_schema self
12
+ end
13
+
14
+ value
7
15
  end
8
16
  end
9
17
 
@@ -12,31 +20,32 @@ module Sequel::Plugins::Typero
12
20
  def validate
13
21
  super
14
22
 
15
- schema = Typero.schema(self.class) || return
16
-
17
- schema.validate(self) do |name, err|
18
- errors.add(name, err) unless (errors.on(name) || []).include?(err)
19
- end
23
+ # Typero schema check
24
+ if schema = Typero.schema?(self.class)
25
+ schema.validate(self) do |name, err|
26
+ errors.add(name, err) unless (errors.on(name) || []).include?(err)
27
+ end
20
28
 
21
- # this are rules unique to database, so we check them here
22
- schema.rules.each do |field, rule|
23
- # check uniqe fields
24
- if unique = rule.dig(:meta, :unique)
25
- id = self[:id] || 0
26
- value = self[field]
27
-
28
- # we only check if field is changed
29
- if value.present? && column_changed?(field) && self.class.xwhere('LOWER(%s)=LOWER(?) and id<>?' % field, value, id).first
30
- error = unique.class == TrueClass ? %[Value "#{value}" for field "#{field}" has been already used, please chose another value.] : unique
31
- errors.add(field, error) unless (errors.on(field) || []).include?(error)
29
+ # this are rules unique to database, so we check them here
30
+ schema.rules.each do |field, rule|
31
+ # check uniqe fields
32
+ if unique = rule.dig(:meta, :unique)
33
+ id = self[:id] || 0
34
+ value = self[field]
35
+
36
+ # we only check if field is changed
37
+ if value.present? && column_changed?(field) && self.class.xwhere("LOWER(%s)=LOWER(?) and #{respond_to?(:ref) ? :ref : :id}::text<>?" % [field], value, id.to_s).first
38
+ error = unique.class == TrueClass ? %[Value "#{value}" for field "#{field}" has been already used, please chose another value.] : unique
39
+ errors.add(field, error) unless (errors.on(field) || []).include?(error)
40
+ end
32
41
  end
33
- end
34
42
 
35
- # check protected fields
36
- if prot = rule.dig(:meta, :protected) && self[:id]
37
- if column_changed?(field)
38
- error = prot.class == TrueClass ? "value once defined can't be overwritten." : prot
39
- errors.add(field, error) unless (errors.on(field) || []).include?(error)
43
+ # check protected fields
44
+ if (prot = rule.dig(:meta, :protected)) && self[:id]
45
+ if column_changed?(field)
46
+ error = prot.class == TrueClass ? "value once defined can't be overwritten." : prot
47
+ errors.add(field, error) unless (errors.on(field) || []).include?(error)
48
+ end
40
49
  end
41
50
  end
42
51
  end
@@ -1,14 +1,17 @@
1
- # Base class for schema validation.
2
- # Accepts set of params and returns hash of porsed rules
1
+ # Base class for schema definition DSL.
2
+ # Accepts a block of field declarations and returns hash of parsed rules.
3
+
4
+ require 'set'
5
+ require 'json'
3
6
 
4
7
  module Typero
5
- class Params
8
+ class Define
6
9
  attr_reader :db_rules
7
10
 
8
- def initialize &block
11
+ def initialize rules = nil, &block
9
12
  @db_rules = []
10
- @rules = {}
11
- instance_exec &block
13
+ @rules = rules || {}
14
+ instance_exec &block if block
12
15
  end
13
16
 
14
17
  def rules
@@ -21,6 +24,52 @@ module Typero
21
24
  def set field, *args, &block
22
25
  raise "Field name not given (Typero)" unless field
23
26
 
27
+ opts = parse_args args
28
+ field = field.to_s
29
+
30
+ # bang suffix defines block type for all fields in the block
31
+ if field.include?('!')
32
+ return define_block_type(field, &block)
33
+ end
34
+
35
+ # question mark suffix makes field optional
36
+ field = parse_field_name field, opts
37
+
38
+ resolve_type opts
39
+
40
+ # inline block defines a nested model schema
41
+ if block_given?
42
+ opts[:type] = :model
43
+ opts[:model] = Typero.schema &block
44
+ end
45
+
46
+ opts[:type] = opts[:type].to_s.downcase.to_sym
47
+ opts[:description] = opts.delete(:desc) unless opts[:desc].nil?
48
+
49
+ validate_opts opts
50
+
51
+ field = field.to_sym
52
+ db(:add_index, field) if opts.delete(:index)
53
+
54
+ @rules[field] = opts
55
+ end
56
+
57
+ # pass values for db_schema only
58
+ # db :timestamps
59
+ # db :add_index, :code -> t.add_index :code
60
+ def db *args
61
+ @db_rules.push args
62
+ end
63
+
64
+ # if method undefined, call set method
65
+ # age Integer -> set :age, type: :integer
66
+ def method_missing field, *args, &block
67
+ set field, *args, &block
68
+ end
69
+
70
+ # --- argument parsing ---
71
+
72
+ def parse_args args
24
73
  if args.first.is_a?(Hash)
25
74
  opts = args.first || {}
26
75
  else
@@ -29,22 +78,10 @@ module Typero
29
78
  end
30
79
 
31
80
  opts[:type] = :string if opts[:type].nil?
81
+ opts
82
+ end
32
83
 
33
- field = field.to_s
34
-
35
- if field.include?('!')
36
- if block
37
- field = field.sub('!', '')
38
- @block_type = field.to_sym
39
- instance_exec &block
40
- @block_type = nil
41
- return
42
- else
43
- raise ArgumentError.new 'If you use ! you have to provide a block'
44
- end
45
- end
46
-
47
- # name? - opional name
84
+ def parse_field_name field, opts
48
85
  if field.include?('?')
49
86
  field = field.sub('?', '')
50
87
  opts[:required] = false
@@ -53,78 +90,66 @@ module Typero
53
90
  opts[:required] = opts.delete(:req) unless opts[:req].nil?
54
91
  opts[:required] = true if opts[:required].nil?
55
92
 
56
- # array that allows duplicates
93
+ field
94
+ end
95
+
96
+ # --- type resolution ---
97
+
98
+ def define_block_type field, &block
99
+ raise ArgumentError, 'If you use ! you have to provide a block' unless block
100
+
101
+ field = field.sub('!', '')
102
+ @block_type = field.to_sym
103
+ instance_exec &block
104
+ @block_type = nil
105
+ end
106
+
107
+ def resolve_type opts
108
+ # bare Array or Set class -> array of strings
109
+ if opts[:type] == Array || opts[:type] == Set
110
+ opts[:type] = :string
111
+ opts[:array] = true
112
+ end
113
+
114
+ # Array[:type] -> typed array
57
115
  if opts[:type].is_a?(Array)
58
116
  opts[:type] = opts[:type].first
59
117
  opts[:array] = true
60
118
  end
61
119
 
62
- # no duplicates array
120
+ # Set[:type] -> typed array (no duplicates)
63
121
  if opts[:type].is_a?(Set)
64
122
  opts[:type] = opts[:type].to_a.first
65
123
  opts[:array] = true
66
124
  end
67
125
 
126
+ # block type override (integer! do ... end)
68
127
  opts[:type] = @block_type if @block_type
69
128
 
70
- # Boolean
129
+ # boolean variants
71
130
  if opts[:type].is_a?(TrueClass) || opts[:type] == :true
72
131
  opts[:required] = false
73
132
  opts[:default] = true
74
133
  opts[:type] = :boolean
75
134
  elsif opts[:type].is_a?(FalseClass) || opts[:type] == :false || opts[:type] == :boolean
76
- opts[:required] = false if opts[:required].nil?
135
+ opts[:required] = false
77
136
  opts[:default] = false if opts[:default].nil?
78
137
  opts[:type] = :boolean
79
138
  end
80
139
 
81
- # model / schema
82
- if opts[:type].class.ancestors.include?(Typero::Schema)
140
+ # model / schema reference
141
+ if opts[:type].is_a?(Typero::Schema)
83
142
  opts[:model] = opts.delete(:type)
84
143
  end
85
144
  opts[:model] = opts.delete(:schema) if opts[:schema]
86
145
  opts[:type] = :model if opts[:model]
146
+ end
87
147
 
88
- if block_given?
89
- opts[:type] = :model
90
- opts[:model] = Typero.schema &block
91
- end
92
-
93
- opts[:type] ||= 'string'
94
- opts[:type] = opts[:type].to_s.downcase.to_sym
95
-
96
- opts[:description] = opts.delete(:desc) unless opts[:desc].nil?
97
-
98
- # chek alloed params, all optional should go in meta
148
+ def validate_opts opts
149
+ type = Typero::Type.load opts[:type]
99
150
  opts.keys.each do |key|
100
- type = Typero::Type.load opts[:type]
101
151
  type.allowed_opt?(key) {|err| raise ArgumentError, err }
102
152
  end
103
-
104
- field = field.to_sym
105
-
106
- db :add_index, field if opts.delete(:index)
107
-
108
- # trigger error if type not found
109
- Typero::Type.load opts[:type]
110
-
111
- @rules[field] = opts
112
- end
113
-
114
- # pass values for db_schema only
115
- # db :timestamps
116
- # db :add_index, :code -> t.add_index :code
117
- def db *args
118
- @db_rules.push args.unshift(:db_rule!)
119
- end
120
-
121
- # set :age, type: :integer -> integer :age
122
- # email :email
123
- #
124
- # set :emails, Array[:email]
125
- # email Array[:emails]
126
- def method_missing field, *args, &block
127
- set field, *args, &block
128
153
  end
129
154
  end
130
155
  end
data/lib/typero/schema.rb CHANGED
@@ -1,38 +1,18 @@
1
- # Typero.schema :user, type: :model do
2
-
3
- # Typero.schema :some_name, type: :model, db: DB_LOG do
4
- # set :name, String, req: true
5
- # set :email, :email, req: true
6
- # set :emails, [:email], min: 2
7
- # end
8
- #
9
- # rules = Typero.schema :some_name
10
- #
11
- # or
12
- #
13
- # rules = Typero.schema do
14
- # string :name, req: true # generic email string
15
- # email :email, req: true # string of type email
16
- # emails [:skills], min: 2 # list of emails in filed named "emails"
17
- # end
18
- #
19
- # errors = rules.validate (@object || @hash)
20
- # rules.valid? (@object)
21
- # rules.validate(@object) {|errors| ... }
22
-
23
1
  module Typero
24
2
  class Schema
25
3
  SCHEMA_STORE ||= {}
26
4
 
27
- attr_reader :klass
28
- attr_reader :schema
29
- attr_reader :opts
5
+ attr_reader :klass, :opts
6
+
7
+ # accepts dsl block to define schema
8
+ # or define: keyword for internal use (only/except)
9
+ def initialize name, opts = nil, define: nil, &block
10
+ @opts = opts || {}
30
11
 
31
- # accepts dsl block to
32
- def initialize name, opts = nil, &block
33
- if block
34
- @opts = opts || {}
35
- @schema = Params.new &block
12
+ if define
13
+ @schema = define
14
+ elsif block
15
+ @schema = Define.new &block
36
16
 
37
17
  if name
38
18
  @klass = name
@@ -43,78 +23,48 @@ module Typero
43
23
  end
44
24
  end
45
25
 
46
- # validates any instance object with hash variable interface
47
- # it also coarces values
48
- def validate object, options = nil
49
- @options = options || {}
50
- @object = object
51
- @errors = {}
52
-
53
- # remove undefined keys if Hash provided
54
- if @options[:strict] && object.is_a?(Hash)
55
- undefined = object.keys.map(&:to_s) - @schema.rules.keys.map(&:to_s)
56
- object.delete_if { |k, _| undefined.include?(k.to_s) }
57
- end
58
-
59
- @schema.rules.each do |field, opts|
60
- # force filed as a symbol
61
- field = field.to_sym
62
-
63
- for k in opts.keys
64
- opts[k] = @object.instance_exec(&opts[k]) if opts[k].is_a?(Proc)
65
- end
66
-
67
- # set value to default if value is blank and default given
68
- @object[field] = opts[:default] if opts[:default] && @object[field].blank?
69
-
70
- if @object.respond_to?(:key?)
71
- if @object.key?(field)
72
- value = @object[field]
73
- elsif @object.key?(field.to_s)
74
- # invalid string key, needs fix
75
- value = @object[field] = @object.delete(field.to_s)
76
- end
77
- else
78
- value = @object[field]
79
- end
26
+ # returns new schema with only specified keys
27
+ def only *keys
28
+ keys = keys.map(&:to_sym)
29
+ filtered = rules.select { |k, _| keys.include?(k) }
30
+ self.class.new(nil, define: Define.new(filtered))
31
+ end
80
32
 
81
- if opts[:array]
82
- unless value.respond_to?(:each)
83
- opts[:delimiter] ||= /\s*[,\n]\s*/
84
- value = value.to_s.split(opts[:delimiter])
85
- end
33
+ # returns new schema without specified keys
34
+ def except *keys
35
+ keys = keys.map(&:to_sym)
36
+ filtered = rules.reject { |k, _| keys.include?(k) }
37
+ self.class.new(nil, define: Define.new(filtered))
38
+ end
86
39
 
87
- value = value
88
- .flatten
89
- .map { |el| el.to_s == '' ? nil : check_filed_value(field, el, opts) }
90
- .compact
40
+ # validates any instance object with hash variable interface
41
+ # it also coerces values
42
+ def validate object, options = nil
43
+ @object = object
44
+ @errors = {}
45
+ options ||= {}
91
46
 
92
- value = Set.new(value).to_a unless opts[:duplicates]
47
+ strip_undefined_keys! if options[:strict] && object.is_a?(Hash)
93
48
 
94
- opts[:max_count] ||= 100
95
- add_error(field, 'Max number of array elements is %d, you have %d' % [opts[:max_count], value.length], opts) if value && value.length > opts[:max_count]
96
- add_error(field, 'Min number of array elements is %d, you have %d' % [opts[:min_count], value.length], opts) if value && opts[:min_count] && value.length < opts[:min_count]
49
+ @schema.rules.each do |field, raw_opts|
50
+ field = field.to_sym
51
+ opts = resolve_opts(raw_opts)
97
52
 
98
- add_required_error field, value.first, opts
99
- else
100
- value = nil if value.to_s == ''
53
+ apply_default field, opts
54
+ value = read_value field
101
55
 
102
- # if value is not list of allowed values, raise error
103
- allowed = opts[:allow] || opts[:allowed] || opts[:values]
104
- if value && allowed && !allowed.map(&:to_s).include?(value.to_s)
105
- add_error field, 'Value "%s" is not allowed' % value, opts
56
+ value =
57
+ if opts[:array]
58
+ validate_array field, value, opts
59
+ else
60
+ validate_scalar field, value, opts
106
61
  end
107
62
 
108
- value = check_filed_value field, value, opts
109
-
110
- add_required_error field, value, opts
111
- end
112
-
113
63
  # present empty string values as nil
114
- @object[field] = value.to_s.sub(/\s+/, '') == '' ? nil : value
64
+ @object[field] = blank?(value) ? nil : value
115
65
  end
116
66
 
117
- if @errors.keys.length > 0 && block_given?
67
+ if @errors.any? && block_given?
118
68
  @errors.each { |k, v| yield(k, v) }
119
69
  end
120
70
 
@@ -122,29 +72,24 @@ module Typero
122
72
  end
123
73
 
124
74
  def valid? object
125
- errors = validate object
126
- errors.keys.length == 0
75
+ validate(object).empty?
76
+ end
77
+
78
+ # returns raw db rules like [:timestamps] or [:add_index, :code]
79
+ def db_rules
80
+ @schema.db_rules
127
81
  end
128
82
 
129
83
  # returns field, db_type, db_opts
130
84
  def db_schema
131
- out = @schema.rules.inject([]) do |total, (field, opts)|
132
- # get db filed schema
133
- type, opts = Typero::Type.load(opts[:type]).new(nil, opts).db_field
134
-
135
- # add array true to field it ont defined in schema
136
- schema_opts = @schema.rules[field]
137
- opts[:array] = true if schema_opts[:array]
138
-
139
- total << [field, type, opts]
85
+ @schema.rules.map do |field, opts|
86
+ type, db_opts = Typero::Type.load(opts[:type]).new(nil, opts).db_field
87
+ db_opts[:array] = true if opts[:array]
88
+ [field, type, db_opts]
140
89
  end
141
-
142
- out += @schema.db_rules
143
-
144
- out
145
90
  end
146
91
 
147
- # iterate trough all the ruels via block interface
92
+ # iterate through all the rules via block interface
148
93
  # schema.rules do |field, opts|
149
94
  # schema.rules(:url) do |field, opts|
150
95
  def rules filter = nil, &block
@@ -159,7 +104,92 @@ module Typero
159
104
 
160
105
  private
161
106
 
162
- # adds error to array or prefixes with field name
107
+ # remove keys not defined in schema
108
+ def strip_undefined_keys!
109
+ defined_keys = @schema.rules.keys.map(&:to_s)
110
+ @object.delete_if { |k, _| !defined_keys.include?(k.to_s) }
111
+ end
112
+
113
+ # dup opts so Proc resolution does not mutate stored schema
114
+ def resolve_opts raw_opts
115
+ opts = raw_opts.dup
116
+ opts.each do |k, v|
117
+ opts[k] = @object.instance_exec(&v) if v.is_a?(Proc)
118
+ end
119
+ opts
120
+ end
121
+
122
+ def apply_default field, opts
123
+ if !opts[:default].nil? && @object[field].to_s.blank?
124
+ @object[field] = opts[:default]
125
+ end
126
+ end
127
+
128
+ # read value from object, normalizing string keys to symbols for Hash
129
+ def read_value field
130
+ if @object.respond_to?(:key?)
131
+ if @object.key?(field)
132
+ @object[field]
133
+ elsif @object.key?(field.to_s)
134
+ @object[field] = @object.delete(field.to_s)
135
+ end
136
+ else
137
+ @object[field]
138
+ end
139
+ end
140
+
141
+ def validate_array field, value, opts
142
+ unless value.respond_to?(:each)
143
+ delimiter = opts[:delimiter] || /\s*[,\n]\s*/
144
+ value = value.to_s.split(delimiter)
145
+ end
146
+
147
+ value = value
148
+ .flatten
149
+ .map { |el| el.to_s == '' ? nil : coerce_value(field, el, opts) }
150
+ .compact
151
+
152
+ value = Set.new(value).to_a unless opts[:duplicates]
153
+
154
+ max_count = opts[:max_count] || 100
155
+ add_error(field, 'Max number of array elements is %d, you have %d' % [max_count, value.length], opts) if value.length > max_count
156
+ add_error(field, 'Min number of array elements is %d, you have %d' % [opts[:min_count], value.length], opts) if opts[:min_count] && value.length < opts[:min_count]
157
+
158
+ add_required_error field, value.first, opts
159
+ value
160
+ end
161
+
162
+ def validate_scalar field, value, opts
163
+ value = nil if value.to_s == ''
164
+
165
+ allowed = opts[:allow] || opts[:allowed] || opts[:values]
166
+ if value && allowed && !allowed.map(&:to_s).include?(value.to_s)
167
+ add_error field, 'Value "%s" is not allowed' % value, opts
168
+ end
169
+
170
+ value = coerce_value field, value, opts
171
+ add_required_error field, value, opts
172
+ value
173
+ end
174
+
175
+ # coerce a single value through its type class
176
+ def coerce_value field, value, opts
177
+ klass = Typero::Type.load(opts[:type])
178
+ check = klass.new value, opts
179
+ check.get
180
+ rescue TypeError => e
181
+ if e.message[0] == '{'
182
+ JSON.parse(e.message).each do |key, msg|
183
+ add_error [field, key].join('.'), msg, opts
184
+ end
185
+ else
186
+ add_error field, e.message, opts
187
+ end
188
+ rescue JSON::ParserError
189
+ add_error field, e.message, opts
190
+ end
191
+
192
+ # adds error to hash, prefixing with field name if message starts lowercase
163
193
  def add_error field, msg, opts
164
194
  if @errors[field]
165
195
  @errors[field] += ", %s" % msg
@@ -173,28 +203,14 @@ module Typero
173
203
  end
174
204
  end
175
205
 
176
- def safe_type type
177
- type.to_s.gsub(/[^\w]/, "").classify
178
- end
179
-
180
206
  def add_required_error field, value, opts
181
207
  return unless opts[:required] && value.nil?
182
208
  msg = opts[:required].class == TrueClass ? "is required" : opts[:required]
183
209
  add_error field, msg, opts
184
210
  end
185
211
 
186
- def check_filed_value field, value, opts
187
- klass = "Typero::%sType" % safe_type(opts[:type])
188
- check = klass.constantize.new value, opts
189
- check.get
190
- rescue TypeError => e
191
- if e.message[0] == '{'
192
- for key, msg in JSON.parse(e.message)
193
- add_error [field, key].join('.'), msg, opts
194
- end
195
- else
196
- add_error field, e.message, opts
197
- end
212
+ def blank? value
213
+ value.to_s.sub(/\s+/, '') == ''
198
214
  end
199
215
  end
200
216
  end
@@ -0,0 +1,75 @@
1
+ # Shared coordinate extraction from map URLs and plain lat,lon strings.
2
+ # Returns [lat, lon] as strings, or nil if no match.
3
+
4
+ module Typero
5
+ module GeoExtract
6
+ # extract lat, lon from various map URLs or plain "lat,lon" string
7
+ def extract_coords data
8
+ data = data.to_s.strip
9
+
10
+ coords = extract_from_url(data) || extract_from_string(data)
11
+ return nil unless coords
12
+
13
+ coords.map { |c| c.to_s.strip }
14
+ end
15
+
16
+ private
17
+
18
+ def extract_from_url data
19
+ return nil unless data.include?('://')
20
+
21
+ # Google Maps: /@45.815,15.982,... or ?q=45.815,15.982
22
+ if data.include?('/@')
23
+ parts = data.split('/@', 2).last.split(',')
24
+ return [parts[0], parts[1]] if parts.length >= 2
25
+ end
26
+
27
+ if data =~ /google.*[?&]q=([-\d.]+),([-\d.]+)/
28
+ return [$1, $2]
29
+ end
30
+
31
+ # OpenStreetMap: /#map=15/45.815/15.982
32
+ if data =~ /openstreetmap.*#map=\d+\/([-\d.]+)\/([-\d.]+)/
33
+ return [$1, $2]
34
+ end
35
+
36
+ # OpenStreetMap: ?mlat=45.815&mlon=15.982
37
+ if data =~ /openstreetmap.*[?&]mlat=([-\d.]+).*[?&]mlon=([-\d.]+)/
38
+ return [$1, $2]
39
+ end
40
+
41
+ # Apple Maps: ?ll=45.815,15.982
42
+ if data =~ /maps\.apple\.com.*[?&]ll=([-\d.]+),([-\d.]+)/
43
+ return [$1, $2]
44
+ end
45
+
46
+ # Waze: ?ll=45.815,15.982
47
+ if data =~ /waze\.com.*[?&]ll=([-\d.]+),([-\d.]+)/
48
+ return [$1, $2]
49
+ end
50
+
51
+ # Bing Maps: ?cp=45.815~15.982
52
+ if data =~ /bing\.com.*[?&]cp=([-\d.]+)~([-\d.]+)/
53
+ return [$1, $2]
54
+ end
55
+
56
+ # Bing Maps: /point.45.815_15.982
57
+ if data =~ /bing\.com.*point\.([-\d.]+)_([-\d.]+)/
58
+ return [$1, $2]
59
+ end
60
+
61
+ nil
62
+ end
63
+
64
+ def extract_from_string data
65
+ return nil if data.include?('://') || data.include?('POINT')
66
+
67
+ if data.include?(',')
68
+ parts = data.split(/\s*,\s*/)
69
+ return [parts[0], parts[1]] if parts.length >= 2
70
+ end
71
+
72
+ nil
73
+ end
74
+ end
75
+ end
@@ -5,12 +5,12 @@ module Typero
5
5
  ERRORS = {
6
6
  en: {
7
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',
8
+ min_length_error: 'min length is %s, you have %s',
9
+ max_length_error: 'max length is %s, you have %s',
10
10
  min_value_error: 'min is %s, got %s',
11
11
  max_value_error: 'max is %s, got %s',
12
12
  unallowed_characters_error: 'is having unallowed characters',
13
- not_in_range: 'Value in not in allowed range (%s)'
13
+ not_in_range: 'Value is not in allowed range (%s)'
14
14
  }
15
15
  }
16
16
 
@@ -23,6 +23,8 @@ module Typero
23
23
  :default,
24
24
  :description,
25
25
  :delimiter,
26
+ :duplicates,
27
+ :index,
26
28
  :max_count,
27
29
  :meta,
28
30
  :min_count,
@@ -80,7 +82,7 @@ module Typero
80
82
  ###
81
83
 
82
84
  def initialize value, opts={}, &block
83
- value = value.strip.rstrip if value.is_a?(String)
85
+ value = value.strip if value.is_a?(String)
84
86
 
85
87
  opts.keys.each {|key| self.class.allowed_opt?(key) }
86
88
 
@@ -101,7 +103,7 @@ module Typero
101
103
  if value.nil?
102
104
  opts[:default].nil? ? default : opts[:default]
103
105
  else
104
- set
106
+ coerce
105
107
 
106
108
  if opts[:values] && !opts[:values].map(&:to_s).include?(@value.to_s)
107
109
  error_for(:not_in_range, opts[:values].join(', '))
@@ -1,7 +1,7 @@
1
1
  class Typero::BooleanType < Typero::Type
2
2
  error :en, :unsupported_boolean, 'Unsupported boolean param value: %s'
3
3
 
4
- def set
4
+ def coerce
5
5
  value do |_|
6
6
  bool = _.to_s
7
7
 
@@ -6,8 +6,11 @@ require_relative './float_type'
6
6
 
7
7
  class Typero::CurrencyType < Typero::FloatType
8
8
 
9
- def set
9
+ def coerce
10
10
  value { |data| data.to_f.round(2) }
11
+
12
+ error_for(:min_value_error, opts[:min], value) if opts[:min] && value < opts[:min]
13
+ error_for(:max_value_error, opts[:max], value) if opts[:max] && value > opts[:max]
11
14
  end
12
15
 
13
16
  def db_schema
@@ -2,12 +2,17 @@ class Typero::DateType < Typero::Type
2
2
  opts :min, 'Smallest date-time allowed'
3
3
  opts :max, 'Maximal date-time allowed'
4
4
 
5
+ error :en, :invalid_date, 'is not a valid date'
5
6
  error :en, :min_date, 'Minimal allowed date is %s'
6
7
  error :en, :max_date, 'Maximal allowed date is %s'
7
8
 
8
- def set
9
+ def coerce
9
10
  unless [Date].include?(value.class)
10
- value { |data| DateTime.parse(data) }
11
+ begin
12
+ value { |data| DateTime.parse(data.to_s) }
13
+ rescue Date::Error, ArgumentError
14
+ error_for(:invalid_date)
15
+ end
11
16
  end
12
17
 
13
18
  value { |data| DateTime.new(data.year, data.month, data.day) }
@@ -24,12 +29,12 @@ class Typero::DateType < Typero::Type
24
29
  def check_date_min_max
25
30
  if min = opts[:min]
26
31
  min = DateTime.parse(min)
27
- error_for(:min_date, min) % min if min > value
32
+ error_for(:min_date, min) if min > value
28
33
  end
29
34
 
30
35
  if max = opts[:max]
31
36
  max = DateTime.parse(max)
32
- error_for(:max_date, max) % max if value > max
37
+ error_for(:max_date, max) if value > max
33
38
  end
34
39
 
35
40
  value
@@ -4,16 +4,22 @@ class Typero::DatetimeType < Typero::DateType
4
4
  opts :min, 'Smallest date allowed'
5
5
  opts :max, 'Maximal date allowed'
6
6
 
7
- def set
7
+ error :en, :invalid_datetime, 'is not a valid datetime'
8
+
9
+ def coerce
8
10
  unless [Time, DateTime].include?(value.class)
9
- value { |data| DateTime.parse(data) }
11
+ begin
12
+ value { |data| DateTime.parse(data.to_s) }
13
+ rescue Date::Error, ArgumentError
14
+ error_for(:invalid_datetime)
15
+ end
10
16
  end
11
17
 
12
18
  check_date_min_max
13
19
  end
14
20
 
15
21
  def db_schema
16
- [:datetime]
22
+ [:timestamp]
17
23
  end
18
24
  end
19
25
 
@@ -2,7 +2,7 @@ class Typero::EmailType < Typero::Type
2
2
  error :en, :not_8_chars_error, 'is not having at least 8 characters'
3
3
  error :en, :missing_monkey_error, 'is missing @'
4
4
 
5
- def set
5
+ def coerce
6
6
  value do |email|
7
7
  email.downcase.gsub(/\s+/,'+')
8
8
  end
@@ -1,24 +1,20 @@
1
1
  class Typero::FloatType < Typero::Type
2
2
  opts :min, 'Minimum value'
3
- opts :max, 'Maximun value'
3
+ opts :max, 'Maximum value'
4
4
  opts :round, 'Round to (decimal spaces)'
5
5
 
6
- def set
7
- @value =
6
+ def coerce
8
7
  if opts[:round]
9
- value.to_f.round(opts[:round])
8
+ value { |data| data.to_f.round(opts[:round]) }
10
9
  else
11
- value.to_f
10
+ value { |data| data.to_f }
12
11
  end
13
12
 
14
- error_for(:min_length_error, opts[:min], value) if opts[:min] && value < opts[:min]
15
- error_for(:max_length_error, opts[:max], value) if opts[:max] && value > opts[:max]
13
+ error_for(:min_value_error, opts[:min], value) if opts[:min] && value < opts[:min]
14
+ error_for(:max_value_error, opts[:max], value) if opts[:max] && value > opts[:max]
16
15
  end
17
16
 
18
17
  def db_schema
19
- opts = {}
20
- opts[:null] = false if opts[:required]
21
- [:float, opts]
18
+ [:float, {}]
22
19
  end
23
20
  end
24
-
@@ -1,13 +1,13 @@
1
1
  class Typero::HashType < Typero::Type
2
2
  error :en, :not_hash_type_error, 'value is not hash type'
3
3
 
4
- def set
4
+ def coerce
5
5
  if value.is_a?(String) && value[0,1] == '{'
6
- @value = JSON.load(value)
6
+ @value = JSON.parse(value)
7
7
  end
8
8
 
9
9
  @value ||= {}
10
- @value.delete_if {|_, v| v.empty? }
10
+ @value.delete_if {|_, v| v.respond_to?(:empty?) && v.empty? }
11
11
 
12
12
  error_for(:not_hash_type_error) unless @value.respond_to?(:keys) && @value.respond_to?(:values)
13
13
 
@@ -6,11 +6,13 @@ class Typero::ImageType < Typero::Type
6
6
 
7
7
  opts :strict, 'Force image to have known extension (%s)' % FORMATS.join(', ')
8
8
 
9
- def set
9
+ def coerce
10
10
  error_for(:image_not_starting_error) unless value =~ /^https?:\/\/./
11
11
 
12
12
  if opts[:strict]
13
- ext = value.split('.').last.downcase
13
+ # strip query string and fragment before checking extension
14
+ path = value.split('?').first.split('#').first
15
+ ext = path.split('.').last.downcase
14
16
  error_for(:image_not_image_format) unless FORMATS.include?(ext)
15
17
  end
16
18
  end
@@ -1,8 +1,8 @@
1
1
  class Typero::IntegerType < Typero::Type
2
2
  opts :min, 'Minimum value'
3
- opts :max, 'Maximun value'
3
+ opts :max, 'Maximum value'
4
4
 
5
- def set
5
+ def coerce
6
6
  value(&:to_i)
7
7
 
8
8
  error_for(:min_value_error, opts[:min], value) if opts[:min] && value < opts[:min]
@@ -1,5 +1,5 @@
1
1
  class Typero::LabelType < Typero::Type
2
- def set
2
+ def coerce
3
3
  value do |data|
4
4
  data
5
5
  .to_s
@@ -1,7 +1,7 @@
1
1
  class Typero::LocaleType < Typero::Type
2
2
  error :en, :locale_bad_format, 'Locale "%s" is in bad format (should be xx or xx-xx)'
3
3
 
4
- def set
4
+ def coerce
5
5
  error_for(:locale_bad_format, value) unless value =~ /^[\w\-]{2,5}$/
6
6
  end
7
7
 
@@ -1,5 +1,5 @@
1
1
  class Typero::ModelType < Typero::Type
2
- def set
2
+ def coerce
3
3
  value(&:to_h)
4
4
 
5
5
  errors = {}
@@ -10,7 +10,7 @@ class Typero::ModelType < Typero::Type
10
10
  errors[field] = error
11
11
  end
12
12
 
13
- @value.delete_if {|_, v| v.empty? }
13
+ @value.delete_if {|_, v| v.respond_to?(:empty?) && v.empty? }
14
14
 
15
15
  raise TypeError.new errors.to_json if errors.keys.first
16
16
  end
@@ -1,9 +1,9 @@
1
1
  class Typero::OibType < Typero::Type
2
2
  error :en, :not_an_oib_error, 'not in an OIB format'
3
3
 
4
- def set
4
+ def coerce
5
5
  value do |data|
6
- check?(data) ? data.to_i : error_for(:not_an_oib_error)
6
+ check?(data) ? data.to_s : error_for(:not_an_oib_error)
7
7
  end
8
8
  end
9
9
 
@@ -22,7 +22,7 @@ class Typero::OibType < Typero::Type
22
22
  return false unless oib.match(/^[0-9]{11}$/)
23
23
 
24
24
  control_sum = (0..9).inject(10) do |middle, position|
25
- middle += oib.at(position).to_i
25
+ middle += oib[position].to_i
26
26
  middle %= 10
27
27
  middle = 10 if middle == 0
28
28
  middle *= 2
@@ -32,7 +32,7 @@ class Typero::OibType < Typero::Type
32
32
  control_sum = 11 - control_sum
33
33
  control_sum = 0 if control_sum == 10
34
34
 
35
- return control_sum == oib.at(10).to_i
35
+ return control_sum == oib[10].to_i
36
36
  end
37
37
 
38
38
  end
@@ -0,0 +1,15 @@
1
+ class Typero::PhoneType < Typero::Type
2
+ error :en, :invalid_phone, 'is not a valid phone number'
3
+
4
+ def coerce
5
+ value do |data|
6
+ data.to_s.gsub(/[\(\)\-]/, ' ').gsub(/\s+/, ' ').strip
7
+ end
8
+
9
+ error_for(:invalid_phone) unless value =~ /^[\d\s\+]+$/ && value.scan(/\d/).length >= 5
10
+ end
11
+
12
+ def db_schema
13
+ [:string, { limit: 50 }]
14
+ end
15
+ end
@@ -2,20 +2,17 @@
2
2
  # point = @object.class.xselect("ST_AsText(#{field}) as #{field}").where(id: @object.id).first[field.to_sym]
3
3
 
4
4
  class Typero::PointType < Typero::Type
5
- def set
6
- if value.include?('/@')
7
- # extract value from google maps link
8
- point = value.split('/@', 2).last.split(',')
9
- value { [point[0], point[1]].join(',') }
10
- end
5
+ include Typero::GeoExtract
11
6
 
12
- if !value.include?('POINT') && value.include?(',')
13
- point = value.sub(/\s*,\s*/, ' ')
14
- value { 'SRID=4326;POINT(%s)' % point }
15
- end
7
+ def coerce
8
+ if value.is_a?(String) && !value.include?('POINT')
9
+ coords = extract_coords(value)
16
10
 
17
- if value && value.include?(',') && !value =~ /^SRID=4326;POINT\(/
18
- error_for(:unallowed_characters_error)
11
+ if coords
12
+ value { 'SRID=4326;POINT(%s %s)' % [coords[0], coords[1]] }
13
+ else
14
+ error_for(:unallowed_characters_error)
15
+ end
19
16
  end
20
17
  end
21
18
 
@@ -23,4 +20,3 @@ class Typero::PointType < Typero::Type
23
20
  [:geography, {}]
24
21
  end
25
22
  end
26
-
@@ -1,15 +1,15 @@
1
- # Same as point, but we keep data as a float in Array
1
+ # Same as point, but we keep data as a float array [lat, lon]
2
2
 
3
3
  class Typero::SimplePointType < Typero::Type
4
- def set
5
- if value.include?('/@')
6
- # extract value from google maps link
7
- point = value.split('/@', 2).last.split(',')[0,2]
8
- value { point }
9
- end
4
+ include Typero::GeoExtract
5
+
6
+ def coerce
7
+ coords = extract_coords(value)
10
8
 
11
- if !value.include?('POINT') && value.include?(',')
12
- value { value.split(/\s*,\s*/)[0,2] }
9
+ if coords
10
+ value { coords }
11
+ else
12
+ error_for(:unallowed_characters_error)
13
13
  end
14
14
  end
15
15
 
@@ -17,4 +17,3 @@ class Typero::SimplePointType < Typero::Type
17
17
  [:float, { array: true }]
18
18
  end
19
19
  end
20
-
@@ -0,0 +1,25 @@
1
+ class Typero::SlugType < Typero::Type
2
+ opts :max, 'Maximum slug length'
3
+
4
+ error :en, :invalid_slug, 'contains invalid characters'
5
+
6
+ def coerce
7
+ max = opts[:max] || 255
8
+
9
+ value do |data|
10
+ data
11
+ .to_s
12
+ .downcase
13
+ .gsub(/[^\w\-]/, '-')
14
+ .gsub(/\-+/, '-')
15
+ .sub(/^\-/, '')
16
+ .sub(/\-$/, '')[0, max]
17
+ end
18
+
19
+ error_for(:invalid_slug) unless value =~ /^[\w][\w\-]*[\w]$|^[\w]$/
20
+ end
21
+
22
+ def db_schema
23
+ [:string, { limit: opts[:max] || 255 }]
24
+ end
25
+ end
@@ -1,9 +1,9 @@
1
1
  class Typero::StringType < Typero::Type
2
- opts :min, 'Minimun string length'
3
- opts :max, 'Maximun string length'
2
+ opts :min, 'Minimum string length'
3
+ opts :max, 'Maximum string length'
4
4
  opts :downcase, 'is the string in downcase?'
5
5
 
6
- def set
6
+ def coerce
7
7
  value(&:to_s)
8
8
  value(&:downcase) if opts[:downcase]
9
9
 
@@ -1,8 +1,16 @@
1
1
  require_relative 'string_type'
2
2
 
3
3
  class Typero::TextType < Typero::StringType
4
- opts :min, 'Minimun string length'
5
- opts :max, 'Maximun string length'
4
+ opts :min, 'Minimum string length'
5
+ opts :max, 'Maximum string length'
6
+
7
+ def coerce
8
+ value(&:to_s)
9
+ value(&:downcase) if opts[:downcase]
10
+
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
6
14
 
7
15
  def db_schema
8
16
  [:text, {}]
@@ -1,7 +1,7 @@
1
1
  class Typero::TimezoneType < Typero::Type
2
2
  error :en, :invalid_time_zone, 'Invalid time zone'
3
3
 
4
- def set
4
+ def coerce
5
5
  TZInfo::Timezone.get(value)
6
6
  rescue TZInfo::InvalidTimezoneIdentifier
7
7
  error_for :invalid_time_zone
@@ -1,9 +1,8 @@
1
1
  class Typero::UrlType < Typero::Type
2
2
  error :en, :url_not_starting_error, 'URL is not starting with http or https'
3
3
 
4
- def set
5
- parts = value.split('://')
6
- error_for(:url_not_starting_error) unless parts[1]
4
+ def coerce
5
+ error_for(:url_not_starting_error) unless value =~ /^https?:\/\//
7
6
  end
8
7
 
9
8
  def db_schema
@@ -0,0 +1,13 @@
1
+ class Typero::UuidType < Typero::Type
2
+ error :en, :invalid_uuid, 'is not a valid UUID'
3
+
4
+ def coerce
5
+ value { |data| data.to_s.strip.downcase }
6
+
7
+ error_for(:invalid_uuid) unless value =~ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
8
+ end
9
+
10
+ def db_schema
11
+ [:string, { limit: 36 }]
12
+ end
13
+ end
data/lib/typero/typero.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # module quick access to other types
2
+
1
3
  module Typero
2
4
  extend self
3
5
 
@@ -50,7 +52,7 @@ module Typero
50
52
  # Typero.schema type: :model
51
53
  out = []
52
54
 
53
- for schema in Schema::SCHEMA_STORE.values
55
+ Schema::SCHEMA_STORE.values.each do |schema|
54
56
  if schema.opts[name.keys.first] == name.values.first
55
57
  out.push schema.klass
56
58
  end
@@ -64,6 +66,17 @@ module Typero
64
66
  end
65
67
  end
66
68
 
69
+ # same as schema but returns nil if not found
70
+ def schema? name
71
+ klass = name.to_s.classify if name
72
+ Typero::Schema::SCHEMA_STORE[klass] if klass
73
+ end
74
+
75
+ # get array of database fields, ruby Sequel compatibile
76
+ def db_schema name
77
+ Typero.schema(name).db_schema
78
+ end
79
+
67
80
  def defined? name
68
81
  Typero::Type.load name
69
82
  true
data/lib/typero.rb CHANGED
@@ -3,8 +3,9 @@
3
3
  # base libs
4
4
  require_relative 'typero/typero'
5
5
  require_relative 'typero/schema'
6
- require_relative 'typero/params'
6
+ require_relative 'typero/define'
7
7
  require_relative 'typero/type/type'
8
+ require_relative 'typero/type/geo_extract'
8
9
 
9
10
  # checker types
10
11
  Dir['%s/typero/type/types/*.rb' % __dir__].each do |file|
metadata CHANGED
@@ -1,29 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typero
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.6
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dino Reic
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2022-12-20 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: fast_blank
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
27
12
  description: Simple and fast ruby type system. Enforce types as Array, Email, Boolean
28
13
  for ruby class instances
29
14
  email: reic.dino@gmail.com
@@ -34,8 +19,9 @@ files:
34
19
  - "./.version"
35
20
  - "./lib/adapters/sequel.rb"
36
21
  - "./lib/typero.rb"
37
- - "./lib/typero/params.rb"
22
+ - "./lib/typero/define.rb"
38
23
  - "./lib/typero/schema.rb"
24
+ - "./lib/typero/type/geo_extract.rb"
39
25
  - "./lib/typero/type/type.rb"
40
26
  - "./lib/typero/type/types/boolean_type.rb"
41
27
  - "./lib/typero/type/types/currency_type.rb"
@@ -50,19 +36,21 @@ files:
50
36
  - "./lib/typero/type/types/locale_type.rb"
51
37
  - "./lib/typero/type/types/model_type.rb"
52
38
  - "./lib/typero/type/types/oib_type.rb"
39
+ - "./lib/typero/type/types/phone_type.rb"
53
40
  - "./lib/typero/type/types/point_type.rb"
54
41
  - "./lib/typero/type/types/simple_point_type.rb"
42
+ - "./lib/typero/type/types/slug_type.rb"
55
43
  - "./lib/typero/type/types/string_type.rb"
56
44
  - "./lib/typero/type/types/text_type.rb"
57
45
  - "./lib/typero/type/types/time_type.rb"
58
46
  - "./lib/typero/type/types/timezone_type.rb"
59
47
  - "./lib/typero/type/types/url_type.rb"
48
+ - "./lib/typero/type/types/uuid_type.rb"
60
49
  - "./lib/typero/typero.rb"
61
50
  homepage: https://github.com/dux/typero
62
51
  licenses:
63
52
  - MIT
64
53
  metadata: {}
65
- post_install_message:
66
54
  rdoc_options: []
67
55
  require_paths:
68
56
  - lib
@@ -77,8 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
65
  - !ruby/object:Gem::Version
78
66
  version: '0'
79
67
  requirements: []
80
- rubygems_version: 3.2.3
81
- signing_key:
68
+ rubygems_version: 4.0.8
82
69
  specification_version: 4
83
70
  summary: Ruby type system
84
71
  test_files: []