semantic_attributes 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +99 -0
- data/MIT-LICENSE +20 -0
- data/README +54 -0
- data/Rakefile +37 -0
- data/gist.rdoc +208 -0
- data/lib/active_record/validation_recursion_control.rb +33 -0
- data/lib/core_ext/class.rb +14 -0
- data/lib/predicates/aliased.rb +22 -0
- data/lib/predicates/association.rb +43 -0
- data/lib/predicates/base.rb +93 -0
- data/lib/predicates/blacklisted.rb +23 -0
- data/lib/predicates/domain.rb +31 -0
- data/lib/predicates/email.rb +42 -0
- data/lib/predicates/enumerated.rb +23 -0
- data/lib/predicates/hex_color.rb +24 -0
- data/lib/predicates/length.rb +71 -0
- data/lib/predicates/number.rb +104 -0
- data/lib/predicates/pattern.rb +22 -0
- data/lib/predicates/phone_number.rb +62 -0
- data/lib/predicates/required.rb +22 -0
- data/lib/predicates/same_as.rb +17 -0
- data/lib/predicates/size.rb +2 -0
- data/lib/predicates/time.rb +43 -0
- data/lib/predicates/unique.rb +71 -0
- data/lib/predicates/url.rb +62 -0
- data/lib/predicates/usa_state.rb +87 -0
- data/lib/predicates/usa_zip_code.rb +25 -0
- data/lib/predicates/whitelisted.rb +2 -0
- data/lib/predicates.rb +3 -0
- data/lib/semantic_attributes/attribute.rb +46 -0
- data/lib/semantic_attributes/attribute_formats.rb +67 -0
- data/lib/semantic_attributes/locale/en.yml +31 -0
- data/lib/semantic_attributes/predicates.rb +170 -0
- data/lib/semantic_attributes/set.rb +40 -0
- data/lib/semantic_attributes/version.rb +3 -0
- data/lib/semantic_attributes.rb +37 -0
- data/semantic_attributes.gemspec +29 -0
- data/test/db/database.yml +3 -0
- data/test/db/models.rb +38 -0
- data/test/db/schema.rb +33 -0
- data/test/fixtures/addresses.yml +15 -0
- data/test/fixtures/roles.yml +4 -0
- data/test/fixtures/roles_users.yml +6 -0
- data/test/fixtures/services.yml +6 -0
- data/test/fixtures/subscriptions.yml +16 -0
- data/test/fixtures/users.yml +20 -0
- data/test/test_helper.rb +67 -0
- data/test/unit/active_record_predicates_test.rb +88 -0
- data/test/unit/attribute_formats_test.rb +40 -0
- data/test/unit/inheritance_test.rb +23 -0
- data/test/unit/predicates/aliased_test.rb +17 -0
- data/test/unit/predicates/association_predicate_test.rb +51 -0
- data/test/unit/predicates/base_test.rb +53 -0
- data/test/unit/predicates/blacklisted_predicate_test.rb +28 -0
- data/test/unit/predicates/domain_predicate_test.rb +27 -0
- data/test/unit/predicates/email_test.rb +82 -0
- data/test/unit/predicates/enumerated_predicate_test.rb +22 -0
- data/test/unit/predicates/hex_color_predicate_test.rb +29 -0
- data/test/unit/predicates/length_predicate_test.rb +85 -0
- data/test/unit/predicates/number_test.rb +109 -0
- data/test/unit/predicates/pattern_predicate_test.rb +29 -0
- data/test/unit/predicates/phone_number_predicate_test.rb +41 -0
- data/test/unit/predicates/required_predicate_test.rb +13 -0
- data/test/unit/predicates/same_as_predicate_test.rb +19 -0
- data/test/unit/predicates/time_test.rb +49 -0
- data/test/unit/predicates/unique_test.rb +58 -0
- data/test/unit/predicates/url_test.rb +86 -0
- data/test/unit/predicates/usa_state_test.rb +31 -0
- data/test/unit/predicates/usa_zip_code_test.rb +42 -0
- data/test/unit/semantic_attribute_test.rb +18 -0
- data/test/unit/semantic_attributes_test.rb +29 -0
- data/test/unit/validations_test.rb +121 -0
- metadata +235 -0
data/lib/predicates.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# a field with its assocatied predicates
|
2
|
+
class SemanticAttributes::Attribute
|
3
|
+
attr_reader :field
|
4
|
+
def initialize(field)
|
5
|
+
@field = field.to_sym
|
6
|
+
@set = []
|
7
|
+
end
|
8
|
+
|
9
|
+
# whether this attribute has the given predicate
|
10
|
+
# accepts the 'short name' of a predicate (e.g. "phone_number" instead of Predicates::PhoneNumber)
|
11
|
+
def has?(predicate)
|
12
|
+
predicate = class_of predicate
|
13
|
+
@set.any? {|item| item.is_a? predicate}
|
14
|
+
end
|
15
|
+
|
16
|
+
# retrieves a predicate by name from the attribute
|
17
|
+
# accepts the 'short name' of a predicate (e.g. "phone_number" instead of Predicates::PhoneNumber)
|
18
|
+
def get(predicate)
|
19
|
+
predicate = class_of predicate
|
20
|
+
@set.find {|item| item.is_a? predicate}
|
21
|
+
end
|
22
|
+
|
23
|
+
# adds a predicate to the attribute, with options
|
24
|
+
# accepts the 'short name' of a predicate (e.g. "phone_number" instead of Predicates::PhoneNumber)
|
25
|
+
def add(predicate, options = {})
|
26
|
+
predicate = class_of predicate
|
27
|
+
@set << predicate.new(self.field, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# The list of predicates, without any shortcuts
|
31
|
+
def predicates
|
32
|
+
@set
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
# fully-qualified-name of a predicate
|
38
|
+
def fqn(short_name)
|
39
|
+
"Predicates::#{short_name.to_s.camelize}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# the actual predicate class for the given name
|
43
|
+
def class_of(short_name)
|
44
|
+
fqn(short_name).constantize
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module SemanticAttributes #:nodoc:
|
2
|
+
class MissingAttribute < NameError
|
3
|
+
def initialize(attr)
|
4
|
+
super("#{attr} is not defined. You may have a typo or need to run migrations.")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module AttributeFormats
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
def respond_to?(method_name, *args)
|
14
|
+
if md = method_name.to_s.match(/_for_human$/) and semantic_attributes.include?(md.pre_match)
|
15
|
+
true
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def method_missing(method_name, *args, &block)
|
24
|
+
if md = method_name.to_s.match(/_for_human$/) and semantic_attributes.include?(md.pre_match)
|
25
|
+
self.class.humanize(md.pre_match, self.send(md.pre_match))
|
26
|
+
else
|
27
|
+
begin
|
28
|
+
super
|
29
|
+
rescue
|
30
|
+
raise $!, $!.to_s, caller
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
# converts the object into human format according to the predicates for the attribute.
|
37
|
+
# the object may really be anything: string, integer, date, etc.
|
38
|
+
def humanize(attr, obj)
|
39
|
+
self.semantic_attributes[attr].predicates.inject(obj) { |val, predicate| val = predicate.to_human(val) }
|
40
|
+
end
|
41
|
+
|
42
|
+
# converts the object into machine format according to the predicates for the attribute.
|
43
|
+
# the object should be a simple object like a string, array, or hash. but really, it depends on the the predicates.
|
44
|
+
def normalize(attr, obj)
|
45
|
+
self.semantic_attributes[attr].predicates.inject(obj) { |val, predicate| val = predicate.normalize(val) }
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def define_normalization_method_for(attr)
|
51
|
+
self.define_attribute_methods if self.respond_to? :attribute_methods_generated? and !self.attribute_methods_generated?
|
52
|
+
|
53
|
+
writer = "#{attr}_with_normalization="
|
54
|
+
old_writer = "#{attr}_without_normalization=".to_sym
|
55
|
+
unless method_defined? writer
|
56
|
+
define_method writer do |val|
|
57
|
+
send(old_writer, self.class.normalize(attr, val))
|
58
|
+
end
|
59
|
+
alias_method_chain "#{attr}=", :normalization
|
60
|
+
end
|
61
|
+
rescue NameError
|
62
|
+
raise SemanticAttributes::MissingAttribute.new(attr)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# based on ActiveRecord's locale/en.yml file
|
2
|
+
en:
|
3
|
+
semantic-attributes:
|
4
|
+
errors:
|
5
|
+
messages:
|
6
|
+
us_zip_code: 'must be a US zip code.'
|
7
|
+
required: 'is required.'
|
8
|
+
exclusion: "is not an allowed option."
|
9
|
+
inclusion: "is not an allowed option."
|
10
|
+
domain: "must be a simple domain."
|
11
|
+
email: 'must be an email address.'
|
12
|
+
hex: "must be a hex color."
|
13
|
+
invalid: "is invalid."
|
14
|
+
phone: "must be a phone number."
|
15
|
+
time: "must be a point in time."
|
16
|
+
taken: "has already been taken."
|
17
|
+
url: 'must be a valid URL.'
|
18
|
+
same_as: "must match."
|
19
|
+
us_state_or_territory: "must be a US state or territory."
|
20
|
+
us_state: "must be a US state."
|
21
|
+
inexact_length: "must be %{count} characters long"
|
22
|
+
wrong_length: "must be %{min} to %{max} characters long"
|
23
|
+
too_short: "must be more than %{min} characters long"
|
24
|
+
too_long: "must be less than %{max} characters long"
|
25
|
+
not_a_number: "must be a number."
|
26
|
+
between: "must be a number from %{min} to %{max}."
|
27
|
+
greater_than_or_equal_to: "must be a number at least %{min}."
|
28
|
+
less_than_or_equal_to: "must be a number no more than %{max}."
|
29
|
+
greater_than: "must be a number greater than %{min}."
|
30
|
+
less_than: "must be a number less than %{max}."
|
31
|
+
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# active record tie-ins
|
2
|
+
module SemanticAttributes
|
3
|
+
module Predicates
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
extend ClassMethods
|
7
|
+
base.semantic_attributes = SemanticAttributes::Set.new
|
8
|
+
attribute_method_suffix '_valid?'
|
9
|
+
validate :validate_predicates
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def semantic_attributes
|
14
|
+
self.class.semantic_attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
# the validation hook that checks all predicates
|
18
|
+
def validate_predicates
|
19
|
+
semantic_attributes.each do |attribute|
|
20
|
+
applicable_predicates = attribute.predicates.select{|p| validate_predicate?(p)}
|
21
|
+
|
22
|
+
next if applicable_predicates.empty?
|
23
|
+
|
24
|
+
# skip validations on associations that aren't already loaded
|
25
|
+
# note: it's not worth skipping a has_one, since there's no way to distinguish
|
26
|
+
# between loaded and absent without a query.
|
27
|
+
if reflection = self.class.reflect_on_association(attribute.field.to_sym)
|
28
|
+
if reflection.collection?
|
29
|
+
next unless self.association(attribute.field).loaded?
|
30
|
+
elsif reflection.macro == :belongs_to and self[reflection.foreign_key]
|
31
|
+
next unless self.association(attribute.field).loaded?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
value = self.send(attribute.field)
|
36
|
+
applicable_predicates.each do |predicate|
|
37
|
+
if value.blank?
|
38
|
+
# it's empty, so add an error or not but either way move along
|
39
|
+
self.errors.add(attribute.field, predicate.error) unless predicate.allow_empty?
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
unless predicate.validate(value, self)
|
44
|
+
self.errors.add(attribute.field, predicate.error)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
# Returns true if this attribute would pass validation during the next save.
|
52
|
+
# Intended to be called via attribute suffix, like:
|
53
|
+
# User#login_valid?
|
54
|
+
def attribute_valid?(attr)
|
55
|
+
semantic_attributes[attr.to_sym].predicates.all? do |p|
|
56
|
+
!validate_predicate?(p) or (self.send(attr).blank? and p.allow_empty?) or p.validate(self.send(attr), self)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns true if the given predicate (for a specific attribute) should be validated.
|
61
|
+
def validate_predicate?(predicate)
|
62
|
+
case predicate.validate_if
|
63
|
+
when Symbol
|
64
|
+
return false unless send(predicate.validate_if)
|
65
|
+
|
66
|
+
when Proc
|
67
|
+
return false unless predicate.validate_if.call(self)
|
68
|
+
end
|
69
|
+
|
70
|
+
case predicate.validate_on
|
71
|
+
when :create
|
72
|
+
return false unless new_record?
|
73
|
+
|
74
|
+
when :update
|
75
|
+
return false if new_record?
|
76
|
+
end
|
77
|
+
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
module ClassMethods
|
82
|
+
def semantic_attributes
|
83
|
+
@semantic_attributes ||= SemanticAttributes::Set.new
|
84
|
+
end
|
85
|
+
|
86
|
+
def semantic_attributes=(val)
|
87
|
+
@semantic_attributes = val || SemanticAttributes::Set.new
|
88
|
+
end
|
89
|
+
|
90
|
+
def inherited(klass)
|
91
|
+
klass.semantic_attributes = self.semantic_attributes.dup
|
92
|
+
super
|
93
|
+
end
|
94
|
+
|
95
|
+
# Provides sugary syntax for adding and querying predicates
|
96
|
+
#
|
97
|
+
# The syntax supports the following forms:
|
98
|
+
# #{attribute}_is_#{predicate}(options = {})
|
99
|
+
# #{attribute}_is_a_#{predicate}(options = {})
|
100
|
+
# #{attribute}_is_an_#{predicate}(options = {})
|
101
|
+
# #{attribute}_has_#{predicate}(options = {})
|
102
|
+
# #{attribute}_has_a_#{predicate}(options = {})
|
103
|
+
# #{attribute}_has_an_#{predicate}(options = {})
|
104
|
+
#
|
105
|
+
# You may also add required-ness into the declaration:
|
106
|
+
# #{attribute}_is_a_required_#{predicate}(options = {})
|
107
|
+
#
|
108
|
+
# If you want to assign a predicate to multiple fields, you may replace the attribute component with the word 'fields', and pass a field list as the first argument, like this:
|
109
|
+
# fields_are_#{predicate}(fields = [], options = {})
|
110
|
+
#
|
111
|
+
# Each form may also have a question mark at the end, to query whether the attribute has the predicate.
|
112
|
+
#
|
113
|
+
# In order to avoid clashing with other method_missing setups, this syntax is checked *last*, after all other method_missing metaprogramming attempts have failed.
|
114
|
+
def method_missing(name, *args)
|
115
|
+
begin
|
116
|
+
super
|
117
|
+
rescue NameError
|
118
|
+
if /^(.*)_(is|has|are)_(an?_)?(required_)?([^?]*)(\?)?$/.match(name.to_s)
|
119
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
120
|
+
options[:or_empty] = false if !$4.nil?
|
121
|
+
fields = ($1 == 'fields') ? args.map(&:to_s) : [$1]
|
122
|
+
|
123
|
+
predicate = $5
|
124
|
+
if $6 == '?'
|
125
|
+
self.semantic_attributes[fields.first].has? predicate
|
126
|
+
else
|
127
|
+
args = [predicate]
|
128
|
+
args << options if options
|
129
|
+
fields.each do |field|
|
130
|
+
# TODO: create a less sugary method that may be used programmatically and takes care of defining the normalization method properly
|
131
|
+
define_normalization_method_for field
|
132
|
+
self.semantic_attributes[field].add *args
|
133
|
+
end
|
134
|
+
end
|
135
|
+
else
|
136
|
+
raise
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Provides a way to pre-validate a single value out of context of
|
142
|
+
# an entire record. This is helpful for validating parts of a form
|
143
|
+
# before it has been submitted.
|
144
|
+
#
|
145
|
+
# For values that are (in)valid only in context, such as the common
|
146
|
+
# :password_confirmation (which is only valid with a matching :password),
|
147
|
+
# additional values may be specified.
|
148
|
+
#
|
149
|
+
# WARNING: I still have not figured out what to do about differences in
|
150
|
+
# validation for new/existing records.
|
151
|
+
#
|
152
|
+
# Returns first error message if value is expected invalid.
|
153
|
+
#
|
154
|
+
# Example:
|
155
|
+
# User.expected_error_for(:username, "bob")
|
156
|
+
# => "has already been taken."
|
157
|
+
# User.expected_error_for(:username, "bob2392")
|
158
|
+
# => nil
|
159
|
+
# User.expected_error_for(:password_confirmation, "mismatched", :password => "opensesame")
|
160
|
+
# => "must be the same as password."
|
161
|
+
def expected_error_for(attribute, value, extra_values = {})
|
162
|
+
@record = self.new(extra_values)
|
163
|
+
semantic_attributes[attribute.to_sym].predicates.each do |predicate|
|
164
|
+
return predicate.error_message unless predicate.validate(value, @record)
|
165
|
+
end
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# a set of SemanticAttributes::Attribute objects, which themselves contain Predicates
|
2
|
+
class SemanticAttributes::Set
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@set = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(semantic_attribute)
|
10
|
+
unless semantic_attribute.is_a? SemanticAttributes::Attribute
|
11
|
+
raise ArgumentError, 'Must pass a SemanticAttributes::Attribute object'
|
12
|
+
end
|
13
|
+
@set << semantic_attribute
|
14
|
+
end
|
15
|
+
|
16
|
+
# method for field lookups. creates the field if it doesn't exist.
|
17
|
+
def [](field)
|
18
|
+
field = field.to_sym
|
19
|
+
semantic_attribute = @set.find {|i| i.field == field}
|
20
|
+
self.add(semantic_attribute = SemanticAttributes::Attribute.new(field)) unless semantic_attribute
|
21
|
+
semantic_attribute
|
22
|
+
end
|
23
|
+
|
24
|
+
# method for field lookups which does *not* create the field if it doesn't exist
|
25
|
+
def include?(field)
|
26
|
+
field = field.to_sym
|
27
|
+
@set.find {|i| i.field == field} ? true : false
|
28
|
+
end
|
29
|
+
|
30
|
+
def each
|
31
|
+
@set.each do |i|
|
32
|
+
yield i
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def initialize_copy(from)
|
38
|
+
@set = from.instance_variable_get('@set').clone
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
I18n.load_path << File.dirname(__FILE__) + '/semantic_attributes/locale/en.yml'
|
2
|
+
|
3
|
+
module SemanticAttributes; end
|
4
|
+
|
5
|
+
require 'core_ext/class'
|
6
|
+
require 'active_record/validation_recursion_control'
|
7
|
+
require 'predicates'
|
8
|
+
require 'predicates/base'
|
9
|
+
require 'predicates/enumerated'
|
10
|
+
require 'predicates/pattern'
|
11
|
+
require 'predicates/aliased'
|
12
|
+
require 'predicates/association'
|
13
|
+
require 'predicates/blacklisted'
|
14
|
+
require 'predicates/domain'
|
15
|
+
require 'predicates/email'
|
16
|
+
require 'predicates/hex_color'
|
17
|
+
require 'predicates/length'
|
18
|
+
require 'predicates/number'
|
19
|
+
require 'predicates/phone_number'
|
20
|
+
require 'predicates/required'
|
21
|
+
require 'predicates/same_as'
|
22
|
+
require 'predicates/size'
|
23
|
+
require 'predicates/time'
|
24
|
+
require 'predicates/unique'
|
25
|
+
require 'predicates/url'
|
26
|
+
require 'predicates/usa_state'
|
27
|
+
require 'predicates/usa_zip_code'
|
28
|
+
require 'predicates/aliased'
|
29
|
+
require 'semantic_attributes/attribute'
|
30
|
+
require 'semantic_attributes/attribute_formats'
|
31
|
+
require 'semantic_attributes/predicates'
|
32
|
+
require 'semantic_attributes/set'
|
33
|
+
require 'semantic_attributes/version'
|
34
|
+
|
35
|
+
ActiveRecord::Base.send(:include, SemanticAttributes::Predicates)
|
36
|
+
ActiveRecord::Base.send(:include, SemanticAttributes::AttributeFormats)
|
37
|
+
ActiveRecord::Base.send(:include, ActiveRecord::ValidationRecursionControl)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
# Maintain your gem's version:
|
4
|
+
require "semantic_attributes/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'semantic_attributes'
|
8
|
+
s.version = SemanticAttributes::VERSION
|
9
|
+
s.authors = ["Lance Ivy"]
|
10
|
+
s.email = 'lance@kickstarter.com'
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.homepage = %q{http://github.com/kickstarter/semantic-attributes}
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
s.summary = 'A validation library for ActiveRecord models.'
|
15
|
+
s.description = 'A validation library for ActiveRecord models that allows ' +
|
16
|
+
'introspection (User.name_is_required?) and supports database' +
|
17
|
+
' normalization (aka "form input cleaning").'
|
18
|
+
|
19
|
+
# s.add_dependency "activerecord", ">= 3.0.12"
|
20
|
+
s.add_dependency "rails", "~> 3.2.2"
|
21
|
+
|
22
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
23
|
+
s.add_development_dependency "sqlite3"
|
24
|
+
s.add_development_dependency "rake", "0.8.7"
|
25
|
+
s.add_development_dependency "mocha", ">= 0.10.5"
|
26
|
+
|
27
|
+
s.files = `git ls-files`.split("\n")
|
28
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
29
|
+
end
|
data/test/db/models.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# A set of classes design to offer not only every association, but every pair of associations.
|
2
|
+
# For example, there's a has_one paired with a normal belongs_to, and another has_one paired with a polymorphic belongs_to.
|
3
|
+
# To use, mix the PluginTestModels module into the TestCase
|
4
|
+
module PluginTestModels
|
5
|
+
def self.included(base)
|
6
|
+
base.set_fixture_class({
|
7
|
+
:addresses => PluginTestModels::Address,
|
8
|
+
:users => PluginTestModels::User,
|
9
|
+
:services => PluginTestModels::Service,
|
10
|
+
:subscriptions => PluginTestModels::Subscription,
|
11
|
+
:roles => PluginTestModels::Role
|
12
|
+
})
|
13
|
+
end
|
14
|
+
|
15
|
+
class Address < ActiveRecord::Base
|
16
|
+
belongs_to :addressable, :polymorphic => true
|
17
|
+
end
|
18
|
+
|
19
|
+
class User < ActiveRecord::Base
|
20
|
+
has_and_belongs_to_many :roles
|
21
|
+
has_one :subscription
|
22
|
+
has_one :address, :as => :addressable
|
23
|
+
end
|
24
|
+
|
25
|
+
class Service < ActiveRecord::Base
|
26
|
+
has_many :subscriptions
|
27
|
+
has_many :users, :through => :subscriptions
|
28
|
+
end
|
29
|
+
|
30
|
+
class Subscription < ActiveRecord::Base
|
31
|
+
belongs_to :service
|
32
|
+
belongs_to :user
|
33
|
+
end
|
34
|
+
|
35
|
+
class Role < ActiveRecord::Base
|
36
|
+
has_and_belongs_to_many :users
|
37
|
+
end
|
38
|
+
end
|
data/test/db/schema.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 1) do
|
2
|
+
|
3
|
+
create_table :addresses, :force => true do |t|
|
4
|
+
t.column :name, :string
|
5
|
+
t.column :addressable_id, :integer
|
6
|
+
t.column :addressable_type, :string
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table :users, :force => true do |t|
|
10
|
+
t.column :login, :string
|
11
|
+
t.column :first_name, :string
|
12
|
+
t.column :last_name, :string
|
13
|
+
t.column :cell, :string
|
14
|
+
end
|
15
|
+
|
16
|
+
create_table :roles_users, :force => true, :id => false do |t|
|
17
|
+
t.column :user_id, :integer
|
18
|
+
t.column :role_id, :integer
|
19
|
+
end
|
20
|
+
|
21
|
+
create_table :services, :force => true do |t|
|
22
|
+
t.column :name, :string
|
23
|
+
end
|
24
|
+
|
25
|
+
create_table :subscriptions, :force => true do |t|
|
26
|
+
t.column :service_id, :integer
|
27
|
+
t.column :user_id, :integer
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table :roles, :force => true do |t|
|
31
|
+
t.column :name, :string
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
bobs_home:
|
2
|
+
id: 1
|
3
|
+
name: Home
|
4
|
+
addressable_id: 1
|
5
|
+
addressable_type: 'PluginTestModels::User'
|
6
|
+
bobs_work:
|
7
|
+
id: 2
|
8
|
+
name: Work
|
9
|
+
addressable_id: 1
|
10
|
+
addressable_type: 'PluginTestModels::User'
|
11
|
+
sues_home:
|
12
|
+
id: 3
|
13
|
+
name: Home
|
14
|
+
addressable_id: 2
|
15
|
+
addressable_type: 'PluginTestModels::User'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
bob:
|
2
|
+
id: 1
|
3
|
+
login: bob
|
4
|
+
first_name: Bobby
|
5
|
+
last_name: Billiards
|
6
|
+
sue:
|
7
|
+
id: 2
|
8
|
+
login: sue
|
9
|
+
first_name: Sue
|
10
|
+
last_name: Swagger
|
11
|
+
george:
|
12
|
+
id: 3
|
13
|
+
login: george
|
14
|
+
first_name: George
|
15
|
+
last_name: Ginormous
|
16
|
+
michael:
|
17
|
+
id: 4
|
18
|
+
login: michael
|
19
|
+
first_name: Michael
|
20
|
+
last_name: Mindful
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
ENV["RAILS_ENV"] = "test"
|
2
|
+
|
3
|
+
# load the support libraries
|
4
|
+
require 'test/unit'
|
5
|
+
require 'rubygems'
|
6
|
+
require 'active_record'
|
7
|
+
require 'active_record/fixtures'
|
8
|
+
require 'active_support/time'
|
9
|
+
|
10
|
+
# load the code-to-be-tested
|
11
|
+
require 'semantic_attributes'
|
12
|
+
|
13
|
+
Time.zone = 'UTC'
|
14
|
+
|
15
|
+
# establish the database connection
|
16
|
+
ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/db/database.yml'))
|
17
|
+
ActiveRecord::Base.establish_connection('semantic_attributes_test')
|
18
|
+
|
19
|
+
# capture the logging
|
20
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/test.log")
|
21
|
+
|
22
|
+
# load the schema ... silently
|
23
|
+
ActiveRecord::Migration.verbose = false
|
24
|
+
load(File.dirname(__FILE__) + "/db/schema.rb")
|
25
|
+
|
26
|
+
# load the ActiveRecord models
|
27
|
+
require File.dirname(__FILE__) + '/db/models'
|
28
|
+
|
29
|
+
# configure the TestCase settings
|
30
|
+
class SemanticAttributes::TestCase < ActiveSupport::TestCase
|
31
|
+
include ActiveRecord::TestFixtures
|
32
|
+
include PluginTestModels
|
33
|
+
|
34
|
+
self.use_transactional_fixtures = true
|
35
|
+
self.use_instantiated_fixtures = false
|
36
|
+
self.fixture_path = File.dirname(__FILE__) + '/fixtures/'
|
37
|
+
|
38
|
+
fixtures :all
|
39
|
+
end
|
40
|
+
|
41
|
+
class ActiveRecord::Base
|
42
|
+
# Aids the management of per-test semantics.
|
43
|
+
#
|
44
|
+
# Examples:
|
45
|
+
#
|
46
|
+
# User.stub_semantics_with(:email => :email)
|
47
|
+
# User.stub_semantics_with(:email => [:email, :unique])
|
48
|
+
# User.stub_semantics_with(:email => {:length => {:above => 5}})
|
49
|
+
# User.stub_semantics_with(:email => [:email, {:length => {:above => 5}}])
|
50
|
+
def self.stub_semantics_with(attr_predicates = {})
|
51
|
+
semantics = SemanticAttributes::Set.new
|
52
|
+
attr_predicates.each do |attr, predicates|
|
53
|
+
[predicates].flatten.each do |predicate|
|
54
|
+
case predicate
|
55
|
+
when String, Symbol
|
56
|
+
semantics[attr].add(predicate)
|
57
|
+
when Hash
|
58
|
+
semantics[attr].add(predicate.keys.first, predicate.values.first)
|
59
|
+
else
|
60
|
+
raise '???'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
self.stubs(:semantic_attributes).returns(semantics)
|
66
|
+
end
|
67
|
+
end
|