Skip to content

Commit 4c1b308

Browse files
Enable order preservation (#50)
* Enable order preservation * Update parse_packwerk_spec.rb * Bump minor version
1 parent cd72e20 commit 4c1b308

5 files changed

Lines changed: 200 additions & 5 deletions

File tree

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
parse_packwerk (0.26.1)
4+
parse_packwerk (0.27.0)
55
bigdecimal
66
sorbet-runtime
77

lib/parse_packwerk.rb

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ def initialize(packwerk_file_name)
3131

3232
extend T::Sig
3333

34+
# Configuration option to preserve the original key order when writing package.yml files.
35+
# When true, keys will be written in their original order from the file rather than
36+
# being sorted according to key_sort_order. This reduces diff churn when tools modify packages.
37+
# Defaults to false for backwards compatibility.
38+
@preserve_key_order = T.let(false, T::Boolean)
39+
40+
class << self
41+
extend T::Sig
42+
43+
sig { returns(T::Boolean) }
44+
attr_accessor :preserve_key_order
45+
end
46+
3447
sig do
3548
returns(T::Array[Package])
3649
end
@@ -90,15 +103,31 @@ def self.write_package_yml!(package)
90103
merged_config.merge!('metadata' => package.metadata)
91104
end
92105

93-
sorted_keys = key_sort_order
94-
merged_config = merged_config.to_a.sort_by { |key, _value| T.unsafe(sorted_keys).index(key) || 1000 }.to_h
106+
merged_config = sort_keys(merged_config, package.original_key_order)
95107

96108
raw_yaml = YAML.dump(merged_config)
97109
stylized_yaml = raw_yaml.gsub("---\n", '')
98110
file.write(stylized_yaml)
99111
end
100112
end
101113

114+
sig { params(config: T::Hash[T.untyped, T.untyped], original_key_order: T::Array[String]).returns(T::Hash[T.untyped, T.untyped]) }
115+
def self.sort_keys(config, original_key_order)
116+
if preserve_key_order && original_key_order.any?
117+
# Preserve original key order: existing keys stay in their original position,
118+
# new keys are appended in the default sort order
119+
existing_keys = original_key_order & config.keys
120+
new_keys = config.keys - original_key_order
121+
sorted_new_keys = new_keys.sort_by { |key| key_sort_order.index(key) || 1000 }
122+
123+
ordered_keys = existing_keys + sorted_new_keys
124+
ordered_keys.each_with_object({}) { |key, hash| hash[key] = config[key] if config.key?(key) }
125+
else
126+
# Default behavior: sort by canonical key order
127+
config.to_a.sort_by { |key, _value| T.unsafe(key_sort_order).index(key) || 1000 }.to_h
128+
end
129+
end
130+
102131
sig { returns(T::Array[String]) }
103132
def self.key_sort_order
104133
%w[

lib/parse_packwerk/package.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ class Package < T::Struct
1313
const :dependencies, T::Array[String]
1414
const :config, T::Hash[T.untyped, T.untyped]
1515
const :violations, T::Array[Violation]
16+
# Stores the original key order from the YAML file for preserving order on write
17+
const :original_key_order, T::Array[String], default: []
1618

1719
sig { params(pathname: Pathname).returns(Package) }
1820
def self.from(pathname)
1921
package_loaded_yml = YAML.load_file(pathname) || {}
2022
package_name = pathname.dirname.cleanpath.to_s
23+
# Capture the original key order from the YAML file
24+
original_keys = package_loaded_yml.is_a?(Hash) ? package_loaded_yml.keys : []
2125

2226
new(
2327
name: package_name,
@@ -28,7 +32,8 @@ def self.from(pathname)
2832
metadata: package_loaded_yml[METADATA] || {},
2933
dependencies: package_loaded_yml[DEPENDENCIES] || [],
3034
config: package_loaded_yml,
31-
violations: PackageTodo.from(PackageTodo.yml(directory(package_name))).violations
35+
violations: PackageTodo.from(PackageTodo.yml(directory(package_name))).violations,
36+
original_key_order: original_keys
3237
)
3338
end
3439

parse_packwerk.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Gem::Specification.new do |spec|
22
spec.name = 'parse_packwerk'
3-
spec.version = '0.26.1'
3+
spec.version = '0.27.0'
44
spec.authors = ['Gusto Engineers']
55
spec.email = ['dev@gusto.com']
66
spec.summary = 'A low-dependency gem for parsing and writing packwerk YML files'

spec/parse_packwerk_spec.rb

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,5 +1201,166 @@ def pack_as_hash(package)
12011201
expect(pack_as_hash(all_packages.first)).to eq pack_as_hash(package)
12021202
end
12031203
end
1204+
1205+
describe 'preserve_key_order option' do
1206+
after do
1207+
ParsePackwerk.preserve_key_order = false
1208+
end
1209+
1210+
context 'when preserve_key_order is false (default)' do
1211+
before do
1212+
ParsePackwerk.preserve_key_order = false
1213+
write_file(package_yml, <<~CONTENTS)
1214+
enforce_privacy: true
1215+
enforce_layers: true
1216+
layer: admin
1217+
enforce_dependencies: true
1218+
dependencies:
1219+
- packs/foo
1220+
CONTENTS
1221+
end
1222+
1223+
it 'reorders keys according to key_sort_order' do
1224+
package = ParsePackwerk::Package.from(package_yml)
1225+
ParsePackwerk.write_package_yml!(package)
1226+
1227+
expect(package_yml.read).to eq <<~PACKAGEYML
1228+
enforce_dependencies: true
1229+
enforce_privacy: true
1230+
enforce_layers: true
1231+
layer: admin
1232+
dependencies:
1233+
- packs/foo
1234+
PACKAGEYML
1235+
end
1236+
end
1237+
1238+
context 'when preserve_key_order is true' do
1239+
before do
1240+
ParsePackwerk.preserve_key_order = true
1241+
write_file(package_yml, <<~CONTENTS)
1242+
enforce_privacy: true
1243+
enforce_layers: true
1244+
layer: admin
1245+
enforce_dependencies: true
1246+
dependencies:
1247+
- packs/foo
1248+
CONTENTS
1249+
end
1250+
1251+
it 'preserves the original key order from the file' do
1252+
package = ParsePackwerk::Package.from(package_yml)
1253+
ParsePackwerk.write_package_yml!(package)
1254+
1255+
expect(package_yml.read).to eq <<~PACKAGEYML
1256+
enforce_privacy: true
1257+
enforce_layers: true
1258+
layer: admin
1259+
enforce_dependencies: true
1260+
dependencies:
1261+
- packs/foo
1262+
PACKAGEYML
1263+
end
1264+
1265+
it 'appends new keys in canonical order at the end' do
1266+
package = ParsePackwerk::Package.from(package_yml)
1267+
# Add metadata which wasn't in the original file
1268+
new_package = package.with(metadata: { 'owner' => 'Team A' })
1269+
ParsePackwerk.write_package_yml!(new_package)
1270+
1271+
expect(package_yml.read).to eq <<~PACKAGEYML
1272+
enforce_privacy: true
1273+
enforce_layers: true
1274+
layer: admin
1275+
enforce_dependencies: true
1276+
dependencies:
1277+
- packs/foo
1278+
metadata:
1279+
owner: Team A
1280+
PACKAGEYML
1281+
end
1282+
1283+
it 'handles removed keys gracefully' do
1284+
package = ParsePackwerk::Package.from(package_yml)
1285+
# Remove dependencies
1286+
new_package = package.with(dependencies: [])
1287+
ParsePackwerk.write_package_yml!(new_package)
1288+
1289+
expect(package_yml.read).to eq <<~PACKAGEYML
1290+
enforce_privacy: true
1291+
enforce_layers: true
1292+
layer: admin
1293+
enforce_dependencies: true
1294+
PACKAGEYML
1295+
end
1296+
end
1297+
1298+
context 'when preserve_key_order is true but package has no original_key_order' do
1299+
before do
1300+
ParsePackwerk.preserve_key_order = true
1301+
end
1302+
1303+
it 'falls back to canonical key order for new packages' do
1304+
package = build_pack(dependencies: ['packs/foo'])
1305+
ParsePackwerk.write_package_yml!(package)
1306+
1307+
expect(package_yml.read).to eq <<~PACKAGEYML
1308+
enforce_dependencies: true
1309+
enforce_privacy: true
1310+
enforce_layers: true
1311+
dependencies:
1312+
- packs/foo
1313+
PACKAGEYML
1314+
end
1315+
end
1316+
1317+
context 'when package is modified after reading' do
1318+
before do
1319+
ParsePackwerk.preserve_key_order = true
1320+
write_file(package_yml, <<~CONTENTS)
1321+
enforce_privacy: true
1322+
enforce_layers: true
1323+
layer: admin
1324+
enforce_dependencies: true
1325+
dependencies:
1326+
- packs/foo
1327+
CONTENTS
1328+
end
1329+
1330+
it 'preserves key order even when dependencies change' do
1331+
package = ParsePackwerk::Package.from(package_yml)
1332+
# Change dependencies value but keep the key
1333+
new_package = package.with(dependencies: ['packs/bar', 'packs/baz'])
1334+
ParsePackwerk.write_package_yml!(new_package)
1335+
1336+
expect(package_yml.read).to eq <<~PACKAGEYML
1337+
enforce_privacy: true
1338+
enforce_layers: true
1339+
layer: admin
1340+
enforce_dependencies: true
1341+
dependencies:
1342+
- packs/bar
1343+
- packs/baz
1344+
PACKAGEYML
1345+
end
1346+
end
1347+
1348+
context 'preserves original key order when reading and writing package' do
1349+
before do
1350+
ParsePackwerk.preserve_key_order = true
1351+
end
1352+
1353+
it 'stores original_key_order when reading package' do
1354+
write_file(package_yml, <<~CONTENTS)
1355+
enforce_privacy: true
1356+
layer: admin
1357+
enforce_dependencies: true
1358+
CONTENTS
1359+
1360+
package = ParsePackwerk::Package.from(package_yml)
1361+
expect(package.original_key_order).to eq %w[enforce_privacy layer enforce_dependencies]
1362+
end
1363+
end
1364+
end
12041365
end
12051366
end

0 commit comments

Comments
 (0)