semantic_attributes 1.0.2
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.
- 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
|