fix: preserve wrapped rule import json values (#1358)

This commit is contained in:
Tomer Horowitz
2026-04-03 13:38:37 +03:00
committed by GitHub
parent 6d7ae0aa8a
commit 0dd3990502
2 changed files with 124 additions and 2 deletions

View File

@@ -289,9 +289,14 @@ class RuleImport < Import
def parse_json_safely(json_string, field_name)
return [] if json_string.blank?
# Clean up the JSON string - remove extra escaping that might come from CSV parsing
cleaned = json_string.to_s.strip
# Most API-created rows already store valid JSON. Parse them as-is before
# falling back to the legacy cleanup path for older malformed payloads.
parse_json_payload(cleaned, normalize_legacy_strings: false)
rescue JSON::ParserError
# Clean up the JSON string - remove extra escaping that might come from CSV parsing
# Remove surrounding quotes if present (both single and double)
cleaned = cleaned.gsub(/\A["']+|["']+\z/, "")
@@ -321,8 +326,46 @@ class RuleImport < Import
end
# Try parsing
JSON.parse(cleaned)
parse_json_payload(cleaned, normalize_legacy_strings: true)
rescue JSON::ParserError => e
raise JSON::ParserError.new("Invalid JSON in #{field_name}: #{e.message}. Raw value: #{json_string.inspect}")
end
def parse_json_payload(payload, normalize_legacy_strings:)
parsed = JSON.parse(payload)
parsed = JSON.parse(parsed) if wrapped_json_payload?(parsed)
normalize_json_values(parsed, normalize_legacy_strings:)
end
def wrapped_json_payload?(value)
return false unless value.is_a?(String)
stripped_value = value.strip
stripped_value.start_with?("[", "{")
end
def normalize_json_values(value, normalize_legacy_strings:)
case value
when Array
value.map { |item| normalize_json_values(item, normalize_legacy_strings:) }
when Hash
value.transform_values { |item| normalize_json_values(item, normalize_legacy_strings:) }
when String
normalized = value
.gsub(/\\u([0-9a-fA-F]{4})/i) { [ $1.to_i(16) ].pack("U") }
.gsub('\\"', '"')
if normalize_legacy_strings
normalized = normalized
.gsub("\\n", "\n")
.gsub("\\r", "\r")
.gsub("\\t", "\t")
end
normalized
else
value
end
end
end

View File

@@ -233,4 +233,83 @@ class RuleImportTest < ActiveSupport::TestCase
import.send(:import!)
end
end
test "imports valid JSON conditions whose values contain escaped quotes" do
csv = CSV.generate do |out|
out << %w[name resource_type active effective_date conditions actions]
out << [
"Quoted value rule",
"transaction",
true,
"",
[ { condition_type: "transaction_name", operator: "=", value: "ני\\u0022ע-קניה" } ].to_json,
[ { action_type: "set_transaction_name", value: "Quoted transfer" } ].to_json
]
end
import = @family.imports.create!(type: "RuleImport", raw_file_str: csv, col_sep: ",")
import.generate_rows_from_csv
assert_nothing_raised do
import.send(:import!)
end
rule = Rule.find_by!(family: @family, name: "Quoted value rule")
condition = rule.conditions.first
assert_equal "ני\"ע-קניה", condition.value
end
test "imports wrapped JSON payloads from legacy rows" do
wrapped_conditions = [ { condition_type: "transaction_name", operator: "like", value: "legacy grocery" } ].to_json.to_json
wrapped_actions = [ { action_type: "set_transaction_category", value: "Groceries" } ].to_json.to_json
csv = CSV.generate do |out|
out << %w[name resource_type active effective_date conditions actions]
out << [
"Wrapped payload rule",
"transaction",
true,
"",
wrapped_conditions,
wrapped_actions
]
end
import = @family.imports.create!(type: "RuleImport", raw_file_str: csv, col_sep: ",")
import.generate_rows_from_csv
assert_nothing_raised do
import.send(:import!)
end
rule = Rule.find_by!(family: @family, name: "Wrapped payload rule")
condition = rule.conditions.first
action = rule.actions.first
assert_equal "legacy grocery", condition.value
assert_equal @category.id, action.value
end
test "preserves literal backslash sequences in valid JSON values" do
csv = CSV.generate do |out|
out << %w[name resource_type active effective_date conditions actions]
out << [
"Literal backslash rule",
"transaction",
true,
"",
[ { condition_type: "transaction_name", operator: "=", value: 'C:\new\test' } ].to_json,
[ { action_type: "set_transaction_name", value: "Path rule" } ].to_json
]
end
import = @family.imports.create!(type: "RuleImport", raw_file_str: csv, col_sep: ",")
import.generate_rows_from_csv
import.send(:import!)
rule = Rule.find_by!(family: @family, name: "Literal backslash rule")
condition = rule.conditions.first
assert_equal 'C:\new\test', condition.value
end
end