rasti-form 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +2 -0
  3. data/.gitignore +9 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +11 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +131 -0
  10. data/Rakefile +24 -0
  11. data/lib/rasti/form/castable.rb +25 -0
  12. data/lib/rasti/form/errors.rb +41 -0
  13. data/lib/rasti/form/formatable.rb +19 -0
  14. data/lib/rasti/form/types/array.rb +36 -0
  15. data/lib/rasti/form/types/boolean.rb +42 -0
  16. data/lib/rasti/form/types/enum.rb +38 -0
  17. data/lib/rasti/form/types/float.rb +33 -0
  18. data/lib/rasti/form/types/form.rb +40 -0
  19. data/lib/rasti/form/types/hash.rb +39 -0
  20. data/lib/rasti/form/types/integer.rb +21 -0
  21. data/lib/rasti/form/types/regexp.rb +28 -0
  22. data/lib/rasti/form/types/string.rb +23 -0
  23. data/lib/rasti/form/types/symbol.rb +23 -0
  24. data/lib/rasti/form/types/time.rb +40 -0
  25. data/lib/rasti/form/types/uuid.rb +19 -0
  26. data/lib/rasti/form/version.rb +5 -0
  27. data/lib/rasti/form.rb +173 -0
  28. data/lib/rasti-form.rb +1 -0
  29. data/rasti-form.gemspec +37 -0
  30. data/spec/coverage_helper.rb +7 -0
  31. data/spec/form_spec.rb +310 -0
  32. data/spec/minitest_helper.rb +13 -0
  33. data/spec/types/array_spec.rb +22 -0
  34. data/spec/types/boolean_spec.rb +24 -0
  35. data/spec/types/enum_spec.rb +20 -0
  36. data/spec/types/float_spec.rb +18 -0
  37. data/spec/types/form_spec.rb +41 -0
  38. data/spec/types/hash_spec.rb +20 -0
  39. data/spec/types/integer_spec.rb +18 -0
  40. data/spec/types/regexp_spec.rb +20 -0
  41. data/spec/types/string_spec.rb +16 -0
  42. data/spec/types/symbol_spec.rb +16 -0
  43. data/spec/types/time_spec.rb +25 -0
  44. data/spec/types/uuid_spec.rb +18 -0
  45. metadata +228 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b3dad1f53996c5b3093f6c924b8475b64ae9826d
4
+ data.tar.gz: 572457d753e9fd911b3c1ea4780bdfd32b0760f7
5
+ SHA512:
6
+ metadata.gz: b09a249237c1b4b324e67dcad8c19ee504d0ed741098eb0766f99379f0a011088694dc695e5ac3bdb88435135af78a9d0d63ed045294974aae95e7242ed1fa4f
7
+ data.tar.gz: 397de77010d7955fadf05b57f5bb189562c17c92c52d2001b43661c06d59f9db1a2d639b55ad0bf354c76f6f46d590a65d6a4dc8b42a8e36ec93a22fd5730c30
data/.coveralls.yml ADDED
@@ -0,0 +1,2 @@
1
+ service_name: travis-ci
2
+ repo_token: 0EGMDDynKz8NseenfliuxtLQ94b2Kg2MN
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ rasti-form
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.3.0
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0
5
+ - 2.1
6
+ - 2.2
7
+ - 2.3.0
8
+ - 2.4.0
9
+ - jruby
10
+ before_install:
11
+ - gem install bundler
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rasti-form.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Gabriel Naiman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # Rasti::Form
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/rasti-form.svg)](https://rubygems.org/gems/rasti-form)
4
+ [![Build Status](https://travis-ci.org/gabynaiman/rasti-form.svg?branch=master)](https://travis-ci.org/gabynaiman/rasti-form)
5
+ [![Coverage Status](https://coveralls.io/repos/github/gabynaiman/rasti-form/badge.svg?branch=master)](https://coveralls.io/github/gabynaiman/rasti-form?branch=master)
6
+ [![Code Climate](https://codeclimate.com/github/gabynaiman/rasti-form.svg)](https://codeclimate.com/github/gabynaiman/rasti-form)
7
+ [![Dependency Status](https://gemnasium.com/gabynaiman/rasti-form.svg)](https://gemnasium.com/gabynaiman/rasti-form)
8
+
9
+ Forms validations and type casting
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'rasti-form'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install rasti-form
26
+
27
+ ## Usage
28
+
29
+ ```ruby
30
+ T = Rasti::Form::Types
31
+ ```
32
+
33
+ ### Type casting
34
+
35
+ ```ruby
36
+ T::Integer.cast '10' # => 10
37
+ T::Integer.cast '10.5' # => 10
38
+ T::Integer.cast 'text' # => Rasti::Types::CastError: Invalid cast: 'text' -> Rasti::Types::Integer
39
+
40
+ T::Boolean.cast 'true' # => true
41
+ T::Boolean.cast 'FALSE' # => false
42
+ T::Boolean.cast 'text' # => Rasti::Types::CastError: Invalid cast: 'text' -> Rasti::Types::Boolean
43
+
44
+ T::Time['%Y-%m-%d'].cast '2016-10-22' # => 2016-10-22 00:00:00 -0300
45
+ T::Time['%Y-%m-%d'].cast '2016-10' # => Rasti::Types::CastError: Invalid cast: '2016-10' -> Rasti::Types::Time['%Y-%m-%d']
46
+
47
+ T::Array[T::Symbol].cast [1, 'test', :sym] # => [:"1", :test, :sym]
48
+ ```
49
+
50
+ ### Form type coercion
51
+
52
+ ```ruby
53
+ PointForm = Rasti::Form[x: T::Integer, y: T::Integer] # => PointForm[:x, :y]
54
+ form = PointForm.new x: '1', y: 2 # => #<PointForm[x: 1, y: 2]>
55
+ form.x # => 1
56
+ form.y # => 2
57
+ form.attributes # => {x: 1, y: 2}
58
+
59
+ PointForm.new x: true # => Validation error: {"x":["Invalid cast: true -> Rasti::Form::Types::Integer"]}
60
+ ```
61
+
62
+ ### Form validations
63
+
64
+ ```ruby
65
+ class DateRangeForm < Rasti::Form
66
+ TIME_FORMAT = '%d/%m/%Y'
67
+
68
+ attribute :from, T::Time[TIME_FORMAT]
69
+ attribute :to, T::Time[TIME_FORMAT]
70
+
71
+ private
72
+
73
+ def validate
74
+ assert_present :from
75
+ assert_present :to
76
+ assert :from, from <= to, 'From must be less than To' if from && to
77
+ end
78
+ end
79
+
80
+ DateRangeForm.new # => Validation error: {"from":["not present"],"to":["not present"]}
81
+ DateRangeForm.new from: '20/10/2016', to: '08/10/2016' # => Validation error: {"from":["From must be less than To"]}
82
+
83
+ form = DateRangeForm.new from: '20/10/2016', to: '28/10/2016'
84
+ form.from # => 2016-10-20 00:00:00 -0300
85
+ form.to # => 2016-10-28 00:00:00 -0300
86
+ ```
87
+
88
+ ### Built-in types
89
+
90
+ - Array
91
+ - Boolean
92
+ - Enum
93
+ - Float
94
+ - Hash
95
+ - Integer
96
+ - Regexp
97
+ - String
98
+ - Struct
99
+ - Symbol
100
+ - Time
101
+ - UUID
102
+
103
+ ### Plugable types
104
+
105
+ ```ruby
106
+ class CustomType
107
+ class << self
108
+ extend Castable
109
+
110
+ private
111
+
112
+ def valid?(value)
113
+ valid.is_a?(String)
114
+ end
115
+
116
+ def transform(value)
117
+ value.upcase
118
+ end
119
+ end
120
+ end
121
+ ```
122
+
123
+ ## Contributing
124
+
125
+ Bug reports and pull requests are welcome on GitHub at https://github.com/gabynaiman/rasti-form.
126
+
127
+
128
+ ## License
129
+
130
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
131
+
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:spec) do |t|
5
+ t.libs << 'spec'
6
+ t.pattern = 'spec/**/*_spec.rb'
7
+ t.verbose = false
8
+ t.warning = false
9
+ t.loader = nil if ENV['TEST']
10
+ ENV['TEST'], ENV['LINE'] = ENV['TEST'].split(':') if ENV['TEST'] && !ENV['LINE']
11
+ t.options = ''
12
+ t.options << "--name=/#{ENV['NAME']}/ " if ENV['NAME']
13
+ t.options << "-l #{ENV['LINE']} " if ENV['LINE'] && ENV['TEST']
14
+ end
15
+
16
+ task default: :spec
17
+
18
+ desc 'Pry console'
19
+ task :console do
20
+ require 'rasti-form'
21
+ require 'pry'
22
+ ARGV.clear
23
+ Pry.start
24
+ end
@@ -0,0 +1,25 @@
1
+ module Rasti
2
+ class Form
3
+ module Castable
4
+
5
+ def cast(value)
6
+ if valid? value
7
+ transform! value
8
+ else
9
+ raise CastError.new self, value
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def transform!(value)
16
+ transform value
17
+ rescue ValidationError => ex
18
+ raise ex
19
+ rescue
20
+ raise CastError.new self, value
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ module Rasti
2
+ class Form
3
+
4
+ class CastError < StandardError
5
+
6
+ attr_reader :type, :value
7
+
8
+ def initialize(type, value)
9
+ @type = type
10
+ @value = value
11
+ end
12
+
13
+ def message
14
+ "Invalid cast: #{display_value} -> #{type}"
15
+ end
16
+
17
+ private
18
+
19
+ def display_value
20
+ value.is_a?(::String) ? "'#{value}'" : value.inspect
21
+ end
22
+
23
+ end
24
+
25
+
26
+ class ValidationError < StandardError
27
+
28
+ attr_reader :errors
29
+
30
+ def initialize(errors)
31
+ @errors = errors
32
+ end
33
+
34
+ def message
35
+ "Validation error: #{JSON.dump(errors)}"
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ module Rasti
2
+ class Form
3
+ module Formatable
4
+
5
+ include Castable
6
+
7
+ private
8
+
9
+ def valid?(value)
10
+ (value.is_a?(::String) || value.is_a?(Symbol)) && value.to_s.match(format)
11
+ end
12
+
13
+ def transform(value)
14
+ value
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Array
5
+
6
+ include Castable
7
+
8
+ attr_reader :type
9
+
10
+ def self.[](type)
11
+ new type
12
+ end
13
+
14
+ def to_s
15
+ "#{self.class}[#{type}]"
16
+ end
17
+ alias_method :inspect, :to_s
18
+
19
+ private
20
+
21
+ def initialize(type)
22
+ @type = type
23
+ end
24
+
25
+ def valid?(value)
26
+ value.is_a? ::Array
27
+ end
28
+
29
+ def transform(value)
30
+ value.map { |e| type.cast e }
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Boolean
5
+ class << self
6
+
7
+ include Castable
8
+
9
+ TRUE_FORMAT = /^t(rue)?$/i
10
+ FALSE_FORMAT = /^f(alse)?$/i
11
+
12
+ private
13
+
14
+ def valid?(value)
15
+ boolean?(value) || valid_string?(value)
16
+ end
17
+
18
+ def transform(value)
19
+ boolean?(value) ? value : true_string?(value)
20
+ end
21
+
22
+ def boolean?(value)
23
+ value == true || value == false
24
+ end
25
+
26
+ def valid_string?(value)
27
+ value.is_a?(::String) && (true_string?(value) || false_string?(value))
28
+ end
29
+
30
+ def true_string?(value)
31
+ !!value.match(TRUE_FORMAT)
32
+ end
33
+
34
+ def false_string?(value)
35
+ !!value.match(FALSE_FORMAT)
36
+ end
37
+
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,38 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Enum
5
+
6
+ include Castable
7
+
8
+ attr_reader :values
9
+
10
+ def self.[](*values)
11
+ new values
12
+ end
13
+
14
+ def to_s
15
+ "#{self.class}[#{values.map(&:inspect).join(', ')}]"
16
+ end
17
+ alias_method :inspect, :to_s
18
+
19
+ private
20
+
21
+ def initialize(values)
22
+ @values = values.map(&:to_s)
23
+ end
24
+
25
+ def valid?(value)
26
+ values.include? String.cast(value)
27
+ rescue
28
+ false
29
+ end
30
+
31
+ def transform(value)
32
+ String.cast value
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Float
5
+ class << self
6
+
7
+ include Castable
8
+
9
+ FORMAT = /^(\d+\.)?\d+$/
10
+
11
+ private
12
+
13
+ def valid?(value)
14
+ !value.nil? && (valid_string?(value) || transformable?(value))
15
+ end
16
+
17
+ def transform(value)
18
+ value.to_f
19
+ end
20
+
21
+ def valid_string?(value)
22
+ value.is_a?(::String) && value.match(FORMAT)
23
+ end
24
+
25
+ def transformable?(value)
26
+ !value.is_a?(::String) && value.respond_to?(:to_f)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Form
5
+
6
+ include Castable
7
+
8
+ attr_reader :form_class
9
+
10
+ def self.[](*args)
11
+ new *args
12
+ end
13
+
14
+ def to_s
15
+ "#{self.class}[#{form_class}]"
16
+ end
17
+ alias_method :inspect, :to_s
18
+
19
+ private
20
+
21
+ def initialize(form)
22
+ @form_class = case
23
+ when form.is_a?(::Hash) then Rasti::Form[form]
24
+ when form.is_a?(Class) && form.ancestors.include?(Rasti::Form) then form
25
+ else raise ArgumentError, "Invalid form specification: #{form.inspect}"
26
+ end
27
+ end
28
+
29
+ def valid?(value)
30
+ value.is_a? ::Hash
31
+ end
32
+
33
+ def transform(value)
34
+ form_class.new value
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Hash
5
+
6
+ include Castable
7
+
8
+ attr_reader :key_type, :value_type
9
+
10
+ def self.[](key_type, value_type)
11
+ new key_type, value_type
12
+ end
13
+
14
+ def to_s
15
+ "#{self.class}[#{key_type}, #{value_type}]"
16
+ end
17
+ alias_method :inspect, :to_s
18
+
19
+ private
20
+
21
+ def initialize(key_type, value_type)
22
+ @key_type = key_type
23
+ @value_type = value_type
24
+ end
25
+
26
+ def valid?(value)
27
+ value.is_a? ::Hash
28
+ end
29
+
30
+ def transform(value)
31
+ value.each_with_object({}) do |(k,v),h|
32
+ h[key_type.cast k] = value_type.cast v
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Integer < Float
5
+ class << self
6
+
7
+ private
8
+
9
+ def transform(value)
10
+ value.to_i
11
+ end
12
+
13
+ def transformable?(value)
14
+ !value.is_a?(::String) && value.respond_to?(:to_i)
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Regexp
5
+
6
+ include Formatable
7
+
8
+ def self.[](format)
9
+ new format
10
+ end
11
+
12
+ def to_s
13
+ "#{self.class}[#{format.inspect}]"
14
+ end
15
+ alias_method :inspect, :to_s
16
+
17
+ private
18
+
19
+ attr_reader :format
20
+
21
+ def initialize(format)
22
+ @format = format.is_a?(String) ? ::Regexp.new(format) : format
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class String
5
+ class << self
6
+
7
+ include Castable
8
+
9
+ private
10
+
11
+ def valid?(value)
12
+ !value.nil? && value.respond_to?(:to_s)
13
+ end
14
+
15
+ def transform(value)
16
+ value.to_s
17
+ end
18
+
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Symbol
5
+ class << self
6
+
7
+ include Castable
8
+
9
+ private
10
+
11
+ def valid?(value)
12
+ !value.nil? && (value.respond_to?(:to_sym) || value.respond_to?(:to_s))
13
+ end
14
+
15
+ def transform(value)
16
+ value.respond_to?(:to_sym) ? value.to_sym : value.to_s.to_sym
17
+ end
18
+
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ module Rasti
2
+ class Form
3
+ module Types
4
+ class Time
5
+
6
+ include Castable
7
+
8
+ attr_reader :format
9
+
10
+ def self.[](format)
11
+ new format
12
+ end
13
+
14
+ def to_s
15
+ "#{self.class}['#{format}']"
16
+ end
17
+ alias_method :inspect, :to_s
18
+
19
+ private
20
+
21
+ def initialize(format)
22
+ @format = format
23
+ end
24
+
25
+ def valid?(value)
26
+ value.is_a?(::String) || value.respond_to?(:to_time)
27
+ end
28
+
29
+ def transform(value)
30
+ value.is_a?(::String) ? string_to_time(value) : value.to_time
31
+ end
32
+
33
+ def string_to_time(value)
34
+ ::Time.strptime(value, format)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end