quick_shoulda 0.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 ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
data/.rdebugrc ADDED
@@ -0,0 +1,3 @@
1
+ set autolist
2
+ set autoeval
3
+ set autoreload
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --debug
3
+ -f documentation
4
+ -p
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3-p194@quick-shoulda --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in quick_shoulda.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Tien Nguyen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # QuickShoulda
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'quick_shoulda'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install quick_shoulda
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :install_mah_gem do
4
+ exec %{
5
+ gem uninstall quick_shoulda;
6
+ gem build quick_shoulda.gemspec;
7
+ gem install quick_shoulda-0.0.2.gem; }
8
+ end
@@ -0,0 +1,44 @@
1
+ [
2
+ #URLs
3
+ 'www.domain.com', 'www.domain.subdomain.com', 'http://www.domain.com', 'http://www.domain.subdomain.com',
4
+ 'http://www.domain.com/path/1', 'https://www.domain.com', 'https://www.domain.subdomain.com', 'domain.subdomain.com',
5
+ 'domain.com/path/1', 'subdomain.domain.com/path/1',
6
+ #Emails
7
+ 'us@gmail.com', 'us.vn@gmail.com', 'us_vn@gmail.com', 'us_vn@gmail.com.vn',
8
+ #IPS
9
+ '192.168.1.1', '192.168.1.2',
10
+ #Sex
11
+ 'male', 'female', 'gay', 'les', 'other',
12
+ #Zip postal code
13
+ '10001', '10002', '10003',
14
+ #Phone numbers
15
+ '732-757-2923', '1732-757-2923',
16
+ #datestimes
17
+ '1999-01-01', '99-01-01', '1999/01/01', '99/01/01', '01/01/99', '03/10/1986', '01-01-1999', '01-01-99', 'January 07, 2013',
18
+ 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday',
19
+ 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december',
20
+ 'am', 'pm',
21
+ #countries
22
+ 'Vietnam', 'America', 'England',
23
+ 'Vietnamese', 'American', 'English',
24
+ #cities
25
+ 'New York', 'London', 'Ho Chi Minh',
26
+ #Status
27
+ 'married', 'single', 'available', 'complicated',
28
+ #Files
29
+ 'filename.txt', 'filename.jpg', 'filename.png', 'filename.jpeg',
30
+ #Extensions
31
+ 'txt', 'jpg', 'png', 'jpeg', 'mp4', 'mp3', 'wmv',
32
+ #Videos
33
+ 'filename.wmv', 'filename.mp4', 'filename.flv',
34
+ #credit cards number
35
+ '4111 1111 1111 1111', '5500 0000 0000 0004', '3400 0000 0000 009', '3000 0000 0000 04', '+84 121731845', '84 121731845', '(84) 121731845',
36
+ #some countries code
37
+ '84', '1', '39',
38
+ #ISO countries code
39
+ 'US', 'USA', 'VN', 'VNM',
40
+ #services'
41
+ 'yahoo', 'google', 'facebook', 'amazone', 'youtube', 'paypal', 'alchemy',
42
+ #marks
43
+ 'excellent', 'good', 'bad'
44
+ ]
@@ -0,0 +1,11 @@
1
+ module QuickShoulda
2
+ module Generators
3
+ class GenerateGenerator < ::Rails::Generators::Base
4
+ argument :model_path, :type => :string
5
+
6
+ def generate_shoulda_testcases
7
+ QuickShoulda.process(model_path)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ module QuickShoulda
2
+ module Errors
3
+ class InvalidPathError < StandardError
4
+ def initialize(msg = 'Invalid path given')
5
+ super(msg)
6
+ end
7
+ end
8
+
9
+ class FileDoesNotExistError < StandardError
10
+ def initialize(msg = 'This file does not exist')
11
+ super(msg)
12
+ end
13
+ end
14
+
15
+ class NotAModelPathError < StandardError
16
+ def initialize(msg = 'file must located inside models directory')
17
+ super(msg)
18
+ end
19
+ end
20
+
21
+ class NotRubyFileError < StandardError
22
+ def initialize(msg = 'Given file must have .rb extension')
23
+ super(msg)
24
+ end
25
+ end
26
+
27
+ class PathNotGivenError < StandardError
28
+ def initialize(msg = 'path to model file must be given')
29
+ super(msg)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ require 'quick_shoulda/errors'
2
+
3
+ module QuickShoulda
4
+ module FileWriter
5
+ DescribeRegex = /^describe\s.+\sdo$/i
6
+
7
+ def create_file_and_write_init_content
8
+ FileUtils.mkdir_p(File.dirname(spec_file_path))
9
+ File.open(spec_file_path, 'w') do |file|
10
+ file.write(spec_init_content)
11
+ end
12
+ end
13
+
14
+ def clear_all_blocks
15
+ [:validation, :association].each do |block_name|
16
+ clear_block block_describe_header(block_name)
17
+ end
18
+ end
19
+
20
+ def write_block(shoulda_content)
21
+ file_content = ''
22
+ File.open(spec_file_path, 'a+') do |file|
23
+ inserted = false
24
+ while( line = file.gets )
25
+ file_content << line
26
+ if !inserted && line.strip.gsub(/[\t\n]/,'') =~ DescribeRegex
27
+ file_content << shoulda_content
28
+ inserted = true
29
+ end
30
+ end
31
+ end
32
+
33
+ File.open(spec_file_path, 'w') { |file | file.write( file_content ) }
34
+ end
35
+
36
+ def clear_block(block)
37
+ file_content = ""
38
+ File.open(spec_file_path, 'r') do | file |
39
+
40
+ delete_mode = false
41
+
42
+ while( line = file.gets )
43
+ filtered_line = line.gsub(/[\n\t]/,'')
44
+
45
+ if filtered_line == block || delete_mode
46
+ delete_mode = true if filtered_line == block
47
+ delete_mode = false if filtered_line == 'end'
48
+ next
49
+ end
50
+
51
+ file_content << line
52
+ end
53
+ end
54
+
55
+ File.open(spec_file_path, 'w') { |file | file.write( file_content ) }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,46 @@
1
+ module QuickShoulda
2
+ module Generator
3
+ module Association
4
+ MappedMacros = {
5
+ :has_one => 'have_one',
6
+ :has_many => 'have_many',
7
+ :belongs_to => 'belong_to',
8
+ :has_and_belongs_to_many => 'have_and_belong_to_many'
9
+ }
10
+
11
+ OptionMethods = {
12
+ :dependent => 'dependent',
13
+ :through => 'through',
14
+ :class_name => 'class_name',
15
+ :touch => 'touch',
16
+ :validate => 'validate',
17
+ :order => 'order',
18
+ :foreign_key => 'with_foreign_key'
19
+ }
20
+
21
+ SingleQuoteMethods = ['order', 'class_name']
22
+
23
+ def generate_associations(model)
24
+ model.reflect_on_all_associations.map { |association| generate_for_association(association) }.compact.flatten
25
+ end
26
+
27
+ private
28
+
29
+ def generate_for_association(association)
30
+ shoulda_method_name = MappedMacros[association.macro.to_sym]
31
+ "it { should #{shoulda_method_name}(:#{association.name})#{shoulda_assciation_option_methods_chain(association.options)} }"
32
+ end
33
+
34
+ def shoulda_assciation_option_methods_chain(options)
35
+ options.map do |option, value|
36
+ shoulda_option_method_with_value(option, value)
37
+ end.compact.join
38
+ end
39
+
40
+ def shoulda_option_method_with_value(option, value)
41
+ method_name = OptionMethods[option.to_sym]
42
+ ".#{method_name}#{value_transform(value)}" if method_name
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ module QuickShoulda
2
+ module Generator
3
+ module SpecContent
4
+ def block_describe_header(block_name)
5
+ case block_name.to_sym
6
+ when :association
7
+ "describe '#Associations' do"
8
+ when :validation
9
+ "describe '#Validations' do"
10
+ end
11
+ end
12
+
13
+ def shoulda_content(block_name, shoulda_lines)
14
+ return "" unless shoulda_lines.size > 0
15
+
16
+ block = block_describe_header(block_name)
17
+ shoulda_lines.map! { |line| "\t#{line}"}
18
+ shoulda_lines.insert(0, "#{block}")
19
+ shoulda_lines << "end\n"
20
+
21
+ shoulda_lines.map { |line| "\t#{line}"}.join("\n")
22
+ end
23
+
24
+ def spec_init_content
25
+ content = "require 'spec_helper'\n\n"
26
+ content << "describe '#{model_full_namespace}' do\n"
27
+ content << "end"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,144 @@
1
+ require 'quick_shoulda/random_string'
2
+
3
+ module QuickShoulda
4
+ module Generator
5
+ module Validation
6
+ AllowsValidators = [
7
+ 'presence', 'uniqueness', 'acceptance', 'confirmation', 'exclusion', 'format', 'inclusion', 'length', 'numericality'
8
+ ]
9
+
10
+ InclusionOptions = {
11
+ :range => 'in_range',
12
+ :array => 'in_array'
13
+ }
14
+
15
+ OptionMethods = {
16
+ :minimum => 'is_at_least',
17
+ :maximum => 'is_at_most',
18
+ :is => 'is_equal_to',
19
+ :scope => 'scoped_to',
20
+ :too_short => 'with_short_message',
21
+ :too_long => 'with_long_message',
22
+ :message => 'with_message',
23
+ :only_integer => 'only_integer',
24
+ :case_insensitive => 'case_insensitive',
25
+ :allow_nil => 'allow_nil',
26
+ :allow_blank => 'allow_blank',
27
+ :in => InclusionOptions,
28
+ :within => InclusionOptions
29
+ }
30
+
31
+ Filters = {
32
+ :allow_blank => ['inclusion'],
33
+ :allow_nil => ['inclusion', 'uniqueness'],
34
+ :case_insensitive => ['uniqueness']
35
+ }
36
+
37
+ AttrsFilters = ['friendly_id']
38
+
39
+ def self.included(base)
40
+ base.class_eval do
41
+ attr_accessor :validation_type
42
+ end
43
+ end
44
+
45
+ def generate_validations(model)
46
+ model.validators.map { |validator| generate_for_validator(validator) }.compact.flatten
47
+ end
48
+
49
+ private
50
+
51
+ def generate_for_validator(validator)
52
+ if @validation_type = _validation_type(validator)
53
+ return if ( attrs = attrs_filter(validator.attributes) ).empty?
54
+ generate_shouldas(attrs, validator.options)
55
+ end
56
+ end
57
+
58
+ def attrs_filter(attrs)
59
+ attrs.select { |attr| !(AttrsFilters.include? attr.to_s) }
60
+ end
61
+
62
+ def _validation_type(validator)
63
+ validation_type = validator.class.to_s.scan(/([^:]+)Validator/)
64
+ unless validation_type.empty?
65
+ validator = validation_type.first.first.downcase
66
+ validator if AllowsValidators.include? validator
67
+ end
68
+ end
69
+
70
+ def generate_shouldas(attributes, options)
71
+ attributes.map { |attribute| generate_shoulda(attribute, options) }.flatten
72
+ end
73
+
74
+ def generate_shoulda(attribute, options)
75
+ unless validation_type == 'format'
76
+ [ "it { should #{shoulda_matcher_method}(:#{attribute})#{shoulda_option_methods_chain(options)} }" ]
77
+ else
78
+ generate_shouldas_for_format_validation(attribute, options)
79
+ end
80
+ end
81
+
82
+ def generate_shouldas_for_format_validation(attribute, options)
83
+ RandomString.generate(options[:with]).map { |type, strings| generate_allow_shouldas(type, strings, attribute) }.flatten
84
+ end
85
+
86
+ def generate_allow_shouldas(type, strings, attribute)
87
+ should_suffix = type.to_s == 'matched_strings' ? '' : '_not'
88
+ strings.map { |string| "it { should#{should_suffix} allow_value('#{string}').for(:#{attribute}) }" }
89
+ end
90
+
91
+ def shoulda_matcher_method
92
+ prefix = ('exclusion|inclusion|length' =~ /#{validation_type}/) ? 'ensure' : 'validate'
93
+ "#{prefix}_#{validation_type}_of"
94
+ end
95
+
96
+ #.is_at_least(50).is_at_max(100)
97
+ def shoulda_option_methods_chain(options)
98
+ options.map { |option, value| shoulda_option_method(option, value) }.compact.join
99
+ end
100
+
101
+ def shoulda_option_method(option, value)
102
+ method = shoulda_option_method_name(option, value)
103
+ return unless method
104
+
105
+ case method
106
+ when OptionMethods[:scope]
107
+ shoulda_scope_to_method(value)
108
+ when OptionMethods[:only_integer], OptionMethods[:case_insensitive]
109
+ shoulda_method_without_value(method)
110
+ else
111
+ shoulda_normal_option_method(method, value)
112
+ end
113
+ end
114
+
115
+ #.is_at_least
116
+ #.is_at_most
117
+ def shoulda_option_method_name(option, value)
118
+ return unless pass_filter?(option)
119
+ option_method = OptionMethods[option.to_sym]
120
+ option_method = option_method[value.class.to_s.downcase.to_sym] if option_method.is_a?(Hash)
121
+ option_method
122
+ end
123
+
124
+ def pass_filter?(option)
125
+ !Filters[option.to_sym] || Filters[option.to_sym].include?(validation_type)
126
+ end
127
+
128
+ #.is_at_least(50)
129
+ def shoulda_normal_option_method(method, value)
130
+ ".#{method}#{value_transform(value)}"
131
+ end
132
+
133
+ def shoulda_method_without_value(method)
134
+ ".#{method}"
135
+ end
136
+
137
+ #.scoped_to(:username).scoped_to(:account)
138
+ def shoulda_scope_to_method(value)
139
+ method = OptionMethods[:scope]
140
+ ([] << value).flatten.map { | v | ".#{method}#{value_transform(v)}" }.join
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,11 @@
1
+ require 'quick_shoulda/generator/validation'
2
+ require 'quick_shoulda/generator/association'
3
+ require 'quick_shoulda/generator/spec_content'
4
+
5
+ module QuickShoulda
6
+ module Generator
7
+ include Validation
8
+ include Association
9
+ include SpecContent
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ module QuickShoulda
2
+ module StringHelpers
3
+ def camelize(str)
4
+ return str if str !~ /_/ && str =~ /[A-Z]+.*/
5
+ str.split("_").map{|e| e.capitalize}.join
6
+ end
7
+
8
+ def value_transform(value)
9
+ if value.is_a? String
10
+ "('#{value}')"
11
+ elsif value.is_a? Symbol
12
+ "(:#{value})"
13
+ else
14
+ "(#{value})"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ module QuickShoulda
2
+ module Main
3
+ def self.included(base)
4
+ base.class_eval do
5
+ attr_accessor :path, :spec_folder, :model_full_namespace, :spec_file_path, :validations_block, :associations_block
6
+ end
7
+ end
8
+
9
+ def configure_and_generate(path, spec_folder)
10
+ configure(path, spec_folder)
11
+ generate
12
+ end
13
+
14
+ private
15
+
16
+ def configure(path, spec_folder)
17
+ @path = path
18
+ @spec_folder = spec_folder[-1] != '/' ? "#{spec_folder}/" : spec_folder
19
+ @model_full_namespace = _model_full_namespace
20
+ @spec_file_path = _spec_file_path
21
+ end
22
+
23
+ def generate
24
+ generate_shoulda_content
25
+ create_and_write_to_file
26
+ end
27
+
28
+ def generate_shoulda_content
29
+ @validations_block = shoulda_content(:validation, generate_validations(model_full_namespace))
30
+ @associations_block = shoulda_content(:association, generate_associations(model_full_namespace))
31
+ end
32
+
33
+ def create_and_write_to_file
34
+ create_file_and_write_init_content unless spec_file_exist?
35
+ clear_all_blocks
36
+ write_block(validations_block + associations_block)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ require 'quick_shoulda/helpers/string_helpers'
2
+
3
+ module QuickShoulda
4
+ module PathResolver
5
+ include StringHelpers
6
+
7
+ private
8
+
9
+ def spec_file_exist?
10
+ File.file? spec_file_path
11
+ end
12
+
13
+ def _model_full_namespace
14
+ eval "::#{_model_full_namespace_in_str}"
15
+ end
16
+
17
+ def _spec_file_path
18
+ model_path = path.split(/\/?models\//)[1]
19
+ file_name = "#{File.basename(model_path, ".rb")}_spec.rb"
20
+ dir_name = File.dirname(model_path) == '.' ? spec_folder : "#{spec_folder}#{File.dirname(model_path)}/"
21
+ "#{dir_name}#{file_name}"
22
+ end
23
+
24
+ # given models/namespace/modelname.rb
25
+ # return Namespace::ModelName
26
+ def _model_full_namespace_in_str
27
+ model_path = path.split(/\/?models\//)[1].gsub('.rb','')
28
+ model_path.split('/').map { |token| camelize(token) }.join('::')
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,51 @@
1
+ module QuickShoulda
2
+ class RandomString
3
+ class << self
4
+ Size = 3
5
+ LowerCaseChars = ('a'..'z').to_a
6
+ UpperCaseChars = ('A'..'Z').to_a
7
+ Digits = (0..9).to_a
8
+
9
+ def sample_strings
10
+ stored_strings + random_digits_strings + random_chars_strings
11
+ end
12
+
13
+ def stored_strings
14
+ @stored_strings ||= YAML.load File.new(File.join(File.dirname(__FILE__),'..','..','data','stored_strings.yml'))
15
+ end
16
+
17
+ def random_digits_strings
18
+ random_strings(Digits)
19
+ end
20
+
21
+ def random_chars_strings
22
+ random_strings(LowerCaseChars + UpperCaseChars)
23
+ end
24
+
25
+ def random_strings(base)
26
+ (1..50).map { |length| length.times.inject('') { |initial, n| initial + base[rand(10)].to_s } }
27
+ end
28
+
29
+ def generate(pattern)
30
+ matched_strings = []
31
+ unmatched_strings = []
32
+ RandomString.sample_strings.each do |sample_string|
33
+ break if matched_strings.size == Size && unmatched_strings.size == Size
34
+
35
+ if sample_string =~ pattern
36
+ matched_strings << sample_string if matched_strings.size < Size
37
+ else
38
+ unmatched_strings << sample_string if unmatched_strings.size < Size
39
+ end
40
+ end
41
+
42
+ {
43
+ matched_strings: matched_strings,
44
+ unmatched_strings: unmatched_strings
45
+ }
46
+ end
47
+
48
+ alias_method :gen, :generate
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module QuickShoulda
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,26 @@
1
+ require "quick_shoulda/version"
2
+ require "quick_shoulda/errors"
3
+ require "quick_shoulda/path_resolver"
4
+ require "quick_shoulda/file_writer"
5
+ require "quick_shoulda/generator"
6
+ require "quick_shoulda/main"
7
+ require 'fileutils'
8
+
9
+ module QuickShoulda
10
+ class << self
11
+ include StringHelpers
12
+ include PathResolver
13
+ include FileWriter
14
+ include Generator
15
+ include Main
16
+ include Errors
17
+
18
+ def process(path, spec_folder = 'spec/models/')
19
+ raise PathNotGivenError unless path
20
+ raise NotAModelPathError unless path =~ /models\//
21
+ raise FileDoesNotExistError unless File.file?(path)
22
+ raise NotRubyFileError unless File.extname(path) == '.rb'
23
+ configure_and_generate(path, spec_folder)
24
+ end
25
+ end
26
+ end