ro 5.1.1 → 5.2.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ro/_lib.rb +1 -1
  3. data/lib/ro/collection.rb +47 -11
  4. data/lib/ro/migrator.rb +6 -4
  5. data/lib/ro/node.rb +15 -8
  6. data/ro.gemspec +78 -1
  7. data/test/integration/dual_structure_test.rb +232 -0
  8. data/test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/assets-only/assets/test.txt +1 -0
  9. data/test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/assets/body.md +5 -0
  10. data/test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/assets/image.jpg +2 -0
  11. data/test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/attributes.yml +2 -0
  12. data/test/tmp/migration_test_1760949758.backup.20251020084238/posts/assets-only/assets/test.txt +1 -0
  13. data/test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/assets/body.md +5 -0
  14. data/test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/assets/image.jpg +2 -0
  15. data/test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/attributes.yml +2 -0
  16. data/test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/assets-only/assets/test.txt +1 -0
  17. data/test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/assets/body.md +5 -0
  18. data/test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/assets/image.jpg +2 -0
  19. data/test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/attributes.yml +2 -0
  20. data/test/tmp/migration_test_1760949808.backup.20251020084328/posts/assets-only/assets/test.txt +1 -0
  21. data/test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/assets/body.md +5 -0
  22. data/test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/assets/image.jpg +2 -0
  23. data/test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/attributes.yml +2 -0
  24. data/test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/assets-only/assets/test.txt +1 -0
  25. data/test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/assets/body.md +5 -0
  26. data/test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/assets/image.jpg +2 -0
  27. data/test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/attributes.yml +2 -0
  28. data/test/tmp/migration_test_1760982056.backup.20251020174056/posts/assets-only/assets/test.txt +1 -0
  29. data/test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/assets/body.md +5 -0
  30. data/test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/assets/image.jpg +2 -0
  31. data/test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/attributes.yml +2 -0
  32. data/test/tmp/new_structure_test_1760949758/mixed/test-json.json +5 -0
  33. data/test/tmp/new_structure_test_1760949758/mixed/test-yaml.yml +3 -0
  34. data/test/tmp/new_structure_test_1760949758/posts/metadata-only.yml +7 -0
  35. data/test/tmp/new_structure_test_1760949758/posts/nested-test/assets/subdirectory/image.png +2 -0
  36. data/test/tmp/new_structure_test_1760949758/posts/nested-test.yml +7 -0
  37. data/test/tmp/new_structure_test_1760949758/posts/sample-post/assets/body.md +5 -0
  38. data/test/tmp/new_structure_test_1760949758/posts/sample-post/assets/image.jpg +2 -0
  39. data/test/tmp/new_structure_test_1760949758/posts/sample-post.yml +7 -0
  40. data/test/unit/collection_test.rb +13 -12
  41. metadata +35 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77bdeded7cbfb7b5bf8f1f855bcb7ebff9e09aa2fc06e6f506f4d8c01d62b51c
4
- data.tar.gz: 43202174de0fee59b7309b92ff29975b77f5a1b580a96659eda54ffa3670543a
3
+ metadata.gz: a38ecc039006ed4a514cf2e7ef7704b571911ab495100939f858abab5018ede2
4
+ data.tar.gz: 0de7ade28c2f781b79cb7320ef4a28cba44c1d7c5551281ec7352c88efdc8885
5
5
  SHA512:
6
- metadata.gz: 23b4449ae106a7ee06354831e5ee4e4a8882965daa4a4ea909121c235095df74e27a06f767effef0929c8032a34468b99adcb7d7e1673681141de738d5d4b9f7
7
- data.tar.gz: 5fe310459d13feaeb0a30600da8af8d2b184e21df58f22f63a7630f41d7f8e60e6b1b0588dc834961d21ed420959566c3654f620ca7902c368e48b7361f2eb87
6
+ metadata.gz: 50b5cb23987b64672b73a5e9437dd922b901c0708639ea145f70a417888cbd98cdccbfb20329d74b012223709c93b901f238189f864ebe59f305669cdcdfa186
7
+ data.tar.gz: 528110f18aeb57caff429b4d27f8fe18cab2f8e6043bd25a9f9e28ff2cc300fba280a2c4e8c4ed4acbd68b9334e4ff0ec73dad7c30b675dfb149459b54c06c2c
data/lib/ro/_lib.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Ro
2
- VERSION = '5.1.1' unless defined?(VERSION)
2
+ VERSION = '5.2.0' unless defined?(VERSION)
3
3
 
4
4
  class << self
5
5
  def version
data/lib/ro/collection.rb CHANGED
@@ -34,18 +34,39 @@ module Ro
34
34
  Node.new(path)
35
35
  end
36
36
 
37
- # T021: Scan for metadata files in new structure format
37
+ # T021: Scan for metadata files in both new and old structure formats
38
+ # Returns array of hashes: [{id: 'node-id', path: Path, type: :new|:old}]
38
39
  def metadata_files
39
40
  extensions = %w[yml yaml json toml]
40
- files = []
41
+ metadata_map = {}
41
42
 
43
+ # First, scan for new structure: collection-level metadata files (e.g., posts/ara.yml)
42
44
  extensions.each do |ext|
43
45
  @path.glob("*.#{ext}").each do |file|
44
- files << file if file.file?
46
+ next unless file.file?
47
+ node_id = file.basename.to_s.sub(/\.(yml|yaml|json|toml)$/, '')
48
+ metadata_map[node_id] ||= {id: node_id, path: file, type: :new}
45
49
  end
46
50
  end
47
51
 
48
- files.sort
52
+ # Second, scan for old structure: subdirectory attributes files (e.g., posts/ara/attributes.yml)
53
+ subdirectories.each do |subdir|
54
+ node_id = subdir.basename.to_s
55
+
56
+ # Check if this subdirectory has an attributes file
57
+ extensions.each do |ext|
58
+ attributes_file = subdir.join("attributes.#{ext}")
59
+ if attributes_file.exist? && attributes_file.file?
60
+ # Only use old structure if new structure doesn't exist
61
+ # This allows gradual migration - new structure takes precedence
62
+ metadata_map[node_id] ||= {id: node_id, path: attributes_file, type: :old}
63
+ break
64
+ end
65
+ end
66
+ end
67
+
68
+ # Return sorted array of metadata info
69
+ metadata_map.values.sort_by { |m| m[:id] }
49
70
  end
50
71
 
51
72
  def subdirectories(...)
@@ -56,28 +77,43 @@ module Ro
56
77
  @path.subdirectory_for(name)
57
78
  end
58
79
 
59
- # T020: Modified to discover nodes from metadata files (new structure)
80
+ # T020: Modified to discover nodes from metadata files (both new and old structure)
60
81
  def each(offset:nil, limit:nil, &block)
61
82
  # Return enumerator if no block given and no offset/limit
62
83
  return to_enum(:each, offset: offset, limit: limit) unless block_given?
63
84
 
64
- # Use metadata files for new structure instead of subdirectories
65
- files = metadata_files
85
+ # Get metadata from both old and new structure
86
+ metadata_entries = metadata_files
66
87
 
67
88
  if offset
68
89
  i = -1
69
90
  n = 0
70
- files.each do |metadata_file|
91
+ metadata_entries.each do |entry|
71
92
  i += 1
72
93
  next if i < offset
73
- node = Node.new(self, metadata_file)
94
+
95
+ # Create node based on structure type
96
+ node = if entry[:type] == :new
97
+ Node.new(self, entry[:path])
98
+ else
99
+ # Old structure: pass subdirectory path
100
+ Node.new(entry[:path].parent)
101
+ end
102
+
74
103
  block.call(node)
75
104
  n += 1
76
105
  break if limit && n >= limit
77
106
  end
78
107
  else
79
- files.each do |metadata_file|
80
- node = Node.new(self, metadata_file)
108
+ metadata_entries.each do |entry|
109
+ # Create node based on structure type
110
+ node = if entry[:type] == :new
111
+ Node.new(self, entry[:path])
112
+ else
113
+ # Old structure: pass subdirectory path
114
+ Node.new(entry[:path].parent)
115
+ end
116
+
81
117
  block.call(node)
82
118
  end
83
119
  end
data/lib/ro/migrator.rb CHANGED
@@ -45,12 +45,14 @@ module Ro
45
45
  result[:collections] << collection_name
46
46
 
47
47
  # Check for new structure (metadata files at collection level)
48
- collection.metadata_files.each do |metadata_file|
49
- node_id = metadata_file.basename.to_s.sub(/\.(yml|yaml|json|toml)$/, '')
48
+ collection.metadata_files.each do |entry|
49
+ # Only count new-structure nodes for migration purposes
50
+ next unless entry[:type] == :new
51
+
50
52
  result[:new_nodes] << {
51
53
  collection: collection_name,
52
- node_id: node_id,
53
- metadata_file: metadata_file
54
+ node_id: entry[:id],
55
+ metadata_file: entry[:path]
54
56
  }
55
57
  result[:has_new_structure] = true
56
58
  end
data/lib/ro/node.rb CHANGED
@@ -85,19 +85,26 @@ module Ro
85
85
 
86
86
  # T026: Modified to load from external metadata_file (new structure)
87
87
  def _load_base_attributes
88
+ # Start with collection-level metadata (new structure) if it exists
89
+ base_attrs = Map.new
90
+
88
91
  if @metadata_file && @metadata_file.exist?
89
92
  # New structure: load from explicit metadata file
90
93
  attrs = _render(@metadata_file)
91
- update_attributes!(attrs, file: @metadata_file)
92
- else
93
- # Old structure: search for attributes.yml in node directory
94
- glob = "attributes.{yml,yaml,json}"
94
+ base_attrs = Map.for(attrs)
95
+ end
95
96
 
96
- @path.glob(glob) do |file|
97
- attrs = _render(file)
98
- update_attributes!(attrs, file:)
99
- end
97
+ # Then merge in nested attributes.yml (old structure) if it exists
98
+ # "Deeper more specific wins" - nested attributes override collection-level
99
+ glob = "attributes.{yml,yaml,json}"
100
+ @path.glob(glob) do |file|
101
+ nested_attrs = _render(file)
102
+ # Use Map's smart merge: base.apply(override) means override wins
103
+ base_attrs = base_attrs.apply(Map.for(nested_attrs))
100
104
  end
105
+
106
+ # Update with the merged result
107
+ update_attributes!(base_attrs.to_hash, file: @metadata_file || @path) if base_attrs.any?
101
108
  end
102
109
 
103
110
  def _load_asset_attributes
data/ro.gemspec CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  Gem::Specification::new do |spec|
5
5
  spec.name = "ro"
6
- spec.version = "5.1.1"
6
+ spec.version = "5.2.0"
7
7
  spec.required_ruby_version = '>= 3.0'
8
8
  spec.platform = Gem::Platform::RUBY
9
9
  spec.summary = "all your content in github, as god intended"
@@ -269,6 +269,7 @@ Gem::Specification::new do |spec|
269
269
  "test/fixtures/old_structure/posts/sample-post/assets/image.jpg",
270
270
  "test/fixtures/old_structure/posts/sample-post/attributes.yml",
271
271
  "test/integration",
272
+ "test/integration/dual_structure_test.rb",
272
273
  "test/integration/ro_integration_test.rb",
273
274
  "test/test_helper.rb",
274
275
  "test/tmp/migration_test_1760746513.backup.20251018001513",
@@ -453,6 +454,66 @@ Gem::Specification::new do |spec|
453
454
  "test/tmp/migration_test_1760944190.backup.20251020070950/posts/sample-post/assets/body.md",
454
455
  "test/tmp/migration_test_1760944190.backup.20251020070950/posts/sample-post/assets/image.jpg",
455
456
  "test/tmp/migration_test_1760944190.backup.20251020070950/posts/sample-post/attributes.yml",
457
+ "test/tmp/migration_test_1760949758.backup.20251020084238",
458
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758",
459
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts",
460
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/assets-only",
461
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/assets-only/assets",
462
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/assets-only/assets/test.txt",
463
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post",
464
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/assets",
465
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/assets/body.md",
466
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/assets/image.jpg",
467
+ "test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/attributes.yml",
468
+ "test/tmp/migration_test_1760949758.backup.20251020084238/posts",
469
+ "test/tmp/migration_test_1760949758.backup.20251020084238/posts/assets-only",
470
+ "test/tmp/migration_test_1760949758.backup.20251020084238/posts/assets-only/assets",
471
+ "test/tmp/migration_test_1760949758.backup.20251020084238/posts/assets-only/assets/test.txt",
472
+ "test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post",
473
+ "test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/assets",
474
+ "test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/assets/body.md",
475
+ "test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/assets/image.jpg",
476
+ "test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/attributes.yml",
477
+ "test/tmp/migration_test_1760949808.backup.20251020084328",
478
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808",
479
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts",
480
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/assets-only",
481
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/assets-only/assets",
482
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/assets-only/assets/test.txt",
483
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post",
484
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/assets",
485
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/assets/body.md",
486
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/assets/image.jpg",
487
+ "test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/attributes.yml",
488
+ "test/tmp/migration_test_1760949808.backup.20251020084328/posts",
489
+ "test/tmp/migration_test_1760949808.backup.20251020084328/posts/assets-only",
490
+ "test/tmp/migration_test_1760949808.backup.20251020084328/posts/assets-only/assets",
491
+ "test/tmp/migration_test_1760949808.backup.20251020084328/posts/assets-only/assets/test.txt",
492
+ "test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post",
493
+ "test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/assets",
494
+ "test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/assets/body.md",
495
+ "test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/assets/image.jpg",
496
+ "test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/attributes.yml",
497
+ "test/tmp/migration_test_1760982056.backup.20251020174056",
498
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056",
499
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts",
500
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/assets-only",
501
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/assets-only/assets",
502
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/assets-only/assets/test.txt",
503
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post",
504
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/assets",
505
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/assets/body.md",
506
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/assets/image.jpg",
507
+ "test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/attributes.yml",
508
+ "test/tmp/migration_test_1760982056.backup.20251020174056/posts",
509
+ "test/tmp/migration_test_1760982056.backup.20251020174056/posts/assets-only",
510
+ "test/tmp/migration_test_1760982056.backup.20251020174056/posts/assets-only/assets",
511
+ "test/tmp/migration_test_1760982056.backup.20251020174056/posts/assets-only/assets/test.txt",
512
+ "test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post",
513
+ "test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/assets",
514
+ "test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/assets/body.md",
515
+ "test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/assets/image.jpg",
516
+ "test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/attributes.yml",
456
517
  "test/tmp/new_structure_test_1760746452",
457
518
  "test/tmp/new_structure_test_1760746452/mixed",
458
519
  "test/tmp/new_structure_test_1760746452/mixed/test-json.json",
@@ -467,6 +528,22 @@ Gem::Specification::new do |spec|
467
528
  "test/tmp/new_structure_test_1760746452/posts/sample-post.yml",
468
529
  "test/tmp/new_structure_test_1760746452/posts/sample-post/body.md",
469
530
  "test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg",
531
+ "test/tmp/new_structure_test_1760949758",
532
+ "test/tmp/new_structure_test_1760949758/mixed",
533
+ "test/tmp/new_structure_test_1760949758/mixed/test-json.json",
534
+ "test/tmp/new_structure_test_1760949758/mixed/test-yaml.yml",
535
+ "test/tmp/new_structure_test_1760949758/posts",
536
+ "test/tmp/new_structure_test_1760949758/posts/metadata-only.yml",
537
+ "test/tmp/new_structure_test_1760949758/posts/nested-test",
538
+ "test/tmp/new_structure_test_1760949758/posts/nested-test.yml",
539
+ "test/tmp/new_structure_test_1760949758/posts/nested-test/assets",
540
+ "test/tmp/new_structure_test_1760949758/posts/nested-test/assets/subdirectory",
541
+ "test/tmp/new_structure_test_1760949758/posts/nested-test/assets/subdirectory/image.png",
542
+ "test/tmp/new_structure_test_1760949758/posts/sample-post",
543
+ "test/tmp/new_structure_test_1760949758/posts/sample-post.yml",
544
+ "test/tmp/new_structure_test_1760949758/posts/sample-post/assets",
545
+ "test/tmp/new_structure_test_1760949758/posts/sample-post/assets/body.md",
546
+ "test/tmp/new_structure_test_1760949758/posts/sample-post/assets/image.jpg",
470
547
  "test/unit",
471
548
  "test/unit/asset_test.rb",
472
549
  "test/unit/collection_test.rb",
@@ -0,0 +1,232 @@
1
+ require_relative '../test_helper'
2
+
3
+ class DualStructureTest < RoTestCase
4
+ def setup
5
+ @test_dir = Pathname.new(Dir.mktmpdir("dual_structure_test"))
6
+ @ro_dir = @test_dir.join('ro')
7
+ @posts_dir = @ro_dir.join('posts')
8
+ @posts_dir.mkpath
9
+ end
10
+
11
+ def teardown
12
+ FileUtils.rm_rf(@test_dir) if @test_dir && @test_dir.exist?
13
+ end
14
+
15
+ def test_discovers_new_structure_nodes
16
+ # Create new structure: posts/foo.yml
17
+ File.write(@posts_dir.join('foo.yml'), {title: 'Foo New'}.to_yaml)
18
+ @posts_dir.join('foo').mkpath
19
+
20
+ root = Ro::Root.new(@ro_dir)
21
+ posts = root.posts
22
+
23
+ assert_equal 1, posts.to_a.size
24
+ assert_equal 'foo', posts.first.id
25
+ assert_equal 'Foo New', posts.first.attributes[:title]
26
+ end
27
+
28
+ def test_discovers_old_structure_nodes
29
+ # Create old structure: posts/bar/attributes.yml
30
+ bar_dir = @posts_dir.join('bar')
31
+ bar_dir.mkpath
32
+ File.write(bar_dir.join('attributes.yml'), {title: 'Bar Old'}.to_yaml)
33
+
34
+ root = Ro::Root.new(@ro_dir)
35
+ posts = root.posts
36
+
37
+ assert_equal 1, posts.to_a.size
38
+ assert_equal 'bar', posts.first.id
39
+ assert_equal 'Bar Old', posts.first.attributes[:title]
40
+ end
41
+
42
+ def test_discovers_both_structure_types_simultaneously
43
+ # Create new structure node
44
+ File.write(@posts_dir.join('new-node.yml'), {title: 'New Structure'}.to_yaml)
45
+ @posts_dir.join('new-node').mkpath
46
+
47
+ # Create old structure node
48
+ old_dir = @posts_dir.join('old-node')
49
+ old_dir.mkpath
50
+ File.write(old_dir.join('attributes.yml'), {title: 'Old Structure'}.to_yaml)
51
+
52
+ root = Ro::Root.new(@ro_dir)
53
+ posts = root.posts
54
+ nodes = posts.to_a
55
+
56
+ assert_equal 2, nodes.size
57
+
58
+ new_node = nodes.find { |n| n.id == 'new-node' }
59
+ old_node = nodes.find { |n| n.id == 'old-node' }
60
+
61
+ assert_equal 'New Structure', new_node.attributes[:title]
62
+ assert_equal 'Old Structure', old_node.attributes[:title]
63
+ end
64
+
65
+ def test_new_structure_takes_precedence_over_old
66
+ # Create both structures for same node - new should win
67
+ node_id = 'conflict'
68
+
69
+ # New structure
70
+ File.write(@posts_dir.join("#{node_id}.yml"), {title: 'New Wins'}.to_yaml)
71
+
72
+ # Old structure
73
+ node_dir = @posts_dir.join(node_id)
74
+ node_dir.mkpath
75
+ File.write(node_dir.join('attributes.yml'), {title: 'Old Loses'}.to_yaml)
76
+
77
+ root = Ro::Root.new(@ro_dir)
78
+ posts = root.posts
79
+ node = posts.to_a.first
80
+
81
+ assert_equal 'conflict', node.id
82
+ assert_equal 'New Wins', node.attributes[:title]
83
+ end
84
+
85
+ def test_mixed_structure_with_assets
86
+ # New structure with assets
87
+ File.write(@posts_dir.join('new-with-assets.yml'), {title: 'New'}.to_yaml)
88
+ new_assets_dir = @posts_dir.join('new-with-assets', 'assets')
89
+ new_assets_dir.mkpath
90
+ File.write(new_assets_dir.join('image.jpg'), 'fake jpg')
91
+
92
+ # Old structure with assets
93
+ old_dir = @posts_dir.join('old-with-assets')
94
+ old_dir.mkpath
95
+ File.write(old_dir.join('attributes.yml'), {title: 'Old'}.to_yaml)
96
+ old_assets_dir = old_dir.join('assets')
97
+ old_assets_dir.mkpath
98
+ File.write(old_assets_dir.join('photo.png'), 'fake png')
99
+
100
+ root = Ro::Root.new(@ro_dir)
101
+ posts = root.posts
102
+ nodes = posts.to_a
103
+
104
+ assert_equal 2, nodes.size
105
+
106
+ new_node = nodes.find { |n| n.id == 'new-with-assets' }
107
+ old_node = nodes.find { |n| n.id == 'old-with-assets' }
108
+
109
+ # Both should have assets
110
+ assert_equal 1, new_node.assets.size
111
+ assert new_node.assets.first.to_s.include?('image.jpg')
112
+
113
+ assert_equal 1, old_node.assets.size
114
+ assert old_node.assets.first.to_s.include?('photo.png')
115
+ end
116
+
117
+ def test_iteration_order_consistent
118
+ # Create nodes with both structures
119
+ File.write(@posts_dir.join('a-new.yml'), {}.to_yaml)
120
+ @posts_dir.join('a-new').mkpath
121
+
122
+ b_dir = @posts_dir.join('b-old')
123
+ b_dir.mkpath
124
+ File.write(b_dir.join('attributes.yml'), {}.to_yaml)
125
+
126
+ File.write(@posts_dir.join('c-new.yml'), {}.to_yaml)
127
+ @posts_dir.join('c-new').mkpath
128
+
129
+ root = Ro::Root.new(@ro_dir)
130
+ posts = root.posts
131
+ ids = posts.to_a.map(&:id)
132
+
133
+ # Should be sorted alphabetically regardless of structure type
134
+ assert_equal ['a-new', 'b-old', 'c-new'], ids
135
+ end
136
+
137
+ def test_supports_multiple_metadata_formats
138
+ # New structure with different formats
139
+ File.write(@posts_dir.join('yaml-new.yml'), {format: 'yaml'}.to_yaml)
140
+ @posts_dir.join('yaml-new').mkpath
141
+
142
+ File.write(@posts_dir.join('json-new.json'), {format: 'json'}.to_json)
143
+ @posts_dir.join('json-new').mkpath
144
+
145
+ # Old structure with different formats
146
+ yaml_old_dir = @posts_dir.join('yaml-old')
147
+ yaml_old_dir.mkpath
148
+ File.write(yaml_old_dir.join('attributes.yaml'), {format: 'yaml_old'}.to_yaml)
149
+
150
+ json_old_dir = @posts_dir.join('json-old')
151
+ json_old_dir.mkpath
152
+ File.write(json_old_dir.join('attributes.json'), {format: 'json_old'}.to_json)
153
+
154
+ root = Ro::Root.new(@ro_dir)
155
+ posts = root.posts
156
+ nodes = posts.to_a
157
+
158
+ assert_equal 4, nodes.size
159
+
160
+ formats = nodes.map { |n| n.attributes[:format] }.sort
161
+ assert_equal ['json', 'json_old', 'yaml', 'yaml_old'], formats
162
+ end
163
+
164
+ def test_merges_both_metadata_files_when_both_exist
165
+ # Create collection-level metadata
166
+ File.write(@posts_dir.join('merged.yml'), {
167
+ title: 'Collection Level',
168
+ author: 'Collection Author',
169
+ tags: ['collection']
170
+ }.to_yaml)
171
+
172
+ # Create nested attributes - should override collection level
173
+ merged_dir = @posts_dir.join('merged')
174
+ merged_dir.mkpath
175
+ File.write(merged_dir.join('attributes.yml'), {
176
+ title: 'Nested Wins', # Override
177
+ published: true, # New field
178
+ tags: ['nested', 'specific'] # Override
179
+ }.to_yaml)
180
+
181
+ root = Ro::Root.new(@ro_dir)
182
+ posts = root.posts
183
+ node = posts.to_a.first
184
+
185
+ # Nested (deeper, more specific) should win
186
+ assert_equal 'Nested Wins', node.attributes[:title]
187
+ assert_equal true, node.attributes[:published]
188
+ assert_equal ['nested', 'specific'], node.attributes[:tags]
189
+
190
+ # Collection-level field that wasn't overridden should still be there
191
+ assert_equal 'Collection Author', node.attributes[:author]
192
+ end
193
+
194
+ def test_deep_merge_with_nested_hashes
195
+ # Collection-level with nested structure
196
+ File.write(@posts_dir.join('deep.yml'), {
197
+ meta: {
198
+ created: '2024-01-01',
199
+ source: 'collection'
200
+ },
201
+ settings: {
202
+ public: true,
203
+ featured: false
204
+ }
205
+ }.to_yaml)
206
+
207
+ # Nested attributes with partial override
208
+ deep_dir = @posts_dir.join('deep')
209
+ deep_dir.mkpath
210
+ File.write(deep_dir.join('attributes.yml'), {
211
+ meta: {
212
+ updated: '2024-02-01',
213
+ source: 'nested' # Override
214
+ },
215
+ settings: {
216
+ featured: true # Override just this field
217
+ }
218
+ }.to_yaml)
219
+
220
+ root = Ro::Root.new(@ro_dir)
221
+ posts = root.posts
222
+ node = posts.to_a.first
223
+
224
+ # Deep merge should preserve non-conflicting values
225
+ assert_equal '2024-01-01', node.attributes[:meta][:created]
226
+ assert_equal '2024-02-01', node.attributes[:meta][:updated]
227
+ assert_equal 'nested', node.attributes[:meta][:source] # Nested wins
228
+
229
+ assert_equal true, node.attributes[:settings][:public]
230
+ assert_equal true, node.attributes[:settings][:featured] # Nested wins
231
+ end
232
+ end
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ {
2
+ "title": "JSON Format Test",
3
+ "format": "json",
4
+ "extension": "json"
5
+ }
@@ -0,0 +1,3 @@
1
+ title: "YAML Format Test"
2
+ format: "yaml"
3
+ extension: "yml"
@@ -0,0 +1,7 @@
1
+ title: "Metadata Only Node"
2
+ author: "Test Author"
3
+ published_at: "2025-01-02"
4
+ tags:
5
+ - test
6
+ - metadata-only
7
+ description: "This node has no asset directory - testing FR-007"
@@ -0,0 +1,2 @@
1
+ FAKE_PNG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file in a nested subdirectory.
@@ -0,0 +1,7 @@
1
+ title: "Nested Assets Test"
2
+ author: "Test Author"
3
+ published_at: "2025-01-03"
4
+ tags:
5
+ - test
6
+ - nested-assets
7
+ description: "This node has nested subdirectories with assets"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the new structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the new structure (no assets/ subdirectory).
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (New Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - new-structure
7
+ description: "This is a test fixture in the new structure format"
@@ -11,31 +11,32 @@ class CollectionTest < RoTestCase
11
11
 
12
12
  # T011: Test Collection#metadata_files
13
13
  def test_metadata_files_returns_yml_and_json_files
14
- files = @collection.metadata_files
14
+ entries = @collection.metadata_files
15
15
 
16
- assert_not_nil files, "metadata_files should not be nil"
17
- assert files.is_a?(Array), "metadata_files should return an Array"
16
+ assert_not_nil entries, "metadata_files should not be nil"
17
+ assert entries.is_a?(Array), "metadata_files should return an Array"
18
18
 
19
19
  # Should find .yml and .json files
20
- yml_files = files.select { |f| f.to_s.end_with?('.yml') }
21
- assert yml_files.any?, "Should find at least one .yml file"
20
+ yml_entries = entries.select { |e| e[:path].to_s.end_with?('.yml') }
21
+ assert yml_entries.any?, "Should find at least one .yml file"
22
22
 
23
- # Should be Pathname or Ro::Path objects
24
- assert files.all? { |f| f.is_a?(Pathname) || f.is_a?(Ro::Path) }, "All files should be Pathname or Ro::Path objects"
23
+ # Each entry should be a hash with :id, :path, :type
24
+ assert entries.all? { |e| e.is_a?(Hash) && e[:id] && e[:path] && e[:type] }, "All entries should be hashes with :id, :path, :type"
25
25
  end
26
26
 
27
27
  def test_metadata_files_excludes_directories
28
- files = @collection.metadata_files
28
+ entries = @collection.metadata_files
29
29
 
30
30
  # Should only return files, not directories
31
- assert files.all?(&:file?), "metadata_files should only return files, not directories"
31
+ assert entries.all? { |e| e[:path].file? }, "metadata_files should only return file entries"
32
32
  end
33
33
 
34
34
  def test_metadata_files_sorted
35
- files = @collection.metadata_files
35
+ entries = @collection.metadata_files
36
36
 
37
- # Should be sorted
38
- assert_equal files.sort.map(&:to_s), files.map(&:to_s), "metadata_files should be sorted"
37
+ # Should be sorted by id
38
+ ids = entries.map { |e| e[:id] }
39
+ assert_equal ids.sort, ids, "metadata_files should be sorted by id"
39
40
  end
40
41
 
41
42
  # T012: Test Collection#each with new structure
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ro
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.1
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ara T. Howard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-20 00:00:00.000000000 Z
11
+ date: 2025-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: map
@@ -329,6 +329,7 @@ files:
329
329
  - test/fixtures/old_structure/posts/sample-post/assets/body.md
330
330
  - test/fixtures/old_structure/posts/sample-post/assets/image.jpg
331
331
  - test/fixtures/old_structure/posts/sample-post/attributes.yml
332
+ - test/integration/dual_structure_test.rb
332
333
  - test/integration/ro_integration_test.rb
333
334
  - test/test_helper.rb
334
335
  - test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg
@@ -407,6 +408,30 @@ files:
407
408
  - test/tmp/migration_test_1760944190.backup.20251020070950/posts/sample-post/assets/body.md
408
409
  - test/tmp/migration_test_1760944190.backup.20251020070950/posts/sample-post/assets/image.jpg
409
410
  - test/tmp/migration_test_1760944190.backup.20251020070950/posts/sample-post/attributes.yml
411
+ - test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/assets-only/assets/test.txt
412
+ - test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/assets/body.md
413
+ - test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/assets/image.jpg
414
+ - test/tmp/migration_test_1760949758.backup.20251020084238/migration_test_1760949758/posts/sample-post/attributes.yml
415
+ - test/tmp/migration_test_1760949758.backup.20251020084238/posts/assets-only/assets/test.txt
416
+ - test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/assets/body.md
417
+ - test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/assets/image.jpg
418
+ - test/tmp/migration_test_1760949758.backup.20251020084238/posts/sample-post/attributes.yml
419
+ - test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/assets-only/assets/test.txt
420
+ - test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/assets/body.md
421
+ - test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/assets/image.jpg
422
+ - test/tmp/migration_test_1760949808.backup.20251020084328/migration_test_1760949808/posts/sample-post/attributes.yml
423
+ - test/tmp/migration_test_1760949808.backup.20251020084328/posts/assets-only/assets/test.txt
424
+ - test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/assets/body.md
425
+ - test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/assets/image.jpg
426
+ - test/tmp/migration_test_1760949808.backup.20251020084328/posts/sample-post/attributes.yml
427
+ - test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/assets-only/assets/test.txt
428
+ - test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/assets/body.md
429
+ - test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/assets/image.jpg
430
+ - test/tmp/migration_test_1760982056.backup.20251020174056/migration_test_1760982056/posts/sample-post/attributes.yml
431
+ - test/tmp/migration_test_1760982056.backup.20251020174056/posts/assets-only/assets/test.txt
432
+ - test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/assets/body.md
433
+ - test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/assets/image.jpg
434
+ - test/tmp/migration_test_1760982056.backup.20251020174056/posts/sample-post/attributes.yml
410
435
  - test/tmp/new_structure_test_1760746452/mixed/test-json.json
411
436
  - test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml
412
437
  - test/tmp/new_structure_test_1760746452/posts/metadata-only.yml
@@ -415,6 +440,14 @@ files:
415
440
  - test/tmp/new_structure_test_1760746452/posts/sample-post.yml
416
441
  - test/tmp/new_structure_test_1760746452/posts/sample-post/body.md
417
442
  - test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg
443
+ - test/tmp/new_structure_test_1760949758/mixed/test-json.json
444
+ - test/tmp/new_structure_test_1760949758/mixed/test-yaml.yml
445
+ - test/tmp/new_structure_test_1760949758/posts/metadata-only.yml
446
+ - test/tmp/new_structure_test_1760949758/posts/nested-test.yml
447
+ - test/tmp/new_structure_test_1760949758/posts/nested-test/assets/subdirectory/image.png
448
+ - test/tmp/new_structure_test_1760949758/posts/sample-post.yml
449
+ - test/tmp/new_structure_test_1760949758/posts/sample-post/assets/body.md
450
+ - test/tmp/new_structure_test_1760949758/posts/sample-post/assets/image.jpg
418
451
  - test/unit/asset_test.rb
419
452
  - test/unit/collection_test.rb
420
453
  - test/unit/migrator_test.rb