seedie 0.2.0 → 0.4.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 (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