seedie 0.3.0 → 0.4.1

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.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seedie
4
+ module Associations
5
+ class HasAndBelongsToMany < BaseAssociation
6
+ def generate_associations
7
+ return if association_config["has_and_belongs_to_many"].nil?
8
+
9
+ report(:has_and_belongs_to_many_start)
10
+ association_config["has_and_belongs_to_many"].each do |association_name, association_config|
11
+ association_class = association_name.to_s.classify.constantize
12
+ count = get_association_count(association_config)
13
+ config = only_count_given?(association_config) ? {} : association_config
14
+
15
+ report(:associated_records, name: association_name, count: count, parent_name: model.to_s)
16
+ count.times do |index|
17
+ field_values_set = FieldValuesSet.new(association_class, config, index).generate_field_values_with_associations
18
+ record_creator = Model::Creator.new(record.send(association_name), reporters)
19
+
20
+ record_creator.create!(field_values_set)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,18 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  module Associations
3
5
  class HasMany < BaseAssociation
4
6
  def generate_associations
5
7
  return if association_config["has_many"].nil?
6
-
8
+
7
9
  report(:has_many_start)
8
10
  association_config["has_many"].each do |association_name, association_config|
9
11
  association_class = association_name.to_s.classify.constantize
10
12
  count = get_association_count(association_config)
11
13
  config = only_count_given?(association_config) ? {} : association_config
12
-
14
+
13
15
  report(:associated_records, name: association_name, count: count, parent_name: model.to_s)
14
16
  count.times do |index|
15
- field_values_set = FieldValuesSet.new(association_class, config, index).generate_field_values
17
+ field_values_set = FieldValuesSet.new(association_class, config, index).generate_field_values_with_associations
16
18
  record_creator = Model::Creator.new(record.send(association_name), reporters)
17
19
 
18
20
  record_creator.create!(field_values_set)
@@ -21,4 +23,4 @@ module Seedie
21
23
  end
22
24
  end
23
25
  end
24
- end
26
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  module Associations
3
5
  class HasOne < BaseAssociation
@@ -5,25 +7,23 @@ module Seedie
5
7
  return if association_config["has_one"].nil?
6
8
 
7
9
  report(:has_one_start)
8
-
10
+
9
11
  association_config["has_one"].each do |association_name, association_config|
10
12
  reflection = model.reflect_on_association(association_name)
11
13
  association_class = reflection.klass
12
14
  count = get_association_count(association_config)
13
-
15
+
14
16
  report(:associated_records, count: count, name: association_name, parent_name: model.to_s)
15
- if count > 1
16
- raise InvalidAssociationConfigError, "has_one association cannot be more than 1"
17
- else
18
- config = only_count_given?(association_config) ? {} : association_config
19
- field_values_set = FieldValuesSet.new(association_class, config, INDEX).generate_field_values
20
- parent_field_set = generate_associated_field(record.id, reflection.foreign_key)
21
-
22
- record_creator = Model::Creator.new(association_class, reporters)
23
- record_creator.create!(field_values_set.merge!(parent_field_set))
24
- end
17
+ raise InvalidAssociationConfigError, "has_one association cannot be more than 1" if count > 1
18
+
19
+ config = only_count_given?(association_config) ? {} : association_config
20
+ field_values_set = FieldValuesSet.new(association_class, config, INDEX).generate_field_values
21
+ parent_field_set = generate_associated_field(record.id, reflection.foreign_key)
22
+
23
+ record_creator = Model::Creator.new(association_class, reporters)
24
+ record_creator.create!(field_values_set.merge!(parent_field_set))
25
25
  end
26
26
  end
27
27
  end
28
28
  end
29
- end
29
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  class Configuration
3
5
  attr_accessor :default_count, :custom_attributes
@@ -6,5 +8,23 @@ module Seedie
6
8
  @default_count = nil
7
9
  @custom_attributes = {}
8
10
  end
11
+
12
+ # Prepares the custom_attributes hash for the specified models.
13
+ #
14
+ # This method ensures that the necessary keys exist in the custom_attributes hash.
15
+ # This prevents NoMethodError when setting model-specific custom attributes.
16
+ #
17
+ # Example usage:
18
+ # config.prepare_custom_attributes_for :user, :account
19
+ #
20
+ # Then this will work:
21
+ # config.custom_attributes[:user][:name] = "Name"
22
+ # config.custom_attributes[:account][:email] = "email@example.com"
23
+ #
24
+ def prepare_custom_attributes_for(*models)
25
+ models.inject(@custom_attributes) do |hash, key|
26
+ hash[key] ||= {}
27
+ end
28
+ end
9
29
  end
10
- end
30
+ end
@@ -1,9 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  module FieldValues
3
5
  class CustomValue
4
- VALID_KEYS = ["values", "value", "options"].freeze
5
- PICK_STRATEGIES = ["random", "sequential"].freeze
6
-
7
6
  attr_reader :name, :parsed_value
8
7
 
9
8
  def initialize(name, value_template, index)
@@ -12,7 +11,7 @@ module Seedie
12
11
  @index = index
13
12
  @parsed_value = ""
14
13
 
15
- validate_value_template
14
+ ValueTemplateValidator.new(@value_template, @index, @name).validate
16
15
  end
17
16
 
18
17
  def generate_custom_field_value
@@ -27,89 +26,21 @@ module Seedie
27
26
 
28
27
  private
29
28
 
30
- def validate_value_template
31
- return unless @value_template.is_a?(Hash)
32
-
33
- validate_keys
34
- validate_values if @value_template.key?("values")
35
- validate_options if @value_template.key?("options")
36
- end
37
-
38
- def validate_values
39
- values = @value_template["values"]
40
- options = @value_template["options"]
41
-
42
- if values.is_a?(Array) || values.is_a?(Hash)
43
- validate_sequential_values_length
44
- else
45
- raise InvalidCustomFieldValuesError, "The values key for #{@name} must be an array or a hash with start and end keys."
46
- end
47
- end
48
-
49
- def validate_options
50
- options = @value_template["options"]
51
- pick_strategy = options["pick_strategy"]
52
-
53
- if pick_strategy.present? && !PICK_STRATEGIES.include?(pick_strategy)
54
- raise InvalidCustomFieldOptionsError,
55
- "The pick_strategy for #{@name} must be either 'sequential' or 'random'."
56
- end
57
- end
58
-
59
- ## If pick strategy is sequential, we need to ensure there is a value for each index
60
- # If there isn't sufficient values, we raise an error
61
- def validate_sequential_values_length
62
- return unless @value_template.key?("options")
63
- return unless @value_template["options"]["pick_strategy"] == "sequential"
64
-
65
- values = @value_template["values"]
66
-
67
- if values.is_a?(Hash) && values.keys.sort == ["end", "start"]
68
- # Assuming the values are an inclusive range
69
- values_length = values["end"] - values["start"] + 1
70
- else
71
- values_length = values.length
72
- end
73
-
74
- if values_length < @index + 1
75
- raise CustomFieldNotEnoughValuesError,
76
- "There are not enough values for #{@name}. Please add more values."
77
- end
78
- end
79
-
80
- def validate_keys
81
- invalid_keys = @value_template.keys - VALID_KEYS
82
-
83
- if invalid_keys.present?
84
- raise InvalidCustomFieldKeysError,
85
- "Invalid keys for #{@name}: #{invalid_keys.join(", ")}. Only #{VALID_KEYS} are allowed."
86
- end
87
-
88
- if @value_template.key?("values")
89
- if @value_template.key?("value")
90
- raise InvalidCustomFieldKeysError,
91
- "Invalid keys for #{@name}: values and value cannot be used together."
92
- end
93
-
94
- if @value_template["values"].is_a?(Hash)
95
- if !@value_template["values"].key?("start") || !@value_template["values"].key?("end")
96
- raise InvalidCustomFieldValuesError,
97
- "The values key for #{@name} must be an array or a hash with start and end keys."
98
- end
99
- end
100
- end
101
- end
102
-
103
29
  def generate_custom_value_from_string
104
30
  @parsed_value = @value_template.gsub("{{index}}", @index.to_s)
105
31
 
106
32
  @parsed_value.gsub!(/\{\{(.+?)\}\}/) do
107
- method_string = $1
33
+ method_string = ::Regexp.last_match(1)
108
34
 
109
- if method_string.start_with?("Faker::")
110
- eval($1)
111
- else
112
- raise InvalidFakerMethodError, "Invalid method: #{method_string}"
35
+ raise InvalidFakerMethodError, "Invalid method: #{method_string}" unless method_string.start_with?("Faker::")
36
+
37
+ method_chain = method_string.split(".")
38
+ # Faker::Name will be shifted off the array
39
+ faker_class = method_chain.shift.constantize
40
+
41
+ # For Faker::Internet.unique.email, there will be two methods in the array
42
+ method_chain.reduce(faker_class) do |current_class_or_value, method|
43
+ current_class_or_value.public_send(method)
113
44
  end
114
45
  end
115
46
  end
@@ -123,7 +54,7 @@ module Seedie
123
54
  generate_custom_values_from_range(@value_template["values"]["start"], @value_template["values"]["end"])
124
55
  end
125
56
  options = @value_template["options"]
126
-
57
+
127
58
  if options.present? && options["pick_strategy"] == "sequential"
128
59
  @parsed_value = values[@index]
129
60
  else
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  module FieldValues
3
5
  class FakeValue
@@ -9,41 +11,107 @@ module Seedie
9
11
  def generate_fake_value
10
12
  case @column.type
11
13
  when :string, :text, :citext
12
- Faker::Lorem.word
14
+ generate_string
13
15
  when :uuid
14
- SecureRandom.uuid
16
+ generate_uuid
15
17
  when :integer, :bigint, :smallint
16
- Faker::Number.number(digits: 5)
18
+ generate_integer
17
19
  when :decimal, :float, :real
18
- Faker::Number.decimal(l_digits: 2, r_digits: 2)
20
+ generate_decimal
19
21
  when :datetime, :timestamp, :timestamptz
20
- Faker::Time.between(from: DateTime.now - 1, to: DateTime.now)
22
+ generate_datetime
21
23
  when :date
22
- Faker::Date.between(from: Date.today - 2, to: Date.today)
24
+ generate_date
23
25
  when :time, :timetz
24
- Faker::Time.forward(days: 23, period: :morning)
26
+ generate_time
25
27
  when :boolean
26
- Faker::Boolean.boolean
28
+ generate_boolean
27
29
  when :json, :jsonb
28
- { "value" => { "key1" => Faker::Lorem.word, "key2" => Faker::Number.number(digits: 2) } }
30
+ generate_json
29
31
  when :inet
30
- Faker::Internet.ip_v4_address
32
+ generate_inet
31
33
  when :cidr, :macaddr
32
- Faker::Internet.mac_address
34
+ generate_macaddr
33
35
  when :bytea
34
- Faker::Internet.password
36
+ generate_bytea
35
37
  when :bit, :bit_varying
36
- ["0","1"].sample
38
+ generate_bit
37
39
  when :money
38
- Faker::Commerce.price.to_s
40
+ generate_money
39
41
  when :hstore
40
- { "value" => { "key1" => Faker::Lorem.word, "key2" => Faker::Number.number(digits: 2) } }
42
+ generate_hstore
41
43
  when :year
42
- rand(1901..2155)
44
+ generate_year
43
45
  else
44
46
  raise UnknownColumnTypeError, "Unknown column type: #{@column.type}"
45
47
  end
46
48
  end
49
+
50
+ private
51
+
52
+ def generate_string
53
+ Faker::Lorem.word
54
+ end
55
+
56
+ def generate_uuid
57
+ SecureRandom.uuid
58
+ end
59
+
60
+ def generate_integer
61
+ Faker::Number.number(digits: 5)
62
+ end
63
+
64
+ def generate_decimal
65
+ Faker::Number.decimal(l_digits: 2, r_digits: 2)
66
+ end
67
+
68
+ def generate_datetime
69
+ Faker::Time.between(from: DateTime.now - 1, to: DateTime.now)
70
+ end
71
+
72
+ def generate_date
73
+ Faker::Date.between(from: Date.today - 2, to: Date.today)
74
+ end
75
+
76
+ def generate_time
77
+ Faker::Time.forward(days: 23, period: :morning)
78
+ end
79
+
80
+ def generate_boolean
81
+ Faker::Boolean.boolean
82
+ end
83
+
84
+ def generate_json
85
+ { "value" => { "key1" => Faker::Lorem.word, "key2" => Faker::Number.number(digits: 2) } }
86
+ end
87
+
88
+ def generate_inet
89
+ Faker::Internet.ip_v4_address
90
+ end
91
+
92
+ def generate_macaddr
93
+ Faker::Internet.mac_address
94
+ end
95
+
96
+ def generate_bytea
97
+ Faker::Internet.password
98
+ end
99
+
100
+ def generate_bit
101
+ %w[0 1].sample
102
+ end
103
+
104
+ def generate_money
105
+ Faker::Commerce.price.to_s
106
+ end
107
+
108
+ def generate_hstore
109
+ { "value" => { "key1" => Faker::Lorem.word, "key2" => Faker::Number.number(digits: 2) } }
110
+ end
111
+
112
+ def generate_year
113
+ rand(1901..2155)
114
+ end
47
115
  end
48
116
  end
49
- end
117
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Seedie
2
4
  module FieldValues
3
5
  class FakerBuilder
@@ -14,14 +16,15 @@ module Seedie
14
16
  end
15
17
 
16
18
  def build_faker_constant
17
- return @seedie_config_custom_attributes[@name.to_sym] if @seedie_config_custom_attributes.key?(@name.to_sym)
19
+ custom_attribute = fetch_custom_attribute
20
+ return custom_attribute if fetch_custom_attribute
18
21
 
19
22
  @unique_prefix = "unique." if has_validation?(:uniqueness)
20
-
23
+
21
24
  add_faker_class_and_method(@column.type)
22
-
25
+
23
26
  if has_validation?(:inclusion)
24
- handle_inclusion_validation
27
+ handle_inclusion_validation
25
28
  else
26
29
  @options += handle_numericality_validation if has_validation?(:numericality)
27
30
  @options += handle_length_validation if has_validation?(:length)
@@ -37,61 +40,57 @@ module Seedie
37
40
 
38
41
  private
39
42
 
43
+ def fetch_custom_attribute
44
+ if @seedie_config_custom_attributes[@name.to_sym].is_a?(Hash)
45
+ return @seedie_config_custom_attributes.dig(@name.to_sym, @column.name.to_sym)
46
+ end
47
+
48
+ @seedie_config_custom_attributes[@name.to_sym]
49
+ end
50
+
40
51
  def add_faker_class_and_method(type)
41
52
  case type
42
53
  when :string, :text, :citext
43
- @class_prefix = "Lorem."
44
- @method_prefix = "word"
54
+ set_faker("Lorem.", "word")
45
55
  when :uuid
46
- @class_prefix = "Internet."
47
- @method_prefix = "uuid"
56
+ set_faker("Internet.", "uuid")
48
57
  when :integer, :bigint, :smallint
49
- @class_prefix = "Number."
50
- @method_prefix = "number"
51
- @options = "(digits: 5)"
58
+ set_faker("Number.", "number", "(digits: 5)")
52
59
  when :decimal, :float, :real
53
- @class_prefix = "Number."
54
- @method_prefix = "decimal"
55
- @options = "(l_digits: 2, r_digits: 2)"
60
+ set_faker("Number.", "decimal", "(l_digits: 2, r_digits: 2)")
56
61
  when :datetime, :timestamp, :timestamptz, :time, :timetz
57
- @class_prefix = "Time."
58
- @method_prefix = "between"
59
- @options = "(from: DateTime.now - 1, to: DateTime.now)"
62
+ set_faker("Time.", "between", "(from: DateTime.now - 1, to: DateTime.now)")
60
63
  when :date
61
- @class_prefix = "Date."
62
- @method_prefix = "between"
63
- @options = "(from: Date.today - 1, to: Date.today)"
64
+ set_faker("Date.", "between", "(from: Date.today - 1, to: Date.today)")
64
65
  when :boolean
65
- @class_prefix = "Boolean."
66
- @method_prefix = "boolean"
66
+ set_faker("Boolean.", "boolean")
67
67
  when :json, :jsonb
68
68
  @faker_expression = { "value" => "Json.shallow_json(width: 3, options: { key: 'Name.first_name', value: 'Number.number(digits: 2)' })" }
69
69
  when :inet
70
- @class_prefix = "Internet."
71
- @method_prefix = "ip_v4_address"
70
+ set_faker("Internet.", "ip_v4_address")
72
71
  when :cidr, :macaddr
73
- @class_prefix = "Internet."
74
- @method_prefix = "mac_address"
72
+ set_faker("Internet.", "mac_address")
75
73
  when :bytea
76
- @class_prefix = "Internet."
77
- @method_prefix = "password"
74
+ set_faker("Internet.", "password")
78
75
  when :bit, :bit_varying
79
- @class_prefix = "Internet."
80
- @method_prefix = "password"
76
+ set_faker("Internet.", "password")
81
77
  when :money
82
- @class_prefix = "Commerce."
83
- @method_prefix = "price.to_s"
78
+ set_faker("Commerce.", "price.to_s")
84
79
  when :hstore
85
80
  @faker_expression = { "value" => "Json.shallow_json(width: 3, options: { key: 'Name.first_name', value: 'Number.number(digits: 2)' })" }
86
81
  when :year
87
- @class_prefix = "Number."
88
- @method_prefix = "number"
89
- @options = "(digits: 4)"
82
+ set_faker("Number.", "number", "(digits: 4)")
90
83
  else
91
84
  raise UnknownColumnTypeError, "Unknown column type: #{type}"
92
85
  end
93
86
  end
94
87
 
88
+ def set_faker(class_prefix, method_prefix, options = "")
89
+ @class_prefix = class_prefix
90
+ @method_prefix = method_prefix
91
+ @options = options
92
+ end
93
+
95
94
  def has_validation?(kind)
96
95
  @validations.any? { |validation| validation.kind == kind }
97
96
  end
@@ -132,7 +131,8 @@ module Seedie
132
131
  @method_prefix = ""
133
132
  @options = ""
134
133
  if options[:in].is_a?(Range)
135
- @faker_expression = { "values" => { "start" => options[:in].first, "end" => options[:in].last }, "options" => { "pick_strategy" => "random" } }
134
+ @faker_expression = { "values" => { "start" => options[:in].first, "end" => options[:in].last },
135
+ "options" => { "pick_strategy" => "random" } }
136
136
  else
137
137
  @faker_expression = { "values" => options[:in], "options" => { "pick_strategy" => "random" } }
138
138
  end
@@ -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