rom-model 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8dae3cb7ed3616ec2fb6dab8815cdcfcbcc5d4ee
4
+ data.tar.gz: dd5df2f70e0b945ec0a42e4ce02bbac700ca13ef
5
+ SHA512:
6
+ metadata.gz: d9e16eedc459dfda4e4ff7d641179a23266781cbd646fedc10234386dfda320487dd30f2a81bf6134cca7b17ffa93c35a92e694872f91e1a91f2f4b6e924703f
7
+ data.tar.gz: 416b99d8c8bdcc6be0377706976e3d42b725f3b3f3136ffcc9f555fef2a34989fbe20a3a340cce8e980c43fc1e3e54b65ecf4a555f6ced6ba37bdfd1d1efcc6b
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ -r ./spec/spec_helper
@@ -0,0 +1,30 @@
1
+ language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+ bundler_args: --without yard guard benchmarks tools
5
+ before_script:
6
+ - psql -c 'create database rom_model' -U postgres
7
+ script: "bundle exec rake ci"
8
+ rvm:
9
+ - 2.0
10
+ - 2.1
11
+ - 2.2
12
+ - rbx-2
13
+ - jruby
14
+ - jruby-head
15
+ - ruby-head
16
+ env:
17
+ global:
18
+ - CODECLIMATE_REPO_TOKEN=TODO
19
+ - JRUBY_OPTS='--dev -J-Xmx1024M'
20
+ matrix:
21
+ allow_failures:
22
+ - rvm: ruby-head
23
+ - rvm: jruby-head
24
+ notifications:
25
+ webhooks:
26
+ urls:
27
+ - https://webhooks.gitter.im/e/39e1225f489f38b0bd09
28
+ on_success: change
29
+ on_failure: always
30
+ on_start: false
@@ -0,0 +1,3 @@
1
+ # 0.1.0 2015-08-14
2
+
3
+ Extracted code from rom-rails 0.5.0
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'rom', github: 'rom-rb/rom', branch: 'master'
6
+ gem 'rom-sql', github: 'rom-rb/rom-sql', branch: 'master'
7
+ gem 'pg', platforms: [:mri, :rbx]
8
+ gem 'pg_jruby', platforms: :jruby
9
+
10
+ group :tools do
11
+ gem 'byebug', platforms: :mri
12
+ end
13
+
14
+ group :test do
15
+ gem 'rspec'
16
+ gem 'codeclimate-test-reporter', require: nil, platform: :rbx
17
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014-2015 Ruby Object Mapper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ 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, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,65 @@
1
+ [gem]: https://rubygems.org/gems/rom-model
2
+ [travis]: https://travis-ci.org/rom-rb/rom-model
3
+ [gemnasium]: https://gemnasium.com/rom-rb/rom-model
4
+ [codeclimate]: https://codeclimate.com/github/rom-rb/rom-model
5
+ [inchpages]: http://inch-ci.org/github/rom-rb/rom-model
6
+
7
+ # ROM::Model
8
+
9
+ [![Gem Version](https://badge.fury.io/rb/rom-model.svg)][gem]
10
+ [![Build Status](https://travis-ci.org/rom-rb/rom-model.svg?branch=master)][travis]
11
+ [![Dependency Status](https://gemnasium.com/rom-rb/rom-model.png)][gemnasium]
12
+ [![Code Climate](https://codeclimate.com/github/rom-rb/rom-model/badges/gpa.svg)][codeclimate]
13
+ [![Test Coverage](https://codeclimate.com/github/rom-rb/rom-model/badges/coverage.svg)][codeclimate]
14
+ [![Inline docs](http://inch-ci.org/github/rom-rb/rom-model.svg?branch=master)][inchpages]
15
+
16
+ This is a set of extensions for PORO objects to help in data coercion and validation.
17
+ It was extracted from rom-rails and for now it uses Virtus and ActiveModel.
18
+
19
+ The package includes:
20
+
21
+ - `ROM::Attributes` for defining input attributes with types and coercion rules
22
+ - `ROM::Validator` a standalone validator object extension built on top of
23
+ `ActiveModel::Validations` with additional features like nested validators
24
+
25
+ ## The Plan™
26
+
27
+ This gem is built on top of existing 3rd party gems that have proven to be stable
28
+ and good-enough. Unfortunatelly neither Virtus nor ActiveModel do not meet certain
29
+ design requirements to be a good fit in the long term.
30
+
31
+ For that reason we're exploring how to build a better foundation for rom-model.
32
+ Specifically following initiatives are taking place:
33
+
34
+ - Exploring a lower-level validation library with great composability features
35
+ and simple interface
36
+ - Investigating a lower-level input data sanitization/coercion library that would
37
+ be a perfect fit for handling web forms and json input
38
+
39
+ Furthermore rom-model will be soon extended with a third extension for defining
40
+ [Algebraic Data Types](https://en.wikipedia.org/wiki/Algebraic_data_type) which
41
+ will work remarkably well with [rom-repository](https://github.com/rom-rb/rom-repository)
42
+ and its auto-mapping features.
43
+
44
+ This project will provide convenient interfaces on top of robust lower-level tools
45
+ and if it turns out to be too big we'll split it into smaller gems.
46
+
47
+ ## Installation
48
+
49
+ Add this line to your application's Gemfile:
50
+
51
+ ```ruby
52
+ gem 'rom-model'
53
+ ```
54
+
55
+ And then execute:
56
+
57
+ $ bundle
58
+
59
+ Or install it yourself as:
60
+
61
+ $ gem install rom-model
62
+
63
+ ## License
64
+
65
+ See `LICENSE` file.
@@ -0,0 +1,8 @@
1
+ require "rspec/core/rake_task"
2
+ require "rake/testtask"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task default: [:ci]
6
+
7
+ desc "Run CI tasks"
8
+ task ci: [:spec]
@@ -0,0 +1,8 @@
1
+ en:
2
+ activemodel:
3
+ errors:
4
+ models:
5
+ user:
6
+ attributes:
7
+ email:
8
+ taken: 'has already been taken'
@@ -0,0 +1 @@
1
+ require 'rom/model'
@@ -0,0 +1,2 @@
1
+ require 'rom/model/attributes'
2
+ require 'rom/model/validator'
@@ -0,0 +1,133 @@
1
+ require 'virtus'
2
+ require 'active_model' # can't cherry-pick conversion :(
3
+
4
+ module ROM
5
+ module Model
6
+ # Mixin for validatable and coercible parameters
7
+ #
8
+ # @example
9
+ #
10
+ # class UserAttributes
11
+ # include ROM::Model::Attributes
12
+ #
13
+ # attribute :email, String
14
+ # attribute :age, Integer
15
+ #
16
+ # validates :email, :age, presence: true
17
+ # end
18
+ #
19
+ # user_attrs = UserAttributes.new(email: '', age: '18')
20
+ #
21
+ # user_attrs.email # => ''
22
+ # user_attrs.age # => 18
23
+ #
24
+ # user_attrs.valid? # => false
25
+ # user_attrs.errors # => #<ActiveModel::Errors:0x007fd2423fadb0 ...>
26
+ #
27
+ # @api public
28
+ module Attributes
29
+ VirtusModel = Virtus.model(nullify_blank: true)
30
+
31
+ # Inclusion hook used to extend a class with required interfaces
32
+ #
33
+ # @api private
34
+ def self.included(base)
35
+ base.class_eval do
36
+ include VirtusModel
37
+ include ActiveModel::Conversion
38
+ end
39
+ base.extend(ClassMethods)
40
+ end
41
+
42
+ # Return model name for the attributes class
43
+ #
44
+ # The model name object is configurable using `set_model_name` macro
45
+ #
46
+ # @see ClassMethods#set_model_name
47
+ #
48
+ # @return [ActiveModel::Name]
49
+ #
50
+ # @api public
51
+ def model_name
52
+ self.class.model_name
53
+ end
54
+
55
+ # Class extensions for an attributes class
56
+ #
57
+ # @api public
58
+ module ClassMethods
59
+ # Default timestamp attribute names used by `timestamps` method
60
+ DEFAULT_TIMESTAMPS = [:created_at, :updated_at].freeze
61
+
62
+ # Process input and return attributes instance
63
+ #
64
+ # @example
65
+ # class UserAttributes
66
+ # include ROM::Model::Attributes
67
+ #
68
+ # attribute :name, String
69
+ # end
70
+ #
71
+ # UserAttributes[name: 'Jane']
72
+ #
73
+ # @param [Hash,#to_hash] input The input params
74
+ #
75
+ # @return [Attributes]
76
+ #
77
+ # @api public
78
+ def [](input)
79
+ input.is_a?(self) ? input : new(input)
80
+ end
81
+
82
+ # Macro for defining ActiveModel::Name object on the attributes class
83
+ #
84
+ # This is essential for rails helpers to work properly when generating
85
+ # form input names etc.
86
+ #
87
+ # @example
88
+ # class UserAttributes
89
+ # include ROM::Model::Attributes
90
+ #
91
+ # set_model_name 'User'
92
+ # end
93
+ #
94
+ # @return [undefined]
95
+ #
96
+ # @api public
97
+ def set_model_name(name)
98
+ class_eval <<-RUBY
99
+ def self.model_name
100
+ @model_name ||= ActiveModel::Name.new(self, nil, #{name.inspect})
101
+ end
102
+ RUBY
103
+ end
104
+
105
+ # Shortcut for defining timestamp attributes like created_at etc.
106
+ #
107
+ # @example
108
+ # class NewPostAttributes
109
+ # include ROM::Model::Attributes
110
+ #
111
+ # # provide name(s) explicitly
112
+ # timestamps :published_at
113
+ #
114
+ # # defaults to :created_at, :updated_at without args
115
+ # timestamps
116
+ # end
117
+ #
118
+ # @api public
119
+ def timestamps(*attrs)
120
+ if attrs.empty?
121
+ DEFAULT_TIMESTAMPS.each do |t|
122
+ attribute t, DateTime, default: proc { DateTime.now }
123
+ end
124
+ else
125
+ attrs.each do |attr|
126
+ attribute attr, DateTime, default: proc { DateTime.now }
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,201 @@
1
+ require 'charlatan'
2
+
3
+ require 'rom/support/constants'
4
+ require 'rom/constants'
5
+
6
+ require 'rom/model/validator/uniqueness_validator'
7
+ require 'rom/support/class_macros'
8
+
9
+ module ROM
10
+ module Model
11
+ class ValidationError < CommandError
12
+ include Charlatan.new(:errors)
13
+ include Equalizer.new(:errors)
14
+ end
15
+
16
+ # Mixin for ROM-compliant validator objects
17
+ #
18
+ # @example
19
+ #
20
+ #
21
+ # class UserAttributes
22
+ # include ROM::Model::Attributes
23
+ #
24
+ # attribute :name
25
+ #
26
+ # validates :name, presence: true
27
+ # end
28
+ #
29
+ # class UserValidator
30
+ # include ROM::Model::Validator
31
+ # end
32
+ #
33
+ # attrs = UserAttributes.new(name: '')
34
+ # UserValidator.call(attrs) # raises ValidationError
35
+ #
36
+ # @api public
37
+ module Validator
38
+ # Inclusion hook that extends a class with required interfaces
39
+ #
40
+ # @api private
41
+ def self.included(base)
42
+ base.class_eval do
43
+ extend ClassMethods
44
+ extend ROM::ClassMacros
45
+
46
+ include ActiveModel::Validations
47
+ include Equalizer.new(:attributes, :errors)
48
+
49
+ base.defines :embedded_validators
50
+
51
+ embedded_validators({})
52
+ end
53
+ end
54
+
55
+ # @return [Model::Attributes]
56
+ #
57
+ # @api private
58
+ attr_reader :attributes
59
+
60
+ # @api private
61
+ attr_reader :attr_names
62
+
63
+ delegate :model_name, to: :attributes
64
+
65
+ # @api private
66
+ def initialize(attributes)
67
+ @attributes = attributes
68
+ @attr_names = self.class.validators.map(&:attributes).flatten.uniq
69
+ end
70
+
71
+ # @return [Model::Attributes]
72
+ #
73
+ # @api public
74
+ def to_model
75
+ attributes
76
+ end
77
+
78
+ # Trigger validations and return attributes on success
79
+ #
80
+ # @raises ValidationError
81
+ #
82
+ # @return [Model::Attributes]
83
+ #
84
+ # @api public
85
+ def call
86
+ raise ValidationError, errors unless valid?
87
+ attributes
88
+ end
89
+
90
+ private
91
+
92
+ # This is needed for ActiveModel::Validations to work properly
93
+ # as it expects the object to provide attribute values. Meh.
94
+ #
95
+ # @api private
96
+ def method_missing(name, *args, &block)
97
+ if attr_names.include?(name)
98
+ attributes[name]
99
+ else
100
+ super
101
+ end
102
+ end
103
+
104
+ module ClassMethods
105
+ # Set relation name for a validator
106
+ #
107
+ # This is needed for validators that require database access
108
+ #
109
+ # @example
110
+ #
111
+ # class UserValidator
112
+ # include ROM::Model::Validator
113
+ #
114
+ # relation :users
115
+ #
116
+ # validates :name, uniqueness: true
117
+ # end
118
+ #
119
+ # @return [Symbol]
120
+ #
121
+ # @api public
122
+ def relation(name = nil)
123
+ @relation = name if name
124
+ @relation
125
+ end
126
+
127
+ # @api private
128
+ def set_model_name(name)
129
+ class_eval <<-RUBY
130
+ def self.model_name
131
+ @model_name ||= ActiveModel::Name.new(self, nil, #{name.inspect})
132
+ end
133
+ RUBY
134
+ end
135
+
136
+ # Trigger validation for specific attributes
137
+ #
138
+ # @param [Model::Attributes] attributes The attributes for validation
139
+ #
140
+ # @raises [ValidationError]
141
+ #
142
+ # @return [Model::Attributes]
143
+ def call(attributes)
144
+ validator = new(attributes)
145
+ validator.call
146
+ end
147
+
148
+ # Specify an embedded validator for nested structures
149
+ #
150
+ # @example
151
+ # class UserValidator
152
+ # include ROM::Model::Validator
153
+ #
154
+ # set_model_name 'User'
155
+ #
156
+ # embedded :address do
157
+ # validates :city, :street, :zipcode, presence: true
158
+ # end
159
+ #
160
+ # emebdded :tasks do
161
+ # validates :title, presence: true
162
+ # end
163
+ # end
164
+ #
165
+ # validator = UserAttributes.new(address: {}, tasks: {})
166
+ #
167
+ # validator.valid? # false
168
+ # validator.errors[:address].first # errors for address
169
+ # validator.errors[:tasks] # errors for tasks
170
+ #
171
+ # @api public
172
+ def embedded(name, &block)
173
+ validator_class = Class.new { include ROM::Model::Validator }
174
+ validator_class.class_eval(&block)
175
+ validator_class.set_model_name(name.to_s.classify)
176
+
177
+ embedded_validators[name] = validator_class
178
+
179
+ validates name, presence: true
180
+
181
+ validate do
182
+ value = attributes[name]
183
+
184
+ if value.present?
185
+ Array([value]).flatten.each do |object|
186
+ validator = validator_class.new(object)
187
+ validator.validate
188
+
189
+ if validator.errors.any?
190
+ errors.add(name, validator.errors)
191
+ else
192
+ errors.add(name, [])
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,83 @@
1
+ require 'active_model/validator'
2
+
3
+ module ROM
4
+ module Model
5
+ module Validator
6
+ # Uniqueness validation
7
+ #
8
+ # @api public
9
+ class UniquenessValidator < ActiveModel::EachValidator
10
+ # Relation validator class
11
+ #
12
+ # @api private
13
+ attr_reader :klass
14
+
15
+ # error message
16
+ #
17
+ # @return [String, Symbol]
18
+ #
19
+ # @api private
20
+ attr_reader :message
21
+
22
+ # @api private
23
+ def initialize(options)
24
+ super
25
+ @klass = options.fetch(:class)
26
+ @message = options.fetch(:message) { :taken }
27
+ @scope_keys = options[:scope]
28
+ end
29
+
30
+ # Hook called by ActiveModel internally
31
+ #
32
+ # @api private
33
+ def validate_each(validator, name, value)
34
+ scope = Array(@scope_keys).each_with_object({}) do |key, scope|
35
+ scope[key] = validator.to_model[key]
36
+ end
37
+ validator.errors.add(name, message) unless unique?(name, value, scope)
38
+ end
39
+
40
+ private
41
+
42
+ # Get relation object from the rom env
43
+ #
44
+ # @api private
45
+ def relation
46
+ if relation_name
47
+ rom.relations[relation_name]
48
+ else
49
+ raise "relation must be specified to use uniqueness validation"
50
+ end
51
+ end
52
+
53
+ # Relation name defined on the validator class
54
+ #
55
+ # @api private
56
+ def relation_name
57
+ klass.relation
58
+ end
59
+
60
+ # Shortcut to access global rom env
61
+ #
62
+ # @return [ROM::Env]
63
+ #
64
+ # @api private
65
+ def rom
66
+ ROM.env
67
+ end
68
+
69
+ # Ask relation if a given attribute value is unique
70
+ #
71
+ # This uses `Relation#unique?` interface that not all adapters can
72
+ # implement.
73
+ #
74
+ # @return [TrueClass,FalseClass]
75
+ #
76
+ # @api private
77
+ def unique?(name, value, scope)
78
+ relation.unique?({name => value}.merge(scope))
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ module ROM
2
+ module Model
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ desc "Run mutant against a specific subject"
2
+ task :mutant do
3
+ subject = ARGV.last
4
+ if subject == 'mutant'
5
+ abort "usage: rake mutant SUBJECT\nexample: rake mutant ROM::Header"
6
+ else
7
+ opts = {
8
+ 'include' => 'lib',
9
+ 'require' => 'rom',
10
+ 'use' => 'rspec',
11
+ 'ignore-subject' => "#{subject}#respond_to_missing?"
12
+ }.to_a.map { |k, v| "--#{k} #{v}" }.join(' ')
13
+
14
+ exec("bundle exec mutant #{opts} #{subject}")
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ begin
2
+ require "rubocop/rake_task"
3
+
4
+ Rake::Task[:default].enhance [:rubocop]
5
+
6
+ RuboCop::RakeTask.new do |task|
7
+ task.options << "--display-cop-names"
8
+ end
9
+
10
+ namespace :rubocop do
11
+ desc 'Generate a configuration file acting as a TODO list.'
12
+ task :auto_gen_config do
13
+ exec "bundle exec rubocop --auto-gen-config"
14
+ end
15
+ end
16
+
17
+ rescue LoadError
18
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rom/model/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rom-model"
8
+ spec.version = ROM::Model::VERSION.dup
9
+ spec.authors = ["Piotr Solnica"]
10
+ spec.email = ["piotr.solnica@gmail.com"]
11
+ spec.summary = 'A small collection of extensions useful for data coercion and validation'
12
+ spec.homepage = "http://rom-rb.org"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_runtime_dependency 'rom-support', '~> 0.1'
21
+ spec.add_runtime_dependency 'charlatan', '~> 0.1'
22
+ spec.add_runtime_dependency 'virtus', '~> 1.0', '>= 1.0.5'
23
+ spec.add_runtime_dependency 'activemodel', '>= 3.0', '< 5.0'
24
+ spec.add_runtime_dependency 'i18n'
25
+
26
+ spec.add_development_dependency "bundler"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "rubocop", "~> 0.28.0"
29
+ end
@@ -0,0 +1,32 @@
1
+ shared_context 'database' do
2
+ let(:rom) { ROM.env }
3
+ let(:uri) { DB_URI }
4
+ let(:conn) { Sequel.connect(uri) }
5
+
6
+ def drop_tables
7
+ [:users].each do |name|
8
+ conn.drop_table?(name)
9
+ end
10
+ end
11
+
12
+ before do
13
+ setup = ROM.setup(:sql, conn)
14
+
15
+ drop_tables
16
+
17
+ conn.create_table :users do
18
+ primary_key :id
19
+ column :name, String, null: false
20
+ column :email, String, null: false
21
+ column :birthday, Date
22
+ index :name, unique: true
23
+ check { char_length(name) > 2 }
24
+ end
25
+
26
+ setup.relation(:users)
27
+ end
28
+
29
+ after do
30
+ conn.disconnect
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ if RUBY_ENGINE == 'rbx'
2
+ require "codeclimate-test-reporter"
3
+ CodeClimate::TestReporter.start
4
+ end
5
+
6
+ require 'rom-model'
7
+ require 'rom-sql'
8
+
9
+ begin
10
+ require 'byebug'
11
+ rescue LoadError
12
+ end
13
+
14
+ DB_URI = ENV.fetch('DB_URI', 'postgres://localhost/rom_model')
15
+
16
+ root = Pathname(__FILE__).dirname
17
+
18
+ Dir[root.join('support/*.rb').to_s].each do |f|
19
+ require f
20
+ end
21
+
22
+ Dir[root.join('shared/*.rb').to_s].each do |f|
23
+ require f
24
+ end
25
+
26
+ I18n.load_path << [root.join('../config/locales/en.yml').realpath]
27
+
28
+ RSpec.configure do |config|
29
+ config.order = "random"
30
+ end
31
+
32
+ ROM.use :auto_registration
@@ -0,0 +1,46 @@
1
+ describe ROM::Model::Attributes do
2
+ let(:attributes) do
3
+ Class.new do
4
+ include ROM::Model::Attributes
5
+
6
+ attribute :name, String
7
+
8
+ timestamps
9
+ end
10
+ end
11
+
12
+ describe '.timestamps' do
13
+ it 'provides a way to specify timestamps with default values' do
14
+ expect(attributes.new.created_at).to be_a(DateTime)
15
+ expect(attributes.new.updated_at).to be_a(DateTime)
16
+ end
17
+
18
+ context 'passing in arbritrary names' do
19
+ it 'excludes :created_at when passing in :updated_at' do
20
+ attributes = Class.new {
21
+ include ROM::Model::Attributes
22
+
23
+ timestamps(:updated_at)
24
+ }
25
+
26
+ model = attributes.new
27
+
28
+ expect(model).not_to respond_to(:created_at)
29
+ expect(model).to respond_to(:updated_at)
30
+ end
31
+
32
+ it 'accepts multiple timestamp attribute names' do
33
+ attributes = Class.new {
34
+ include ROM::Model::Attributes
35
+
36
+ timestamps(:published_at, :revised_at)
37
+ }
38
+
39
+ model = attributes.new
40
+
41
+ expect(model).to respond_to(:published_at)
42
+ expect(model).to respond_to(:revised_at)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,116 @@
1
+ describe 'Embedded validators' do
2
+ it 'allows defining a validator for a nested hash' do
3
+ user_validator = Class.new do
4
+ include ROM::Model::Validator
5
+
6
+ set_model_name 'User'
7
+
8
+ validates :name, presence: true
9
+
10
+ embedded :address do
11
+ set_model_name 'Address'
12
+
13
+ validates :street, :city, :zipcode, presence: true
14
+ end
15
+ end
16
+
17
+ attributes = { name: '', address: { street: '', city: '', zipcode: '' } }
18
+
19
+ expect { user_validator.call(attributes) }.to raise_error(
20
+ ROM::Model::ValidationError)
21
+
22
+ validator = user_validator.new(attributes)
23
+
24
+ expect(validator).to_not be_valid
25
+
26
+ expect(validator.errors[:name]).to include("can't be blank")
27
+
28
+ address_errors = validator.errors[:address].first
29
+
30
+ expect(address_errors).to_not be_empty
31
+
32
+ expect(address_errors[:street]).to include("can't be blank")
33
+ expect(address_errors[:city]).to include("can't be blank")
34
+ expect(address_errors[:zipcode]).to include("can't be blank")
35
+ end
36
+
37
+ it 'allows defining a validator for a nested array' do
38
+ user_validator = Class.new do
39
+ include ROM::Model::Validator
40
+
41
+ set_model_name 'User'
42
+
43
+ validates :name, presence: true
44
+
45
+ embedded :tasks do
46
+ set_model_name 'Task'
47
+
48
+ validates :title, presence: true
49
+ end
50
+ end
51
+
52
+ attributes = {
53
+ name: '',
54
+ tasks: [
55
+ { title: '' },
56
+ { title: 'Two' }
57
+ ]
58
+ }
59
+
60
+ expect { user_validator.call(attributes) }.to raise_error(
61
+ ROM::Model::ValidationError)
62
+
63
+ validator = user_validator.new(attributes)
64
+
65
+ expect(validator).to_not be_valid
66
+
67
+ expect(validator.errors[:name]).to include("can't be blank")
68
+
69
+ task_errors = validator.errors[:tasks]
70
+
71
+ expect(task_errors).to_not be_empty
72
+
73
+ expect(task_errors[0][:title]).to include("can't be blank")
74
+ expect(task_errors[1]).to be_empty
75
+ end
76
+
77
+ it 'validates presence of the nested structure' do
78
+ user_validator = Class.new do
79
+ include ROM::Model::Validator
80
+
81
+ set_model_name 'User'
82
+
83
+ validates :name, presence: true
84
+
85
+ embedded :tasks do
86
+ set_model_name 'Task'
87
+
88
+ validates :title, presence: true
89
+ end
90
+ end
91
+
92
+ validator = user_validator.new(name: '')
93
+ validator.validate
94
+
95
+ expect(validator.errors[:name]).to include("can't be blank")
96
+ expect(validator.errors[:tasks]).to include("can't be blank")
97
+ end
98
+
99
+ it 'exposes registered validators in embedded_validators hash' do
100
+ user_validator = Class.new do
101
+ include ROM::Model::Validator
102
+
103
+ set_model_name 'User'
104
+
105
+ validates :name, presence: true
106
+
107
+ embedded :tasks do
108
+ set_model_name 'Task'
109
+
110
+ validates :title, presence: true
111
+ end
112
+ end
113
+
114
+ expect(user_validator.embedded_validators[:tasks]).to be_present
115
+ end
116
+ end
@@ -0,0 +1,165 @@
1
+ describe 'Validation' do
2
+ include_context 'database'
3
+
4
+ subject(:validator) { user_validator.new(attributes) }
5
+
6
+ let(:user_attrs) do
7
+ Class.new {
8
+ include ROM::Model::Attributes
9
+
10
+ set_model_name 'User'
11
+
12
+ attribute :name, String
13
+ attribute :email, String
14
+ attribute :birthday, Date
15
+ }
16
+ end
17
+
18
+ let(:user_validator) do
19
+ Class.new {
20
+ include ROM::Model::Validator
21
+
22
+ relation :users
23
+
24
+ validates :name, presence: true, uniqueness: { message: 'TAKEN!' }
25
+ validates :email, uniqueness: true
26
+
27
+ def self.name
28
+ 'User'
29
+ end
30
+ }
31
+ end
32
+
33
+ before { ROM.finalize }
34
+
35
+ describe '#call' do
36
+ let(:attributes) { user_attrs.new }
37
+
38
+ it 'raises validation error when attributes are not valid' do
39
+ expect { validator.call }.to raise_error(ROM::Model::ValidationError)
40
+ end
41
+ end
42
+
43
+ describe "#validate" do
44
+ let(:attributes) { user_attrs.new }
45
+
46
+ it "sets errors when attributes are not valid" do
47
+ validator.validate
48
+ expect(validator.errors[:name]).to eql(["can't be blank"])
49
+ end
50
+ end
51
+
52
+ describe ':presence' do
53
+ let(:attributes) { user_attrs.new(name: '') }
54
+
55
+ it 'sets error messages' do
56
+ expect(validator).to_not be_valid
57
+ expect(validator.errors[:name]).to eql(["can't be blank"])
58
+ end
59
+ end
60
+
61
+ describe ':uniqueness' do
62
+ let(:attributes) { user_attrs.new(name: 'Jane', email: 'jane@doe.org') }
63
+
64
+ it 'sets default error messages' do
65
+ rom.relations.users.insert(name: 'Jane', email: 'jane@doe.org')
66
+
67
+ expect(validator).to_not be_valid
68
+ expect(validator.errors[:email]).to eql(['has already been taken'])
69
+ end
70
+
71
+ it 'sets custom error messages' do
72
+ rom.relations.users.insert(name: 'Jane', email: 'jane@doe.org')
73
+
74
+ expect(validator).to_not be_valid
75
+ expect(validator.errors[:name]).to eql(['TAKEN!'])
76
+ end
77
+
78
+ context 'with unique attributes within a scope' do
79
+ let(:user_validator) do
80
+ Class.new {
81
+ include ROM::Model::Validator
82
+
83
+ relation :users
84
+
85
+ validates :email, uniqueness: {scope: :name}
86
+
87
+ def self.name
88
+ 'User'
89
+ end
90
+ }
91
+ end
92
+
93
+ let(:doubly_scoped_validator) do
94
+ Class.new {
95
+ include ROM::Model::Validator
96
+
97
+ relation :users
98
+
99
+ validates :email, uniqueness: {scope: [:name, :birthday]}
100
+
101
+ def self.name
102
+ 'User'
103
+ end
104
+ }
105
+ end
106
+
107
+ it 'does not add errors' do
108
+ rom.relations.users.insert(name: 'Jane', email: 'jane+doe@doe.org')
109
+ attributes = user_attrs.new(name: 'Jane', email: 'jane@doe.org', birthday: Date.parse('2014-12-12'))
110
+ validator = user_validator.new(attributes)
111
+ expect(validator).to be_valid
112
+ end
113
+
114
+ it 'adds an error when the doubly scoped validation fails' do
115
+ attributes = user_attrs.new(name: 'Jane', email: 'jane@doe.org', birthday: Date.parse('2014-12-12'))
116
+ validator = doubly_scoped_validator.new(attributes)
117
+ expect(validator).to be_valid
118
+
119
+ rom.relations.users.insert(attributes.attributes)
120
+ expect(validator).to_not be_valid
121
+
122
+ attributes = user_attrs.new(name: 'Jane', email: 'jane+doe@doe.org', birthday: Date.parse('2014-12-12'))
123
+ validator = doubly_scoped_validator.new(attributes)
124
+ expect(validator).to be_valid
125
+ end
126
+ end
127
+
128
+ describe 'with missing relation' do
129
+ let(:user_validator) do
130
+ Class.new {
131
+ include ROM::Model::Validator
132
+
133
+ validates :email, uniqueness: true
134
+
135
+ def self.name
136
+ 'User'
137
+ end
138
+ }
139
+ end
140
+
141
+ it 'raises a helpful error' do
142
+ validator = user_validator.new(user_attrs.new)
143
+ expect {
144
+ validator.valid?
145
+ }.to raise_error(/relation must be specified/)
146
+ end
147
+ end
148
+ end
149
+
150
+ describe '#method_missing' do
151
+ let(:attributes) { { name: 'Jane' } }
152
+
153
+ it 'returns attribute value if present' do
154
+ expect(validator.name).to eql('Jane')
155
+ end
156
+
157
+ it 'returns nil if attribute is not present' do
158
+ expect(validator.email).to be(nil)
159
+ end
160
+
161
+ it 'raises error when name does not match any of the attributes' do
162
+ expect { validator.foobar }.to raise_error(NoMethodError, /foobar/)
163
+ end
164
+ end
165
+ end
metadata ADDED
@@ -0,0 +1,196 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rom-model
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Solnica
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rom-support
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: charlatan
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: virtus
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 1.0.5
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '1.0'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.0.5
61
+ - !ruby/object:Gem::Dependency
62
+ name: activemodel
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ - - "<"
69
+ - !ruby/object:Gem::Version
70
+ version: '5.0'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '3.0'
78
+ - - "<"
79
+ - !ruby/object:Gem::Version
80
+ version: '5.0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: i18n
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: bundler
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ - !ruby/object:Gem::Dependency
110
+ name: rake
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ - !ruby/object:Gem::Dependency
124
+ name: rubocop
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: 0.28.0
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: 0.28.0
137
+ description:
138
+ email:
139
+ - piotr.solnica@gmail.com
140
+ executables: []
141
+ extensions: []
142
+ extra_rdoc_files: []
143
+ files:
144
+ - ".gitignore"
145
+ - ".rspec"
146
+ - ".travis.yml"
147
+ - CHANGELOG.md
148
+ - Gemfile
149
+ - LICENSE
150
+ - README.md
151
+ - Rakefile
152
+ - config/locales/en.yml
153
+ - lib/rom-model.rb
154
+ - lib/rom/model.rb
155
+ - lib/rom/model/attributes.rb
156
+ - lib/rom/model/validator.rb
157
+ - lib/rom/model/validator/uniqueness_validator.rb
158
+ - lib/rom/model/version.rb
159
+ - rakelib/mutant.rake
160
+ - rakelib/rubocop.rake
161
+ - rom-model.gemspec
162
+ - spec/shared/database.rb
163
+ - spec/spec_helper.rb
164
+ - spec/unit/attributes_spec.rb
165
+ - spec/unit/validator/embedded_spec.rb
166
+ - spec/unit/validator_spec.rb
167
+ homepage: http://rom-rb.org
168
+ licenses:
169
+ - MIT
170
+ metadata: {}
171
+ post_install_message:
172
+ rdoc_options: []
173
+ require_paths:
174
+ - lib
175
+ required_ruby_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ required_rubygems_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: '0'
185
+ requirements: []
186
+ rubyforge_project:
187
+ rubygems_version: 2.4.5
188
+ signing_key:
189
+ specification_version: 4
190
+ summary: A small collection of extensions useful for data coercion and validation
191
+ test_files:
192
+ - spec/shared/database.rb
193
+ - spec/spec_helper.rb
194
+ - spec/unit/attributes_spec.rb
195
+ - spec/unit/validator/embedded_spec.rb
196
+ - spec/unit/validator_spec.rb