to_factory 0.1.1 → 0.2.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.
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