to_factory 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/.travis.yml +1 -0
  2. data/README.md +8 -11
  3. data/lib/to_factory/collation.rb +40 -14
  4. data/lib/to_factory/file_sync.rb +9 -7
  5. data/lib/to_factory/file_writer.rb +21 -11
  6. data/lib/to_factory/finders/factory.rb +12 -6
  7. data/lib/to_factory/finders/model.rb +25 -9
  8. data/lib/to_factory/generation/attribute.rb +35 -17
  9. data/lib/to_factory/generation/factory.rb +32 -24
  10. data/lib/to_factory/klass_inference.rb +50 -0
  11. data/lib/to_factory/options_parser.rb +34 -0
  12. data/lib/to_factory/parsing/file.rb +1 -0
  13. data/lib/to_factory/parsing/syntax.rb +6 -17
  14. data/lib/to_factory/representation.rb +27 -0
  15. data/lib/to_factory/version.rb +1 -1
  16. data/lib/to_factory.rb +4 -4
  17. data/spec/example_factories/new_syntax/user_admin_root.rb +21 -0
  18. data/spec/example_factories/new_syntax/user_admin_super_admin.rb +4 -4
  19. data/spec/example_factories/old_syntax/user_admin_root.rb +21 -0
  20. data/spec/example_factories/old_syntax/user_admin_super_admin.rb +4 -3
  21. data/spec/integration/file_sync_spec.rb +9 -3
  22. data/spec/integration/file_writer_spec.rb +5 -3
  23. data/spec/integration/to_factory_method_spec.rb +17 -11
  24. data/spec/unit/collation_spec.rb +25 -18
  25. data/spec/unit/file_writer_spec.rb +4 -2
  26. data/spec/unit/finders/factory_spec.rb +2 -3
  27. data/spec/unit/generator_spec.rb +13 -16
  28. data/spec/unit/parsing/file_spec.rb +10 -11
  29. data/spec/unit/parsing/klass_inference_spec.rb +15 -16
  30. data/to_factory.gemspec +0 -1
  31. metadata +21 -32
  32. data/lib/to_factory/definition_group.rb +0 -47
  33. data/lib/to_factory/parsing/klass_inference.rb +0 -34
  34. data/spec/unit/definition_group_spec.rb +0 -19
data/.travis.yml CHANGED
@@ -9,6 +9,7 @@ rvm:
9
9
  - 2.1.0
10
10
  - 2.0.0
11
11
  - 1.9.3
12
+ - 1.9.2
12
13
  - 1.8.7
13
14
 
14
15
  script: "./bin/ci"
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ToFactory
1
+ ToFactory :wrench:
2
2
  =========
3
3
 
4
4
  [![Build Status](https://travis-ci.org/markburns/to_factory.svg)](https://travis-ci.org/markburns/to_factory)
@@ -16,11 +16,10 @@ Easily add factories with valid data for an existing project.
16
16
  * unintrusively update factory files in place
17
17
  * Parses and writes both new `FactoryGirl`, syntax or older `Factory.define` syntax
18
18
 
19
- Tested against Ruby 1.8.7, 1.9.3, 2.0.0, 2.1.x, 2.2.0
19
+ Tested against Ruby 1.8.7, 1.9.2, 1.9.3, 2.0.0, 2.1.x, 2.2.0
20
20
 
21
21
 
22
- #Installation
23
- ___________
22
+ ## Installation :file_folder:
24
23
 
25
24
  ```ruby
26
25
 
@@ -31,20 +30,21 @@ group :test, :development do
31
30
  end
32
31
  ```
33
32
 
34
-
35
- #Warning
33
+ ## Warning :warning:
36
34
  `ToFactory` writes into the `spec/factories` folder. Whilst it
37
35
  is tested and avoids overwriting existing factories,
38
36
  it is recommended that you execute after committing or when in a known
39
37
  safe state.
40
38
 
41
- #Example
39
+
40
+
42
41
  ```bash
43
42
  git add spec/factories
44
43
  git commit -m "I know what I am doing"
45
44
  rails c
46
45
  >ToFactory()
47
46
  ```
47
+ ## Example :computer:
48
48
 
49
49
  ```ruby
50
50
  #Generate all factories
@@ -87,10 +87,7 @@ ToFactory :admin => User.last
87
87
 
88
88
  ```
89
89
 
90
- #Known bugs/limitations
91
- * Factory generation does not follow a hierarchical order, so factory files may require manual editing for now.
92
-
93
- #Other useful projects
90
+ ### Other useful projects
94
91
 
95
92
  If you are adding specs to an existing project you may want to look at:
96
93
 
@@ -2,32 +2,58 @@ module ToFactory
2
2
  AlreadyExists = Class.new ArgumentError
3
3
 
4
4
  class Collation
5
- def self.merge(a, b)
6
- c = new
5
+ def self.organize(a,b)
6
+ new(a, b).organize
7
+ end
7
8
 
8
- c.merge_without_collisions(a.with_indifferent_access, b.with_indifferent_access)
9
+ def self.representations_from(a,b)
10
+ new(a, b).representations
9
11
  end
10
12
 
11
- def merge_without_collisions(a,b)
12
- nested_detect_collisions!(a, b)
13
+ def initialize(a, b)
14
+ @a = a
15
+ @b = b
16
+ end
13
17
 
14
- a.deep_merge(b)
18
+ def organize
19
+ representations.group_by(&:klass).inject({}) do |o, (klass,r)|
20
+ o[klass] = r.sort_by(&:hierarchy_order)
21
+ o
22
+ end
15
23
  end
16
24
 
17
- def nested_detect_collisions!(a,b)
18
- a.each do |a_klass, _|
19
- b.each do |b_klass, _|
20
- detect_collisions!(a[a_klass] || {}, b[b_klass] || {})
25
+ def representations
26
+ detect_collisions!(@a,@b)
27
+
28
+ inference = KlassInference.new(merged)
29
+
30
+ merged.each do |r|
31
+ klass, order = inference.infer(r.name)
32
+ r.klass = klass
33
+ r.hierarchy_order = order
34
+ end
35
+
36
+ merged
37
+ end
38
+
39
+ def detect_collisions!(a,b)
40
+ collisions = []
41
+ a.each do |x|
42
+ b.each do |y|
43
+ collisions << x.name if x.name == y.name
21
44
  end
22
45
  end
46
+
47
+ raise_already_exists!(collisions) if collisions.any?
23
48
  end
24
49
 
25
- def detect_collisions!(a, b)
26
- overlapping = a.keys & b.keys
27
- raise_already_exists!(overlapping) if overlapping.any?
50
+ private
51
+
52
+ def merged
53
+ @merged ||= @a + @b
28
54
  end
29
55
 
30
- def raise_already_exists!(keys)
56
+ def raise_already_exists!(keys)
31
57
  raise ToFactory::AlreadyExists.new "an item for each of the following keys #{keys} already exists"
32
58
  end
33
59
  end
@@ -9,22 +9,24 @@ module ToFactory
9
9
  end
10
10
 
11
11
  def perform(exclusions=[])
12
- definitions = Collation.merge(new_definitions(exclusions), pre_existing)
13
-
14
- @file_writer.write(definitions)
12
+ @file_writer.write(all_representations exclusions)
15
13
  end
16
14
 
17
- def new_definitions(exclusions=[])
18
- return {} if exclusions == [:all]
15
+ def all_representations(exclusions=[])
16
+ Collation.organize(
17
+ new_representations(exclusions),
18
+ existing_representations)
19
+ end
19
20
 
21
+ def new_representations(exclusions=[])
20
22
  instances = @model_finder.call(exclusions)
21
23
 
22
- DefinitionGroup.perform(instances)
24
+ instances.map{|r| Representation.from(r) }
23
25
  end
24
26
 
25
27
  private
26
28
 
27
- def pre_existing
29
+ def existing_representations
28
30
  @factory_finder.call
29
31
  end
30
32
 
@@ -5,33 +5,43 @@ module ToFactory
5
5
  end
6
6
 
7
7
  def write(definitions)
8
- definitions.each do |klass, factories|
9
- name = klass.name.underscore.gsub /^"|"$/, ""
10
- mkdir(name) if name.to_s["/"]
11
-
12
- File.open(File.join(ToFactory.factories, "#{name}.rb"), "w") do |f|
13
- f << wrap_factories(factories.values)
8
+ definitions.each do |klass, representations|
9
+ write_to(name_from klass) do
10
+ wrap_factories(representations.map(&:definition))
14
11
  end
15
12
  end
16
13
  end
17
14
 
18
15
  private
19
16
 
20
- def wrap_factories(factories)
17
+ def name_from(klass)
18
+ klass.name.underscore.gsub /^"|"$/, ""
19
+ end
20
+
21
+ def write_to(name)
22
+ mkdir(name)
23
+
24
+ File.open(File.join(ToFactory.factories, "#{name}.rb"), "w") do |f|
25
+ f << yield
26
+ end
27
+ end
28
+
29
+ def wrap_factories(definitions)
21
30
  if ToFactory.new_syntax?
22
- modern_header(factories)
31
+ modern_header(definitions)
23
32
  else
24
- factories.join("\n\n")
33
+ definitions.join("\n\n")
25
34
  end
26
35
  end
27
36
 
28
- def modern_header(factories)
37
+ def modern_header(definitions)
29
38
  out = "FactoryGirl.define do\n"
30
- out << factories.join("\n")
39
+ out << definitions.join("\n")
31
40
  out << "\nend\n"
32
41
  end
33
42
 
34
43
  def mkdir(name)
44
+ return unless name.to_s["/"]
35
45
  dir = name.to_s.split("/")[0..-2]
36
46
  FileUtils.mkdir_p File.join(ToFactory.factories, dir)
37
47
  end
@@ -2,15 +2,21 @@ module ToFactory
2
2
  module Finders
3
3
  class Factory
4
4
  def call
5
- all_factories = {}
5
+ all = []
6
6
 
7
- Dir.glob(File.join(ToFactory.factories, "**/*.rb")).each do |f|
8
- factories = ToFactory::Parsing::File.parse(f)
9
-
10
- all_factories = Collation.merge(all_factories, factories)
7
+ parsed_files.each do |r|
8
+ all = Collation.representations_from(all, r)
11
9
  end
12
10
 
13
- all_factories
11
+ all
12
+ end
13
+
14
+ private
15
+
16
+ def parsed_files
17
+ Dir.glob(File.join(ToFactory.factories, "**/*.rb")).map do |f|
18
+ ToFactory::Parsing::File.parse(f)
19
+ end
14
20
  end
15
21
  end
16
22
  end
@@ -4,15 +4,9 @@ module ToFactory
4
4
  def call(exclusions=[])
5
5
  instances = []
6
6
 
7
- Dir.glob("#{ToFactory.models}/**/*.rb").each do |file|
8
- File.readlines(file).each do |f|
9
- if match = f.match(/class (.*) ?</)
10
- klass = rescuing_require file, match
11
- break if exclusions.include?(klass)
12
- instance = get_active_record_instance(klass)
13
- instances << instance if instance
14
- break
15
- end
7
+ each_klass(exclusions) do |klass|
8
+ if instance = get_active_record_instance(klass)
9
+ instances << instance
16
10
  end
17
11
  end
18
12
 
@@ -21,6 +15,28 @@ module ToFactory
21
15
 
22
16
  private
23
17
 
18
+ def each_klass(exclusions)
19
+ models.each do |file|
20
+ matching_lines(file) do |match|
21
+ klass = rescuing_require(file, match)
22
+
23
+ break if exclusions.include?(klass) || yield(klass)
24
+ end
25
+ end
26
+ end
27
+
28
+ def models
29
+ Dir.glob("#{ToFactory.models}/**/*.rb")
30
+ end
31
+
32
+ def matching_lines(file)
33
+ File.readlines(file).each do |l|
34
+ if match = l.match(/class (.*) ?</)
35
+ yield match
36
+ end
37
+ end
38
+ end
39
+
24
40
  def rescuing_require(file, match)
25
41
  require file
26
42
  klass = eval(match[1])
@@ -17,35 +17,53 @@ module ToFactory
17
17
  end
18
18
 
19
19
  def inspect_value(value, nested=false)
20
- formatted = case value
20
+ formatted = format(value, nested)
21
+
22
+ if !value.is_a?(Hash) && !nested
23
+ formatted = " #{formatted}"
24
+ end
25
+
26
+ formatted
27
+ end
28
+
29
+ private
30
+
31
+ def format(value, nested)
32
+ case value
21
33
  when Time, DateTime
22
- time = in_utc(value).strftime("%Y-%m-%dT%H:%M%Z").inspect
23
- time.gsub(/UTC"$/, "Z\"").gsub(/GMT"$/, "Z\"")
34
+ inspect_time(value)
24
35
  when BigDecimal
25
36
  value.to_f.inspect
26
37
  when Hash
27
- formatted = value.keys.inject([]) do |a, key|
28
- formatted_key = inspect_value(key, true)
29
- formatted_value = inspect_value(value.fetch(key), true)
30
- a << "#{formatted_key} => #{formatted_value}"
31
- end.join(', ')
32
-
33
- if nested
34
- "{#{formatted}}"
35
- else
36
- "({#{formatted}})"
37
- end
38
+ inspect_hash(value, nested)
38
39
  when Array
39
40
  value.map{|v| inspect_value(v)}
40
41
  else
41
42
  value.inspect
42
43
  end
44
+ end
43
45
 
44
- if !value.is_a?(Hash) && !nested
45
- formatted = " #{formatted}"
46
+ def inspect_time(value)
47
+ time = in_utc(value).strftime("%Y-%m-%dT%H:%M%Z").inspect
48
+ time.gsub(/UTC"$/, "Z\"").gsub(/GMT"$/, "Z\"")
49
+ end
50
+
51
+ def inspect_hash(value, nested)
52
+ formatted = value.keys.inject([]) do |a, key|
53
+ a << key_value_pair(key, value)
54
+ end.join(', ')
55
+
56
+ if nested
57
+ "{#{formatted}}"
58
+ else
59
+ "({#{formatted}})"
46
60
  end
61
+ end
47
62
 
48
- formatted
63
+ def key_value_pair(key, value)
64
+ formatted_key = inspect_value(key, true)
65
+ formatted_value = inspect_value(value.fetch(key), true)
66
+ "#{formatted_key} => #{formatted_value}"
49
67
  end
50
68
 
51
69
  def in_utc(time)
@@ -1,42 +1,36 @@
1
1
  module ToFactory
2
2
  module Generation
3
3
  class Factory
4
- def initialize(object, name)
5
- unless object.is_a? ActiveRecord::Base
6
- message = "Generation::Factory requires initializing with an ActiveRecord::Base instance"
7
- message << "\n but received #{object.inspect}"
8
- raise ToFactory::MissingActiveRecordInstanceException.new(message)
9
- end
10
-
11
- @name = add_quotes name
12
- @attributes = object.attributes
4
+ def initialize(representation)
5
+ @representation = representation
13
6
  end
14
7
 
15
- def to_factory(parent_name=nil)
16
- header(parent_name) do
17
- to_skip = [:id, :created_at, :updated_at]
18
- attributes = @attributes.delete_if{|key, _| to_skip.include? key.to_sym}
8
+ def name
9
+ add_quotes @representation.name
10
+ end
19
11
 
12
+ def to_factory
13
+ header do
20
14
  attributes.map do |attr, value|
21
15
  factory_attribute(attr, value)
22
16
  end.sort.join("\n") << "\n"
23
17
  end
24
18
  end
25
19
 
26
- def header(parent_name=nil, &block)
20
+ def header(&block)
27
21
  if ToFactory.new_syntax?
28
- modern_header parent_name, &block
22
+ modern_header &block
29
23
  else
30
- header_factory_girl_1 parent_name, &block
24
+ header_factory_girl_1 &block
31
25
  end
32
26
  end
33
27
 
34
- def modern_header(parent_name=nil, &block)
35
- generic_header(parent_name, "factory", "", &block)
28
+ def modern_header(&block)
29
+ generic_header("factory", "", &block)
36
30
  end
37
31
 
38
- def header_factory_girl_1(parent_name=nil, &block)
39
- generic_header(parent_name, "Factory.define", "|o|", &block)
32
+ def header_factory_girl_1(&block)
33
+ generic_header("Factory.define", " |o|", &block)
40
34
  end
41
35
 
42
36
  def factory_attribute(attr, value)
@@ -45,14 +39,28 @@ module ToFactory
45
39
 
46
40
  private
47
41
 
48
- def generic_header(parent_name, factory_start, block_arg, &block)
49
- out = " #{factory_start}(:#{@name}#{parent_clause(parent_name)}) do#{block_arg}\n"
42
+ def attributes
43
+ to_skip = [:id, :created_at, :updated_at]
44
+
45
+ @representation.attributes.delete_if{|key, _| to_skip.include? key.to_sym}
46
+ end
47
+
48
+ def parent_name
49
+ @representation.parent_name
50
+ end
51
+
52
+ def generic_header(factory_start, block_arg, &block)
53
+ out = " #{factory_start}(:#{name}#{parent_clause}) do#{block_arg}\n"
50
54
  out << yield.to_s
51
55
  out << " end\n"
52
56
  end
53
57
 
54
- def parent_clause(name)
55
- name ? ", :parent => :#{add_quotes name}" : ""
58
+ def parent_clause
59
+ has_parent? ? ", :parent => :#{add_quotes parent_name}" : ""
60
+ end
61
+
62
+ def has_parent?
63
+ parent_name.to_s.length > 0
56
64
  end
57
65
 
58
66
  def add_quotes(name)
@@ -0,0 +1,50 @@
1
+ module ToFactory
2
+ class CannotInferClass < ArgumentError
3
+ def initialize(message)
4
+ super message.inspect
5
+ end
6
+ end
7
+
8
+ class KlassInference
9
+ def initialize(representations)
10
+ @mapping = {}
11
+
12
+ representations.each do |r|
13
+ set_mapping_from(r)
14
+ end
15
+ end
16
+
17
+ def infer(factory_name, count=0)
18
+ count = count + 1
19
+ result = @mapping[factory_name]
20
+ return [result, count] if result.is_a? Class
21
+
22
+ if result.nil?
23
+ raise CannotInferClass.new(factory_name)
24
+ end
25
+
26
+ infer(result, count)
27
+ end
28
+
29
+ private
30
+
31
+ def set_mapping_from(r)
32
+ if parent_klass = to_const(r.parent_name)
33
+ @mapping[r.parent_name] = parent_klass
34
+ end
35
+
36
+ @mapping[r.name] =
37
+ if factory_klass = to_const(r.name)
38
+ factory_klass
39
+ else
40
+ r.parent_name
41
+ end
42
+ end
43
+
44
+ def to_const(factory_name)
45
+ factory_name.to_s.camelize.constantize
46
+ rescue NameError
47
+ nil
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ module ToFactory
2
+ class OptionsParser
3
+ def initialize(options)
4
+ @options = options
5
+ end
6
+
7
+ def get_instance
8
+ args = case @options
9
+ when ActiveRecord::Base
10
+ from_record(@options)
11
+ when Array
12
+ from_array(*@options)
13
+ end
14
+
15
+ Representation.new(*args)
16
+ end
17
+
18
+ def from_record(record)
19
+ name = calculate_name record.class
20
+
21
+ [name, nil, nil, record]
22
+ end
23
+
24
+ def from_array(name, record)
25
+ parent_name = calculate_name(record.class)
26
+ parent_name = nil if parent_name.to_s == name.to_s
27
+ [name, parent_name, nil, record]
28
+ end
29
+
30
+ def calculate_name(klass)
31
+ klass.name.to_s.underscore
32
+ end
33
+ end
34
+ end
@@ -8,6 +8,7 @@ module ToFactory
8
8
  module Parsing
9
9
  class File
10
10
  delegate :multiple_factories?, :header?, :parse, :to => :parser
11
+ attr_reader :contents
11
12
 
12
13
  def self.parse(filename)
13
14
  from_file(filename).parse
@@ -1,7 +1,7 @@
1
- require "to_factory/parsing/klass_inference"
2
-
3
1
  module ToFactory
4
2
  module Parsing
3
+ ParseException = Class.new ::Exception
4
+
5
5
  class Syntax
6
6
  attr_accessor :contents
7
7
 
@@ -14,17 +14,12 @@ module ToFactory
14
14
  end
15
15
 
16
16
  def parse
17
- result = {}
18
- @klass_inference = KlassInference.new
19
- @klass_inference.setup(all_factories)
20
-
21
- all_factories.each do |factory_name, parent_name, ruby|
22
- klass = @klass_inference.infer(factory_name)
23
- result[klass] ||= {}
24
- result[klass][factory_name] = ruby
17
+ factories.map do |x|
18
+ Representation.new(name_from(x), parent_from(x), to_ruby(x))
25
19
  end
26
20
 
27
- result
21
+ rescue Racc::ParseError, StringScanner::Error => e
22
+ raise ParseException.new("Original exception: #{e.message}\n #{e.backtrace}\nToFactory Error parsing \n#{@contents}\n o")
28
23
  end
29
24
 
30
25
  def factories
@@ -39,12 +34,6 @@ module ToFactory
39
34
  header? ? sexp[3] : sexp
40
35
  end
41
36
 
42
- def all_factories
43
- factories.map do |x|
44
- [name_from(x), parent_from(x), to_ruby(x)]
45
- end
46
- end
47
-
48
37
  def name_from(sexp)
49
38
  sexp[1][3][1]
50
39
  end
@@ -0,0 +1,27 @@
1
+ module ToFactory
2
+ class Representation
3
+ delegate :attributes, :to => :record
4
+ attr_accessor :klass, :name, :parent_name, :definition, :hierarchy_order, :record
5
+
6
+ def self.from(options)
7
+ OptionsParser.new(options).get_instance
8
+ end
9
+
10
+ def initialize(name, parent_name, definition=nil, record=nil)
11
+ @name, @parent_name, @definition, @record =
12
+ name.to_s, parent_name.to_s, definition, record
13
+ end
14
+
15
+ def inspect
16
+ "#<ToFactory::Representation:#{object_id} @name: #{@name.inspect}, @parent_name: #{@parent_name.inspect}, @klass: #{klass_name_inspect}>"
17
+ end
18
+
19
+ def klass_name_inspect
20
+ @klass.name.inspect rescue "nil"
21
+ end
22
+
23
+ def definition
24
+ @definition ||= ToFactory::Generation::Factory.new(self).to_factory
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  module ToFactory
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/to_factory.rb CHANGED
@@ -1,5 +1,3 @@
1
- require "deep_merge"
2
-
3
1
  require "to_factory/version"
4
2
  require "to_factory/config"
5
3
  require "to_factory/generation/factory"
@@ -8,9 +6,11 @@ require "to_factory/collation"
8
6
  require "to_factory/file_writer"
9
7
  require "to_factory/finders/model"
10
8
  require "to_factory/finders/factory"
11
- require "to_factory/definition_group"
9
+ require "to_factory/representation"
12
10
  require "to_factory/file_sync"
13
11
  require "to_factory/parsing/file"
12
+ require "to_factory/klass_inference"
13
+ require "to_factory/options_parser"
14
14
 
15
15
  module ToFactory
16
16
  class MissingActiveRecordInstanceException < Exception;end
@@ -29,7 +29,7 @@ module ToFactory
29
29
 
30
30
  def definitions
31
31
  results = Finders::Factory.new.call
32
- results.map{|_, r| r.keys}.flatten
32
+ results.map(&:name)
33
33
  end
34
34
  end
35
35
  end
@@ -0,0 +1,21 @@
1
+ FactoryGirl.define do
2
+ factory(:"to_factory/user") do
3
+ name("User")
4
+ end
5
+
6
+ factory(:root, :parent => :"to_factory/user") do
7
+ birthday "2014-07-08T15:30Z"
8
+ email "test@example.com"
9
+ name "Jeff"
10
+ some_attributes({:a => 1})
11
+ some_id 8
12
+ end
13
+
14
+ factory(:admin, :parent => :"to_factory/user") do
15
+ name("Admin")
16
+ end
17
+
18
+ factory(:super_admin, :parent => :admin) do
19
+ name("Super Admin")
20
+ end
21
+ end