Skip to content

Commit 5a00cbe

Browse files
authored
Migrate add_value command to new tool (#579)
### TL;DR Migrated command to add new attribute value to the taxonomy from deprecated tool version to new one. ### What changed? - Migrated `AddValueCommand` to handle value creation logic - Added `add_value` CLI command that accepts a name and attribute friendly ID - Added `next_id` helper method to `Value` model for ID generation - Added `add_value` method to `Attribute` model ### How to test? 1. Run the CLI command: `bin/product_taxonomy add_value "Blueish" "color"` to add a new value to the color attribute 2. Verify the value is created with correct ID, friendly_id, and handle 3. Confirm the value appears in the appropriate data files 4. Try adding a value to an extended attribute to verify it's blocked 5. Attempt to add a duplicate value to ensure it's prevented Sample run: ``` ❯ bin/product_taxonomy add_value "Blueish" "color" Created value `Blueish` for attribute `Color` Dumping attributes... Updated `/Users/danielgross/product-taxonomy/data/attributes.yml` Dumping values... Updated `/Users/danielgross/product-taxonomy/data/values.yml` Syncing EN localizations Syncing values... Wrote values localizations to /Users/danielgross/product-taxonomy/data/localizations/values/en.yml Version: unstable Generating sibling groups... Generating category search index... Generating attributes... Generating mappings... Generating attributes with categories... Generating attribute with categories search index... Completed in 2.78 seconds ```
2 parents a75567d + 2d40925 commit 5a00cbe

File tree

7 files changed

+237
-0
lines changed

7 files changed

+237
-0
lines changed

dev/lib/product_taxonomy.rb

+1
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,4 @@ def data_path = DATA_PATH
5353
require_relative "product_taxonomy/commands/dump_values_command"
5454
require_relative "product_taxonomy/commands/sync_en_localizations_command"
5555
require_relative "product_taxonomy/commands/add_category_command"
56+
require_relative "product_taxonomy/commands/add_value_command"

dev/lib/product_taxonomy/cli.rb

+5
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,10 @@ def sync_en_localizations
6767
def add_category(name, parent_id)
6868
AddCategoryCommand.new(options.merge(name:, parent_id:)).run
6969
end
70+
71+
desc "add_value NAME ATTRIBUTE_FRIENDLY_ID", "Add a new value with NAME to the primary attribute with ATTRIBUTE_FRIENDLY_ID"
72+
def add_value(name, attribute_friendly_id)
73+
AddValueCommand.new(options.merge(name:, attribute_friendly_id:)).run
74+
end
7075
end
7176
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
module ProductTaxonomy
4+
class AddValueCommand < Command
5+
def initialize(options)
6+
super
7+
load_taxonomy
8+
@name = options[:name]
9+
@attribute_friendly_id = options[:attribute_friendly_id]
10+
end
11+
12+
def execute
13+
create_value!
14+
update_data_files!
15+
end
16+
17+
private
18+
19+
def create_value!
20+
@attribute = Attribute.find_by(friendly_id: @attribute_friendly_id)
21+
raise "Attribute `#{@attribute_friendly_id}` not found" if @attribute.nil?
22+
if @attribute.extended?
23+
raise "Attribute `#{@attribute.name}` is an extended attribute, please use a primary attribute instead"
24+
end
25+
26+
friendly_id = IdentifierFormatter.format_friendly_id("#{@attribute.friendly_id}__#{@name}")
27+
value = Value.create_validate_and_add!(
28+
id: Value.next_id,
29+
name: @name,
30+
friendly_id:,
31+
handle: IdentifierFormatter.format_handle(friendly_id),
32+
)
33+
@attribute.add_value(value)
34+
35+
logger.info("Created value `#{value.name}` for attribute `#{@attribute.name}`")
36+
end
37+
38+
def update_data_files!
39+
DumpAttributesCommand.new(options).execute
40+
DumpValuesCommand.new(options).execute
41+
SyncEnLocalizationsCommand.new(targets: "values").execute
42+
GenerateDocsCommand.new({}).execute
43+
44+
logger.warn(
45+
"Attribute has custom sorting, please ensure your new value is in the right position in data/attributes.yml",
46+
) if @attribute.manually_sorted?
47+
end
48+
end
49+
end

dev/lib/product_taxonomy/models/attribute.rb

+7
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ def add_extended_attribute(extended_attribute)
107107
@extended_attributes << extended_attribute
108108
end
109109

110+
# Add a value to the attribute.
111+
#
112+
# @param value [Value] The value to add.
113+
def add_value(value)
114+
@values << value
115+
end
116+
110117
# The global ID of the attribute
111118
#
112119
# @return [String]

dev/lib/product_taxonomy/models/value.rb

+5
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ def all_values_sorted
6363
]
6464
end
6565
end
66+
67+
# Get the next ID for a newly created value.
68+
#
69+
# @return [Integer] The next ID.
70+
def next_id = (all.max_by(&:id)&.id || 0) + 1
6671
end
6772

6873
validates :id, presence: true, numericality: { only_integer: true }
+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
module ProductTaxonomy
6+
class AddValueCommandTest < TestCase
7+
setup do
8+
@attribute = Attribute.new(
9+
id: 1,
10+
name: "Color",
11+
description: "Defines the primary color or pattern",
12+
friendly_id: "color",
13+
handle: "color",
14+
values: []
15+
)
16+
@extended_attribute = ExtendedAttribute.new(
17+
name: "Clothing Color",
18+
description: "Color of the clothing",
19+
friendly_id: "clothing_color",
20+
handle: "clothing_color",
21+
values_from: @attribute
22+
)
23+
@existing_value = Value.new(
24+
id: 1,
25+
name: "Black",
26+
friendly_id: "color__black",
27+
handle: "color__black"
28+
)
29+
@attribute.add_value(@existing_value)
30+
31+
Attribute.add(@attribute)
32+
Attribute.add(@extended_attribute)
33+
Value.add(@existing_value)
34+
35+
AddValueCommand.any_instance.stubs(:load_taxonomy)
36+
DumpAttributesCommand.any_instance.stubs(:load_taxonomy)
37+
DumpValuesCommand.any_instance.stubs(:load_taxonomy)
38+
SyncEnLocalizationsCommand.any_instance.stubs(:load_taxonomy)
39+
GenerateDocsCommand.any_instance.stubs(:load_taxonomy)
40+
end
41+
42+
test "execute successfully adds a new value to an attribute" do
43+
DumpAttributesCommand.any_instance.expects(:execute).once
44+
DumpValuesCommand.any_instance.expects(:execute).once
45+
SyncEnLocalizationsCommand.any_instance.expects(:execute).once
46+
GenerateDocsCommand.any_instance.expects(:execute).once
47+
48+
AddValueCommand.new(name: "Blue", attribute_friendly_id: "color").execute
49+
50+
new_value = @attribute.values.find { |v| v.name == "Blue" }
51+
assert_not_nil new_value
52+
assert_equal 2, new_value.id # Since id 1 already exists
53+
assert_equal "Blue", new_value.name
54+
assert_equal "color__blue", new_value.friendly_id
55+
assert_equal "color__blue", new_value.handle
56+
assert_not_nil Value.find_by(friendly_id: "color__blue")
57+
end
58+
59+
test "execute raises error when attribute not found" do
60+
assert_raises(RuntimeError) do
61+
AddValueCommand.new(name: "Blue", attribute_friendly_id: "nonexistent").execute
62+
end
63+
end
64+
65+
test "execute raises error when trying to add value to extended attribute" do
66+
assert_raises(RuntimeError) do
67+
AddValueCommand.new(name: "Blue", attribute_friendly_id: "clothing_color").execute
68+
end
69+
end
70+
71+
test "execute raises error when value already exists" do
72+
assert_raises(ActiveModel::ValidationError) do
73+
AddValueCommand.new(name: "Black", attribute_friendly_id: "color").execute
74+
end
75+
end
76+
77+
test "execute warns when attribute has custom sorting" do
78+
@attribute.stubs(:manually_sorted?).returns(true)
79+
80+
DumpAttributesCommand.any_instance.stubs(:execute)
81+
DumpValuesCommand.any_instance.stubs(:execute)
82+
SyncEnLocalizationsCommand.any_instance.stubs(:execute)
83+
GenerateDocsCommand.any_instance.stubs(:execute)
84+
85+
logger = mock
86+
logger.expects(:info).once
87+
logger.expects(:warn).once.with(regexp_matches(/custom sorting/))
88+
89+
command = AddValueCommand.new(name: "Blue", attribute_friendly_id: "color")
90+
command.stubs(:logger).returns(logger)
91+
command.execute
92+
93+
new_value = @attribute.values.find { |v| v.name == "Blue" }
94+
assert_not_nil new_value
95+
end
96+
97+
test "execute generates correct friendly_id and handle" do
98+
DumpAttributesCommand.any_instance.stubs(:execute)
99+
DumpValuesCommand.any_instance.stubs(:execute)
100+
SyncEnLocalizationsCommand.any_instance.stubs(:execute)
101+
GenerateDocsCommand.any_instance.stubs(:execute)
102+
103+
IdentifierFormatter.expects(:format_friendly_id).with("color__Multi Word").returns("color__multi_word")
104+
IdentifierFormatter.expects(:format_handle).with("color__multi_word").returns("color__multi_word")
105+
106+
AddValueCommand.new(name: "Multi Word", attribute_friendly_id: "color").execute
107+
108+
new_value = @attribute.values.find { |v| v.name == "Multi Word" }
109+
assert_not_nil new_value
110+
assert_equal "color__multi_word", new_value.friendly_id
111+
assert_equal "color__multi_word", new_value.handle
112+
end
113+
114+
test "execute calls update_data_files! with correct options" do
115+
options = { name: "Blue", attribute_friendly_id: "color" }
116+
117+
DumpAttributesCommand.expects(:new).with(options).returns(stub(execute: true))
118+
DumpValuesCommand.expects(:new).with(options).returns(stub(execute: true))
119+
SyncEnLocalizationsCommand.expects(:new).with(targets: "values").returns(stub(execute: true))
120+
GenerateDocsCommand.expects(:new).with({}).returns(stub(execute: true))
121+
122+
AddValueCommand.new(options).execute
123+
end
124+
125+
test "execute assigns sequential IDs correctly" do
126+
DumpAttributesCommand.any_instance.stubs(:execute)
127+
DumpValuesCommand.any_instance.stubs(:execute)
128+
SyncEnLocalizationsCommand.any_instance.stubs(:execute)
129+
GenerateDocsCommand.any_instance.stubs(:execute)
130+
131+
AddValueCommand.new(name: "Blue", attribute_friendly_id: "color").execute
132+
AddValueCommand.new(name: "Green", attribute_friendly_id: "color").execute
133+
134+
new_values = @attribute.values.select { |v| v.name != "Black" }.sort_by(&:id)
135+
assert_equal 2, new_values.size
136+
assert_equal 2, new_values[0].id
137+
assert_equal 3, new_values[1].id
138+
assert_equal "Blue", new_values[0].name
139+
assert_equal "Green", new_values[1].name
140+
assert_not_nil Value.find_by(friendly_id: "color__blue")
141+
assert_not_nil Value.find_by(friendly_id: "color__green")
142+
end
143+
end
144+
end

dev/test/models/value_test.rb

+26
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,32 @@ class ValueTest < TestCase
213213
assert_equal ["Animal", "Rayé", "Autre"], sorted_values.map { _1.name(locale: "fr") }
214214
end
215215

216+
test "next_id returns 1 when there are no values" do
217+
Value.reset
218+
assert_equal 1, Value.next_id
219+
end
220+
221+
test "next_id returns max id + 1 when there are existing values" do
222+
Value.reset
223+
Value.add(Value.new(id: 5, name: "Red", friendly_id: "color__red", handle: "color__red"))
224+
Value.add(Value.new(id: 10, name: "Blue", friendly_id: "color__blue", handle: "color__blue"))
225+
Value.add(Value.new(id: 3, name: "Green", friendly_id: "color__green", handle: "color__green"))
226+
227+
assert_equal 11, Value.next_id
228+
end
229+
230+
test "next_id returns correct value after values have been reset" do
231+
Value.reset
232+
Value.add(Value.new(id: 5, name: "Red", friendly_id: "color__red", handle: "color__red"))
233+
assert_equal 6, Value.next_id
234+
235+
Value.reset
236+
assert_equal 1, Value.next_id
237+
238+
Value.add(Value.new(id: 3, name: "Blue", friendly_id: "color__blue", handle: "color__blue"))
239+
assert_equal 4, Value.next_id
240+
end
241+
216242
private
217243

218244
def stub_localizations

0 commit comments

Comments
 (0)