seedie 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +91 -9
  3. data/CHANGELOG.md +123 -0
  4. data/Gemfile +4 -1
  5. data/README.md +77 -3
  6. data/Rakefile +1 -1
  7. data/lib/generators/seedie/install_generator.rb +84 -37
  8. data/lib/generators/seedie/templates/blank_seedie.yml +22 -0
  9. data/lib/generators/seedie/templates/seedie_initializer.rb +8 -0
  10. data/lib/seedie/associations/base_association.rb +35 -31
  11. data/lib/seedie/associations/belongs_to.rb +12 -13
  12. data/lib/seedie/associations/has_and_belongs_to_many.rb +26 -0
  13. data/lib/seedie/associations/has_many.rb +6 -4
  14. data/lib/seedie/associations/has_one.rb +13 -13
  15. data/lib/seedie/configuration.rb +12 -0
  16. data/lib/seedie/field_values/custom_value.rb +14 -83
  17. data/lib/seedie/field_values/fake_value.rb +85 -17
  18. data/lib/seedie/field_values/faker_builder.rb +29 -35
  19. data/lib/seedie/field_values/value_template_validator.rb +91 -0
  20. data/lib/seedie/field_values_set.rb +21 -4
  21. data/lib/seedie/model/creator.rb +7 -5
  22. data/lib/seedie/model/id_generator.rb +10 -8
  23. data/lib/seedie/model/model_sorter.rb +14 -19
  24. data/lib/seedie/model_fields.rb +5 -3
  25. data/lib/seedie/model_seeder.rb +11 -22
  26. data/lib/seedie/polymorphic_association_helper.rb +20 -16
  27. data/lib/seedie/railtie.rb +3 -2
  28. data/lib/seedie/reporters/base_reporter.rb +75 -68
  29. data/lib/seedie/reporters/console_reporter.rb +16 -12
  30. data/lib/seedie/reporters/reportable.rb +14 -10
  31. data/lib/seedie/seeder.rb +5 -3
  32. data/lib/seedie/version.rb +1 -1
  33. data/lib/seedie.rb +21 -27
  34. data/lib/tasks/seedie.rake +4 -2
  35. metadata +32 -13
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seedie
4
+ module FieldValues
5
+ class ValueTemplateValidator
6
+ VALID_KEYS = %w[values value options].freeze
7
+ PICK_STRATEGIES = %w[random sequential].freeze
8
+
9
+ def initialize(value_template, index, name)
10
+ @value_template = value_template
11
+ @index = index
12
+ @name = name
13
+ end
14
+
15
+ def validate
16
+ return unless @value_template.is_a?(Hash)
17
+
18
+ validate_keys
19
+ validate_values if @value_template.key?("values")
20
+ validate_options if @value_template.key?("options")
21
+ end
22
+
23
+ private
24
+
25
+ def validate_keys
26
+ invalid_keys = @value_template.keys - VALID_KEYS
27
+
28
+ if invalid_keys.present?
29
+ raise InvalidCustomFieldKeysError,
30
+ "Invalid keys for #{@name}: #{invalid_keys.join(', ')}. Only #{VALID_KEYS} are allowed."
31
+ end
32
+
33
+ return unless @value_template.key?("values")
34
+
35
+ if @value_template.key?("value")
36
+ raise InvalidCustomFieldKeysError,
37
+ "Invalid keys for #{@name}: values and value cannot be used together."
38
+ end
39
+
40
+ return unless @value_template["values"].is_a?(Hash)
41
+
42
+ return unless !@value_template["values"].key?("start") || !@value_template["values"].key?("end")
43
+
44
+ raise InvalidCustomFieldValuesError,
45
+ "The values key for #{@name} must be an array or a hash with start and end keys."
46
+ end
47
+
48
+ def validate_values
49
+ values = @value_template["values"]
50
+
51
+ unless values.is_a?(Array) || values.is_a?(Hash)
52
+ raise InvalidCustomFieldValuesError,
53
+ "The values key for #{@name} must be an array or a hash with start and end keys."
54
+ end
55
+
56
+ validate_sequential_values_length
57
+ end
58
+
59
+ def validate_options
60
+ options = @value_template["options"]
61
+ pick_strategy = options["pick_strategy"]
62
+
63
+ return unless pick_strategy.present? && !PICK_STRATEGIES.include?(pick_strategy)
64
+
65
+ raise InvalidCustomFieldOptionsError,
66
+ "The pick_strategy for #{@name} must be either 'sequential' or 'random'."
67
+ end
68
+
69
+ ## If pick strategy is sequential, we need to ensure there is a value for each index
70
+ # If there isn't sufficient values, we raise an error
71
+ def validate_sequential_values_length
72
+ return unless @value_template.key?("options")
73
+ return unless @value_template["options"]["pick_strategy"] == "sequential"
74
+
75
+ values = @value_template["values"]
76
+
77
+ values_length = if values.is_a?(Hash) && values.keys.sort == %w[end start]
78
+ # Assuming the values are an inclusive range
79
+ values["end"] - values["start"] + 1
80
+ else
81
+ values.length
82
+ end
83
+
84
+ return unless values_length < @index + 1
85
+
86
+ raise CustomFieldNotEnoughValuesError,
87
+ "There are not enough values for #{@name}. Please add more values."
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  class FieldValuesSet
3
- attr_reader :attributes_config, :index
5
+ attr_reader :model, :model_config, :attributes_config, :index
4
6
 
5
7
  def initialize(model, model_config, index)
6
8
  @model = model
@@ -18,6 +20,12 @@ module Seedie
18
20
  @field_values
19
21
  end
20
22
 
23
+ def generate_field_values_with_associations
24
+ associated_field_values_set = generate_belongs_to_associations
25
+ model_field_values_set = generate_field_values
26
+ model_field_values_set.merge!(associated_field_values_set)
27
+ end
28
+
21
29
  def generate_field_value(name, column)
22
30
  return generate_custom_field_value(name) if @attributes_config&.key?(name)
23
31
 
@@ -26,18 +34,27 @@ module Seedie
26
34
 
27
35
  private
28
36
 
37
+ def generate_belongs_to_associations
38
+ associations_config = model_config["associations"]
39
+ return {} unless associations_config.present?
40
+
41
+ belongs_to_associations = Associations::BelongsTo.new(model, associations_config)
42
+ belongs_to_associations.generate_associations
43
+ belongs_to_associations.associated_field_set
44
+ end
45
+
29
46
  def populate_values_for_model_fields
30
47
  @field_values = @model.columns_hash.map do |name, column|
31
48
  next if @model_fields.disabled_fields.include?(name)
32
49
  next if @model_fields.foreign_fields.include?(name)
33
-
50
+
34
51
  [name, generate_field_value(name, column)]
35
52
  end.compact.to_h
36
53
  end
37
54
 
38
55
  def populate_values_for_virtual_fields
39
56
  virtual_fields = @attributes_config.keys - @model.columns_hash.keys
40
-
57
+
41
58
  virtual_fields.each do |name|
42
59
  @field_values[name] = generate_custom_field_value(name) if @attributes_config[name]
43
60
  end
@@ -47,4 +64,4 @@ module Seedie
47
64
  FieldValues::CustomValue.new(name, @attributes_config[name], @index).generate_custom_field_value
48
65
  end
49
66
  end
50
- end
67
+ end
@@ -1,18 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  module Model
3
5
  class Creator
4
6
  include Reporters::Reportable
5
-
7
+
6
8
  def initialize(model, reporters = [])
7
9
  @model = model
8
10
  @reporters = reporters
9
11
 
10
12
  add_observers(@reporters)
11
13
  end
12
-
14
+
13
15
  def create!(field_values_set)
14
16
  record = @model.create!(field_values_set)
15
- report(:record_created, name: "#{record.class}", id: "#{record.id}")
17
+ report(:record_created, name: record.class.to_s, id: record.id.to_s)
16
18
 
17
19
  record
18
20
  end
@@ -22,9 +24,9 @@ module Seedie
22
24
  create!(field_values_set)
23
25
  rescue ActiveRecord::RecordInvalid => e
24
26
  report(:record_invalid, record: e.record)
25
- return nil
27
+ nil
26
28
  end
27
29
  end
28
30
  end
29
31
  end
30
- end
32
+ end
@@ -1,30 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  module Model
3
5
  class IdGenerator
4
6
  def initialize(model)
5
7
  @model = model
6
8
  end
7
-
9
+
8
10
  def random_id
9
11
  id = @model.pluck(:id).sample
10
12
  raise InvalidAssociationConfigError, "#{@model} has no records" unless id
11
13
 
12
- return id
14
+ id
13
15
  end
14
-
16
+
15
17
  def unique_id_for(association_klass, model_id_column)
16
18
  unless association_klass.column_names.include?(model_id_column)
17
- raise InvalidAssociationConfigError, "#{model_id_column} does not exist in #{association_klass}"
19
+ raise InvalidAssociationConfigError, "#{model_id_column} does not exist in #{association_klass}"
18
20
  end
19
-
21
+
20
22
  unique_ids = @model.ids - association_klass.pluck(model_id_column)
21
-
23
+
22
24
  if unique_ids.empty?
23
25
  raise InvalidAssociationConfigError, "No unique ids for #{@model}"
24
26
  end
25
-
27
+
26
28
  unique_ids.first
27
29
  end
28
30
  end
29
31
  end
30
- end
32
+ end
@@ -1,15 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  module Model
3
5
  class ModelSorter
4
6
  include PolymorphicAssociationHelper
5
-
7
+
6
8
  def initialize(models)
7
9
  @models = models
8
- @model_dependencies = models.map {|m| [m, get_model_dependencies(m)]}.to_h
10
+ @model_dependencies = models.map { |m| [m, get_model_dependencies(m)] }.to_h
9
11
  @resolved_queue = []
10
12
  @unresolved = []
11
13
  end
12
-
14
+
13
15
  def sort_by_dependency
14
16
  add_independent_models_to_queue
15
17
 
@@ -19,9 +21,8 @@ module Seedie
19
21
 
20
22
  @resolved_queue
21
23
  end
22
-
23
- private
24
24
 
25
+ private
25
26
 
26
27
  # Independent models need to be added first
27
28
  def add_independent_models_to_queue
@@ -31,7 +32,7 @@ module Seedie
31
32
  end
32
33
  end
33
34
  end
34
-
35
+
35
36
  def resolve_dependencies(model)
36
37
  if @unresolved.include?(model)
37
38
  puts "Circular dependency detected for #{model}. Ignoring..."
@@ -39,25 +40,21 @@ module Seedie
39
40
  end
40
41
 
41
42
  @unresolved << model
42
- dependencies = @model_dependencies[model]
43
-
44
- if dependencies
45
- dependencies.each do |dependency|
46
- resolve_dependencies(dependency) unless @resolved_queue.include?(dependency)
47
- end
43
+ @model_dependencies[model]&.each do |dependency|
44
+ resolve_dependencies(dependency) unless @resolved_queue.include?(dependency)
48
45
  end
49
46
 
50
47
  @resolved_queue << model
51
48
  @unresolved.delete(model)
52
49
  end
53
-
50
+
54
51
  def get_model_dependencies(model)
55
52
  associations = model.reflect_on_all_associations(:belongs_to).reject do |association|
56
53
  association.options[:optional] == true # Excluded Optional Associations
57
54
  end
58
-
55
+
59
56
  return [] if associations.blank?
60
-
57
+
61
58
  associations.map do |association|
62
59
  if association.options[:class_name]
63
60
  constantize_class_name(association.options[:class_name], model.name)
@@ -65,7 +62,7 @@ module Seedie
65
62
  types = find_polymorphic_types(model, association.name)
66
63
 
67
64
  if types.blank?
68
- puts "Polymorphic type not found for #{model.name}. Ignoring..."
65
+ puts "Polymorphic type not found for #{model.name}. Ignoring..."
69
66
  next
70
67
  end
71
68
  else
@@ -74,8 +71,6 @@ module Seedie
74
71
  end.compact
75
72
  end
76
73
 
77
- private
78
-
79
74
  def constantize_class_name(class_name, model_name)
80
75
  namespaced_class_name = if model_name.include?("::")
81
76
  "#{model_name.deconstantize}::#{class_name}"
@@ -90,6 +85,6 @@ module Seedie
90
85
  class_name.constantize
91
86
  end
92
87
  end
93
- end
88
+ end
94
89
  end
95
90
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  class ModelFields
3
- DEFAULT_DISABLED_FIELDS = %w[id created_at updated_at]
5
+ DEFAULT_DISABLED_FIELDS = %w[id created_at updated_at].freeze
4
6
 
5
7
  attr_reader :model_name, :model_config, :fields, :disabled_fields, :foreign_fields
6
-
8
+
7
9
  def initialize(model, model_config)
8
10
  @model_name = model.to_s
9
11
  @model_config = model_config
@@ -13,4 +15,4 @@ module Seedie
13
15
  @other_fields = model.column_names - @disabled_fields - @custom_fields - @foreign_fields
14
16
  end
15
17
  end
16
- end
18
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  class ModelSeeder
3
5
  include Reporters::Reportable
@@ -12,22 +14,22 @@ module Seedie
12
14
  @config = config
13
15
  @record_creator = Model::Creator.new(model, reporters)
14
16
  @reporters = reporters
15
-
16
17
  add_observers(@reporters)
17
18
  end
18
19
 
19
20
  def generate_records
20
- report(:model_seed_start, name: "#{model.to_s}")
21
+ report(:model_seed_start, name: model.to_s)
21
22
  model_count(model_config).times do |index|
22
23
  record = generate_record(model_config, index)
23
24
  associations_config = model_config["associations"]
24
25
 
25
- if associations_config.present?
26
- Associations::HasMany.new(record, model, associations_config, reporters).generate_associations
27
- Associations::HasOne.new(record, model, associations_config, reporters).generate_associations
28
- end
26
+ next unless associations_config.present?
27
+
28
+ Associations::HasMany.new(record, model, associations_config, reporters).generate_associations
29
+ Associations::HasAndBelongsToMany.new(record, model, associations_config, reporters).generate_associations
30
+ Associations::HasOne.new(record, model, associations_config, reporters).generate_associations
29
31
  end
30
- report(:model_seed_finish, name: "#{model.to_s}")
32
+ report(:model_seed_finish, name: model.to_s)
31
33
  end
32
34
 
33
35
  private
@@ -40,21 +42,8 @@ module Seedie
40
42
  end
41
43
 
42
44
  def generate_record(model_config, index)
43
- associated_field_set = generate_belongs_to_associations(model, model_config)
44
-
45
- field_values_set = FieldValuesSet.new(model, model_config, index).generate_field_values
46
- field_values_set.merge!(associated_field_set)
45
+ field_values_set = FieldValuesSet.new(model, model_config, index).generate_field_values_with_associations
47
46
  @record_creator.create!(field_values_set)
48
47
  end
49
-
50
- def generate_belongs_to_associations(model, model_config)
51
- associations_config = model_config["associations"]
52
- return {} unless associations_config.present?
53
-
54
- belongs_to_associations = Associations::BelongsTo.new(model, associations_config, reporters)
55
- belongs_to_associations.generate_associations
56
-
57
- return belongs_to_associations.associated_field_set
58
- end
59
48
  end
60
- end
49
+ end
@@ -1,20 +1,24 @@
1
- module PolymorphicAssociationHelper
2
- # Returns the type of the polymorphic association
3
- # We need only one polymorphic association while generating config
4
- # this makes it easier to sort according to dependencies
5
- def find_polymorphic_types(model, association_name)
6
- type = @models.find { |potential_model| has_association?(potential_model, association_name) }
7
- type&.name&.underscore
8
- end
1
+ # frozen_string_literal: true
9
2
 
10
- def has_association?(model, association_name)
11
- associations = select_associations(model)
12
- associations.any? { |association| association.options[:as] == association_name }
13
- end
14
-
15
- def select_associations(model)
16
- model.reflect_on_all_associations.select do |reflection|
17
- %i[has_many has_one].include?(reflection.macro)
3
+ module Seedie
4
+ module PolymorphicAssociationHelper
5
+ # Returns the type of the polymorphic association
6
+ # We need only one polymorphic association while generating config
7
+ # this makes it easier to sort according to dependencies
8
+ def find_polymorphic_types(_model, association_name)
9
+ type = @models.find { |potential_model| has_association?(potential_model, association_name) }
10
+ type&.name&.underscore
11
+ end
12
+
13
+ def has_association?(model, association_name)
14
+ associations = select_associations(model)
15
+ associations.any? { |association| association.options[:as] == association_name }
16
+ end
17
+
18
+ def select_associations(model)
19
+ model.reflect_on_all_associations.select do |reflection|
20
+ %i[has_many has_one].include?(reflection.macro)
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "seedie"
2
- require "rails"
3
4
 
4
5
  module Seedie
5
6
  class Railtie < Rails::Railtie
@@ -7,4 +8,4 @@ module Seedie
7
8
  load "tasks/seedie.rake"
8
9
  end
9
10
  end
10
- end
11
+ end
@@ -1,81 +1,88 @@
1
- module Reporters
2
- class BaseReporter
3
- INDENT_SIZE = 2
1
+ # frozen_string_literal: true
4
2
 
5
- attr_reader :output, :reports
3
+ module Seedie
4
+ module Reporters
5
+ class BaseReporter
6
+ INDENT_SIZE = 2
6
7
 
7
- def initialize(output = nil)
8
- @output = output || StringIO.new
9
- @reports = []
10
- @indent_level = 0
11
- end
8
+ attr_reader :output, :reports
12
9
 
13
- def update(event_type, options)
14
- raise NotImplementedError, "Subclasses must define 'update'."
15
- end
10
+ def initialize(output = nil)
11
+ @output = output || StringIO.new
12
+ @reports = []
13
+ @indent_level = 0
14
+ end
16
15
 
17
- def close
18
- return if output.closed?
19
- output.flush
20
- end
16
+ def update(event_type, options)
17
+ raise NotImplementedError, "Subclasses must define 'update'."
18
+ end
21
19
 
22
- private
20
+ def close
21
+ return if output.closed?
23
22
 
24
- def messages(event_type, options)
25
- case event_type
26
- when :seed_start
27
- "############ SEEDIE RUNNING #############"
28
- when :seed_finish
29
- "############ SEEDIE FINISHED ############"
30
- when :model_seed_start
31
- "Seeding #{options[:name]}"
32
- when :model_seed_finish
33
- "Seeding #{options[:name]} finished!"
34
- when :record_created
35
- "Created #{options[:name]} with id: #{options[:id]}"
36
- when :has_many_start
37
- "Creating HasMany associations:"
38
- when :belongs_to_start
39
- "Creating BelongsTo associations:"
40
- when :has_one_start
41
- "Creating HasOne associations:"
42
- when :associated_records
43
- "Creating #{options[:count]} #{options[:name]} for #{options[:parent_name]}"
44
- when :random_association
45
- "Randomly associating #{options[:name]} with id: #{options[:id]} for #{options[:parent_name]}"
46
- when :unique_association
47
- "Uniquely associating #{options[:name]} for #{options[:parent_name]}"
48
- when :belongs_to_associations
49
- "Creating a new #{options[:name].titleize} for #{options[:parent_name]}"
50
- else
51
- "Unknown event type"
23
+ output.flush
52
24
  end
53
- end
54
25
 
55
- def indent_level_for(event_type)
56
- indent_levels = {
57
- seed_start: 0,
58
- seed_finish: 0,
59
- model_seed_start: 1,
60
- model_seed_finish: 1,
61
- record_created: 1,
62
- random_association: 1,
63
- has_many_start: 2,
64
- belongs_to_start: 2,
65
- has_one_start: 2,
66
- associated_records: 3,
67
- belongs_to_associations: 3
68
- }
26
+ private
69
27
 
70
- indent_levels[event_type]
71
- end
28
+ def messages(event_type, options)
29
+ case event_type
30
+ when :seed_start
31
+ "############ SEEDIE RUNNING #############"
32
+ when :seed_finish
33
+ "############ SEEDIE FINISHED ############"
34
+ when :model_seed_start
35
+ "Seeding #{options[:name]}"
36
+ when :model_seed_finish
37
+ "Seeding #{options[:name]} finished!"
38
+ when :record_created
39
+ "Created #{options[:name]} with id: #{options[:id]}"
40
+ when :has_many_start
41
+ "Creating HasMany associations:"
42
+ when :belongs_to_start
43
+ "Creating BelongsTo associations:"
44
+ when :has_one_start
45
+ "Creating HasOne associations:"
46
+ when :has_and_belongs_to_many_start
47
+ "Creating HasAndBelongsToMany associations:"
48
+ when :associated_records
49
+ "Creating #{options[:count]} #{options[:name]} for #{options[:parent_name]}"
50
+ when :random_association
51
+ "Randomly associating #{options[:name]} with id: #{options[:id]} for #{options[:parent_name]}"
52
+ when :unique_association
53
+ "Uniquely associating #{options[:name]} for #{options[:parent_name]}"
54
+ when :belongs_to_associations
55
+ "Creating a new #{options[:name].titleize} for #{options[:parent_name]}"
56
+ else
57
+ "Unknown event type"
58
+ end
59
+ end
60
+
61
+ def indent_level_for(event_type)
62
+ indent_levels = {
63
+ seed_start: 0,
64
+ seed_finish: 0,
65
+ model_seed_start: 1,
66
+ model_seed_finish: 1,
67
+ record_created: 1,
68
+ random_association: 1,
69
+ has_many_start: 2,
70
+ belongs_to_start: 2,
71
+ has_one_start: 2,
72
+ associated_records: 3,
73
+ belongs_to_associations: 3
74
+ }
75
+
76
+ indent_levels[event_type]
77
+ end
72
78
 
73
- def set_indent_level(event_type)
74
- if event_type.in?([:record_created, :random_association, :unique_association])
75
- @indent_level += 1 if !@reports.last[:event_type].in?([:record_created, :random_association, :unique_association])
76
- elsif @reports.blank? || @reports.last[:event_type] != event_type
77
- @indent_level = indent_level_for(event_type)
79
+ def update_indent_level(event_type)
80
+ if event_type.in?(%i[record_created random_association unique_association])
81
+ @indent_level += 1 if !@reports.last[:event_type].in?(%i[record_created random_association unique_association])
82
+ elsif @reports.blank? || @reports.last[:event_type] != event_type
83
+ @indent_level = indent_level_for(event_type)
84
+ end
78
85
  end
79
86
  end
80
87
  end
81
- end
88
+ end
@@ -1,16 +1,20 @@
1
- module Reporters
2
- class ConsoleReporter < BaseReporter
3
- def initialize
4
- super($stdout)
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Seedie
4
+ module Reporters
5
+ class ConsoleReporter < BaseReporter
6
+ def initialize
7
+ super($stdout)
8
+ end
6
9
 
7
- def update(event_type, options)
8
- indent_level = set_indent_level(event_type)
9
- message = messages(event_type, options)
10
- @reports << { event_type: event_type, message: message }
10
+ def update(event_type, options)
11
+ update_indent_level(event_type)
12
+ message = messages(event_type, options)
13
+ @reports << { event_type: event_type, message: message }
11
14
 
12
- output.print "#{" " * INDENT_SIZE * @indent_level}"
13
- output.puts message
15
+ output.print "#{' ' * INDENT_SIZE * @indent_level}"
16
+ output.puts message
17
+ end
14
18
  end
15
19
  end
16
- end
20
+ end
@@ -1,16 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "observer"
2
4
 
3
- module Reporters
4
- module Reportable
5
- include Observable
5
+ module Seedie
6
+ module Reporters
7
+ module Reportable
8
+ include Observable
6
9
 
7
- def report(event_type, options = {})
8
- changed
9
- notify_observers(event_type, options)
10
- end
10
+ def report(event_type, options = {})
11
+ changed
12
+ notify_observers(event_type, options)
13
+ end
11
14
 
12
- def add_observers(observers)
13
- observers.each { |observer| add_observer(observer) }
15
+ def add_observers(observers)
16
+ observers.each { |observer| add_observer(observer) }
17
+ end
14
18
  end
15
19
  end
16
- end
20
+ end