koujou 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ === 0.0.1 2009-07-14
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
@@ -0,0 +1,29 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ Rakefile
6
+ koujou.gemspec
7
+ lib/koujou.rb
8
+ lib/koujou/builder.rb
9
+ lib/koujou/custom_validation.rb
10
+ lib/koujou/data_generator.rb
11
+ lib/koujou/sequence.rb
12
+ lib/koujou/validation_reflection.rb
13
+ script/console
14
+ script/destroy
15
+ script/generate
16
+ test/lib/active_record_test_connector.rb
17
+ test/lib/models/car.rb
18
+ test/lib/models/comment.rb
19
+ test/lib/models/message.rb
20
+ test/lib/models/photo.rb
21
+ test/lib/models/post.rb
22
+ test/lib/models/profile.rb
23
+ test/lib/models/user.rb
24
+ test/test_builder.rb
25
+ test/test_custom_validation.rb
26
+ test/test_data_generator.rb
27
+ test/test_helper.rb
28
+ test/test_kojo.rb
29
+ test/test_sequence.rb
@@ -0,0 +1,7 @@
1
+
2
+ For more information on koujou, see http://koujou.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
@@ -0,0 +1,43 @@
1
+ = koujou
2
+
3
+ * http://github.com/mleung/koujou
4
+
5
+ == DESCRIPTION:
6
+
7
+ Koujou is a fixture replacement that requires no effort to use. You don't have to define a single thing
8
+ in your test_helper or whatever. Just call the koujou method on your active record model, and you're
9
+ off.
10
+
11
+ Check out: http://www.michaelleung.us/koujou for all the juicy details.
12
+
13
+ Please note this is a very early release of koujou, so if you find any issues, just post
14
+ them here: http://github.com/mleung/koujou/issues
15
+
16
+ == INSTALL:
17
+
18
+ gem install mleung-koujou --source http://gems.github.com
19
+
20
+ == LICENSE:
21
+
22
+ (The MIT License)
23
+
24
+ Copyright (c) 2009 Michael Leung
25
+
26
+ Permission is hereby granted, free of charge, to any person obtaining
27
+ a copy of this software and associated documentation files (the
28
+ 'Software'), to deal in the Software without restriction, including
29
+ without limitation the rights to use, copy, modify, merge, publish,
30
+ distribute, sublicense, and/or sell copies of the Software, and to
31
+ permit persons to whom the Software is furnished to do so, subject to
32
+ the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be
35
+ included in all copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
38
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
39
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
40
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
41
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
42
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
43
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ require 'fileutils'
5
+ require './lib/koujou'
6
+
7
+ Hoe.plugin :newgem
8
+ # Hoe.plugin :website
9
+ # Hoe.plugin :cucumberfeatures
10
+
11
+ # Generate all the Rake tasks
12
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
13
+ $hoe = Hoe.spec 'koujou' do
14
+ self.developer 'Michael Leung', 'me@michaelleung.us'
15
+ self.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
16
+ self.rubyforge_name = self.name # TODO this is default value
17
+ # self.extra_deps = [['activesupport','>= 2.0.2']]
18
+
19
+ end
20
+
21
+ require 'newgem/tasks'
22
+ Dir['tasks/**/*.rake'].each { |t| load t }
23
+
24
+ # TODO - want other tests/tasks run by default? Add them to the list
25
+ # remove_task :default
26
+ # task :default => [:spec, :features]
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{koujou}
5
+ s.version = "0.1.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Michael Leung"]
9
+ s.date = %q{2009-07-19}
10
+ s.description = %q{Koujou is a fixture replacement that requires no effort to use. You don't have to define a single thing in your test_helper or whatever. Just call the koujou method on your active record model, and you're off. check out: http://www.michaelleung.us/koujou for all the juicy details.}
11
+ s.email = ["me@michaelleung.us"]
12
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "PostInstall.txt"]
13
+ s.files = ["History.txt", "Manifest.txt", "PostInstall.txt", "README.rdoc", "Rakefile", "lib/koujou.rb", "lib/koujou/builder.rb", "lib/koujou/data_generator.rb", "lib/koujou/sequence.rb", "lib/koujou/validation_reflection.rb", "script/console", "script/destroy", "script/generate", "test/lib/active_record_test_connector.rb", "test/lib/models/comment.rb", "test/lib/models/post.rb", "test/lib/models/profile.rb", "test/lib/models/user.rb", "test/test_builder.rb", "test/test_data_generator.rb", "test/test_helper.rb", "test/test_kojo.rb", "test/test_sequence.rb", "lib/koujou/custom_validation.rb"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://github.com/mleung/koujou}
16
+ s.post_install_message = %q{PostInstall.txt}
17
+ s.rdoc_options = ["--main", "README.rdoc"]
18
+ s.require_paths = ["lib"]
19
+ s.rubyforge_project = %q{koujou}
20
+ s.rubygems_version = %q{1.3.1}
21
+ s.summary = %q{Koujou is a fixture replacement that requires no effort to use}
22
+ s.test_files = ["test/test_builder.rb", "test/test_data_generator.rb", "test/test_helper.rb", "test/test_kojo.rb", "test/test_sequence.rb"]
23
+ s.add_dependency('faker', '>= 0.3.1')
24
+
25
+ if s.respond_to? :specification_version then
26
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
27
+ s.specification_version = 3
28
+
29
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
30
+ s.add_development_dependency(%q<hoe>, [">= 2.3.2"])
31
+ else
32
+ s.add_dependency(%q<hoe>, [">= 2.3.2"])
33
+ end
34
+ else
35
+ s.add_dependency(%q<hoe>, [">= 2.3.2"])
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'rubygems'
5
+ require 'active_record'
6
+ require 'faker'
7
+ require 'koujou/sequence'
8
+ require 'koujou/builder'
9
+ require 'koujou/data_generator'
10
+ require 'koujou/validation_reflection'
11
+ require 'koujou/custom_validation'
12
+
13
+ module Koujou
14
+ VERSION = '0.1.0'
15
+ end
16
+
17
+ if ENV["RAILS_ENV"] == "test"
18
+ ActiveRecord::Base.class_eval do
19
+ include Koujou::ActiveRecordExtensions::ValidationReflection
20
+ Koujou::ActiveRecordExtensions::ValidationReflection.install(self)
21
+ include Koujou::ActiveRecordExtensions::Builder
22
+ end
23
+ end
@@ -0,0 +1,214 @@
1
+ module Koujou #:nodoc:
2
+ module ActiveRecordExtensions # :nodoc:
3
+ module Builder # :nodoc:
4
+
5
+ def self.included(base) # :nodoc:
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods #:nodoc:
10
+
11
+ def koujou(create = true, attributes = nil)
12
+ generate_instance(create, attributes)
13
+ end
14
+
15
+ def koujou_build(attributes = nil)
16
+ koujou(false, attributes)
17
+ end
18
+
19
+ def koujou_create(attributes = nil)
20
+ koujou(true, attributes)
21
+ end
22
+
23
+ protected
24
+ def generate_instance(create, attributes)
25
+ # There were a metric ton of AR warnings about instance vars
26
+ # not being initialized when running our specs. This hides those.
27
+ silence_warnings do
28
+ instance = build_model_instance(self, attributes)
29
+ instance.save! if create
30
+ instance
31
+ end
32
+ end
33
+
34
+ def build_model_instance(klass, attributes = nil, recursed_from_model = nil)
35
+ # If we pass in a string here for klass instead of a constant
36
+ # we want to convert that.
37
+ klass = Kernel.const_get(klass) unless klass.respond_to?(:new)
38
+ instance = klass.new
39
+ # Set the models attributes if the user passed them in.
40
+ # This will allow attributes to be set regardless if
41
+ # they're required or not.
42
+ instance.attributes = attributes unless attributes.nil?
43
+
44
+ set_required_attributes!(instance, attributes)
45
+ set_unique_attributes!(instance, attributes)
46
+ set_confirmation_attributes!(instance, attributes)
47
+ set_length_validated_attributes!(instance, attributes)
48
+ set_inclusion_validated_attributes!(instance, attributes)
49
+ create_associations(instance, recursed_from_model)
50
+ CustomValidation.stub_custom_validations!(instance)
51
+
52
+ instance
53
+ end
54
+
55
+ def set_required_attributes!(instance, attributes)
56
+ instance.class.required_validations.each do |v|
57
+ # We want to skip over setting any required fields if the field
58
+ # should also be unique. We handle that in the set_unique_attributes!
59
+ # method with a sequence. Also, if it's a confirmation field (e.g. password_confirmation)
60
+ # we can skip it, because that gets set below.
61
+ standard_required_attributes(instance, v) do
62
+ # We don't want to set anything if the user passed in data for this field
63
+ # or if this is a validates_presence_of :some_id. The ids will be set
64
+ # when we create the association.
65
+ next if overridden_attribute?(attributes, v.name)
66
+
67
+ generate_and_set_data(instance, v, false)
68
+ end
69
+ end
70
+ end
71
+
72
+ def set_unique_attributes!(instance, attributes)
73
+ instance.class.unique_validations.each do |v|
74
+ next if overridden_attribute?(attributes, v.name)
75
+ generate_and_set_data(instance, v, true)
76
+ end
77
+ end
78
+
79
+ # This generates set_length_validated_attributes! and set_inclusion_validated_attributes! methods.
80
+ # They used to look like this:
81
+ #
82
+ # def set_length_validated_attributes!(instance, attributes)
83
+ # instance.class.length_validations.each do |v|
84
+ # non_required_attributes(instance, v, attributes) { generate_and_set_data(instance, v, false) }
85
+ # end
86
+ # end
87
+ #
88
+ # def set_inclusion_of_validated_attributes!(instance, attributes)
89
+ # instance.class.inclusion_validations.each do |v|
90
+ # non_required_attributes(instance, v, attributes) { generate_and_set_data(instance, v, false) }
91
+ # end
92
+ # end
93
+ # I'm sure you see the similarities.
94
+ %w(length inclusion).each do |validation|
95
+ define_method("set_#{validation}_validated_attributes!") do |instance, attributes|
96
+ instance.class.send("#{validation}_validations").each do |v|
97
+ # Non required attributes are anything that doesn't have validates_presence_of,
98
+ # validates_uniqueness_of and also anything that hasn't been overriden. These
99
+ # values get set there. So there's no real point in setting them again here.
100
+ non_required_attributes(instance, v, attributes) { generate_and_set_data(instance, v, false) }
101
+ end
102
+ end
103
+ end
104
+
105
+ def set_confirmation_attributes!(instance, attributes)
106
+ instance.class.confirmation_validations.each do |v|
107
+ # This goes in and sets the models confirmation to whatever the corresponding
108
+ # fields value is. (e.g. password_confirmation= password)
109
+ instance.send("#{v.name}_confirmation=", instance.send("#{v.name}"))
110
+ end
111
+ end
112
+
113
+ def create_associations(instance, recursed_from_model = nil)
114
+ # We loop through all the has_one or belongs_to associations on the current instance
115
+ # using introspection, and build up and assign some models to each, if the user has
116
+ # required the id (e.g. requires_presence_of :user_id). So we're only going to build
117
+ # the minimum requirements for each model.
118
+ instance.class.reflect_on_all_associations.each do |a|
119
+ # We only want to create the association if the user has required the id field.
120
+ # This will build the minimum valid requirements.
121
+ next unless has_required_id_validation?(instance, a)
122
+
123
+ if a.macro == :has_one || a.macro == :belongs_to
124
+ # If there's a two way association here (user has_one profile, profile belongs_to user)
125
+ # we only want to create one of those, or it'll recurse forever. That's what the
126
+ # recursed_from_model does.
127
+ unless recursed_from_model.to_s == instance.class.to_s.downcase
128
+ instance.send("#{a.name.to_s}=", build_model_instance(get_assocation_class_name(a), nil, a.name))
129
+ end
130
+ end
131
+
132
+ end
133
+ end
134
+
135
+ def get_assocation_class_name(assocation)
136
+ # This condition is used if the class_name option
137
+ # is passed to has_many or has_one. We'll definitely
138
+ # want to use that key instead of the name of the association.
139
+ if assocation.options.has_key?(:class_name)
140
+ klass = assocation.options[:class_name]
141
+ else
142
+ klass = assocation.name.to_s.singularize.classify
143
+ end
144
+ end
145
+
146
+ def generate_and_set_data(instance, validation, sequenced)
147
+ data_generator = DataGenerator.new(sequenced, validation)
148
+ data_generator.required_length = get_required_length(instance, validation)
149
+ if has_inclusion_validation?(instance, validation)
150
+ data_generator.inclusion_values = get_inclusion_values(instance, validation)
151
+ end
152
+ instance.send("#{validation.name}=", data_generator.generate_data_for_column_type)
153
+ end
154
+
155
+ def standard_required_attributes(instance, validation)
156
+ yield unless has_unique_validation?(instance, validation)
157
+ end
158
+
159
+ def non_required_attributes(instance, validation, attributes)
160
+ yield unless has_unique_validation?(instance, validation) || has_required_validation?(instance, validation) ||
161
+ overridden_attribute?(attributes, validation.name)
162
+ end
163
+
164
+ def overridden_attribute?(attributes, key)
165
+ attributes && attributes.has_key?(key.to_sym)
166
+ end
167
+
168
+ # This creates has_unique_validation?, has_length_validation? etc.
169
+ # We could probably make one method that takes a type, but I'd rather
170
+ # get separate methods for each. Cool?
171
+ %w(unique length required inclusion).each do |v|
172
+ define_method("has_#{v}_validation?") do |instance, validation|
173
+ !instance.class.send("#{v}_validations").select{|u| u.name == validation.name }.empty?
174
+ end
175
+ end
176
+
177
+ def get_required_length(instance, validation)
178
+ return unless has_length_validation?(instance, validation)
179
+
180
+ options = instance.class.length_validations.select{|v| v.name == validation.name }.first.options
181
+
182
+ retval = nil
183
+ # If the validation is validates_length_of :name, :within => 1..20 (or in, which is an alias),
184
+ # let's just return the minimum value of the range.
185
+ %w(within in).each do |o|
186
+ retval = options[o.to_sym].entries.first if options.has_key?(o.to_sym)
187
+ break
188
+ end
189
+ if retval.nil?
190
+ # These other validations should just return the value set.
191
+ %w(is minimum maximum).each do |o|
192
+ retval = options[o.to_sym] if options.has_key?(o.to_sym)
193
+ break
194
+ end
195
+ end
196
+ retval
197
+ end
198
+
199
+ def get_inclusion_values(instance, validation)
200
+ return unless has_inclusion_validation?(instance, validation)
201
+ options = instance.class.inclusion_validations.select{|v| v.name == validation.name }.first.options
202
+ options[:in]
203
+ end
204
+
205
+ def has_required_id_validation?(instance, association)
206
+ name = association.options.has_key?(:class_name) ? association.options[:class_name].downcase : association.name
207
+ !instance.class.required_validations.select{|v| v.name.to_s == "#{name}_id" }.empty?
208
+ end
209
+
210
+ end
211
+
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,15 @@
1
+ # TODO: We need to override any before_validation, or before_validation_on create callbacks as well, I think.
2
+ module Koujou #:nodoc:
3
+ class CustomValidation # :nodoc:
4
+
5
+ # This just goes in and redefines any custom validation methods. Since we can't
6
+ # really ascertain what the intent of those are when koujou runs, it seems
7
+ # like stubbing them out is the most logical choice.
8
+ def self.stub_custom_validations!(instance)
9
+ instance.class.custom_validations.each do |v|
10
+ instance.class.module_eval { define_method(v.name.to_s) { true } }
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,106 @@
1
+ module Koujou #:nodoc:
2
+ class DataGenerator
3
+
4
+ attr_accessor :required_length, :inclusion_values
5
+
6
+ def initialize(sequenced, validation)
7
+ @sequenced = sequenced
8
+ # Validation is actually a ActiveRecord::Reflection::MacroReflection
9
+ @validation = validation
10
+ @required_length = nil
11
+ @inclusion_values = nil
12
+ end
13
+
14
+ def generate_data_for_column_type
15
+ # So if there was an inclusion passed in for validations_inclusion_of (which is just
16
+ # an enumberable object), let's just return the first element to ensure the value
17
+ # set is the correct one. Mmmkay?
18
+ return get_first_value_for_inclusion unless @inclusion_values.nil?
19
+ # Sometimes models have a validates_presence_of set, but there's no corresponding
20
+ # db column. The only example I can think of for this is a user model where the actual
21
+ # column is hashed_password but the model requires the presence of password. So we'll
22
+ # assume string. This could bite me in the ass later, but we'll see.
23
+ if @validation.active_record.columns_hash.has_key?("#{@validation.name}")
24
+ db_type = @validation.active_record.columns_hash["#{@validation.name}"].type
25
+ else
26
+ db_type = 'string'
27
+ end
28
+ # Since the method names are all based on the db types, we'll just go ahead and
29
+ # dynamically call the appropriate one.
30
+ send("generate_#{db_type}")
31
+ end
32
+
33
+ def generate_string
34
+ retval = ''
35
+ # I don't really like all these elsif's but, apparently
36
+ # it's more efficient than the numerous explicit returns
37
+ # we used to have. SEE: http://gist.github.com/160718
38
+ if @validation.name.to_s.match(/email/)
39
+ retval = format_if_sequenced(Faker::Internet.email)
40
+ elsif @validation.name.to_s == 'first_name'
41
+ retval = format_if_sequenced(Faker::Name.first_name)
42
+ elsif @validation.name.to_s == 'last_name'
43
+ retval = format_if_sequenced(Faker::Name.last_name)
44
+ elsif @validation.name.to_s.match(/login|user_name/)
45
+ retval = format_if_sequenced(Faker::Internet.user_name)
46
+ elsif @validation.name.to_s.match(/city/)
47
+ retval = format_if_sequenced(Faker::Address.city)
48
+ elsif @validation.name.to_s.match(/state|province/)
49
+ retval = format_if_sequenced(Faker::Address.us_state)
50
+ elsif @validation.name.to_s.match(/zip|postal/)
51
+ retval = format_if_sequenced(Faker::Address.zip_code)
52
+ elsif @validation.name.to_s == 'password' || @validation.name.to_s == 'password_confirmation'
53
+ retval = 'koujourama'
54
+ else
55
+ # If we don't match any standard stuff, just return a regular bs lorem string comprised of 10 words.
56
+ # 10 is sort of a "magic number" I might make a constant for that.
57
+ standard_text = format_if_sequenced(Faker::Lorem.words(10).to_s)
58
+ # So if there's a length validation set, we need to return just that amount of data.
59
+ retval = @required_length ? standard_text[0..@required_length - 1].to_s : standard_text
60
+ end
61
+ retval
62
+ end
63
+
64
+ def generate_text
65
+ if @required_length
66
+ # So if there's a length validation set, we need to return just that amount of data.
67
+ Faker::Lorem.paragraph(2).to_s[0..@required_length - 1]
68
+ else
69
+ Faker::Lorem.paragraph.to_s
70
+ end
71
+ end
72
+
73
+ def generate_integer
74
+ generate_number
75
+ end
76
+
77
+ def generate_float
78
+ generate_number.to_f
79
+ end
80
+
81
+ def generate_datetime
82
+ DateTime.now
83
+ end
84
+
85
+ def generate_boolean
86
+ true
87
+ end
88
+
89
+ protected
90
+ def generate_number
91
+ # If this is supposed to be sequenced (aka unique), we'll get the next int from the
92
+ # Sequence class, and randomize that.
93
+ random = rand(999)
94
+ @sequenced ? random + (Sequence.instance.next * rand(2)) : random
95
+ end
96
+
97
+ def format_if_sequenced(val)
98
+ @sequenced ? "#{Sequence.instance.next}#{val}" : val
99
+ end
100
+
101
+ def get_first_value_for_inclusion
102
+ @inclusion_values.first
103
+ end
104
+
105
+ end
106
+ end