ro 4.2.0 → 5.0.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +57 -10
- data/LICENSE +1 -1
- data/MIGRATION.md +320 -0
- data/README.md +286 -111
- data/Rakefile +2 -2
- data/a.yml +60 -0
- data/bin/ro +10 -0
- data/lib/ro/_lib.rb +18 -6
- data/lib/ro/asset.rb +67 -16
- data/lib/ro/collection.rb +91 -10
- data/lib/ro/config.rb +4 -0
- data/lib/ro/error.rb +5 -2
- data/lib/ro/html.rb +23 -0
- data/lib/ro/html_safe.rb +143 -0
- data/lib/ro/methods.rb +95 -38
- data/lib/ro/migrator.rb +285 -0
- data/lib/ro/node.rb +128 -45
- data/lib/ro/path.rb +4 -0
- data/lib/ro/root.rb +75 -1
- data/lib/ro/script/migrate.rb +204 -0
- data/lib/ro/script/server.rb +1 -1
- data/lib/ro/template.rb +62 -22
- data/lib/ro/text.rb +120 -0
- data/lib/ro.rb +5 -0
- data/public/api/ro/index-1.json +997 -79
- data/public/api/ro/index.json +997 -79
- data/public/api/ro/nerd/fastest-possible-embeddings/index.json +90 -0
- data/public/api/ro/nerd/ima/index.json +49 -0
- data/public/api/ro/nerd/index/index.json +74 -0
- data/public/api/ro/nerd/index-1.json +204 -0
- data/public/api/ro/nerd/index.json +194 -0
- data/public/api/ro/pages/about/index.json +60 -0
- data/public/api/ro/pages/contact/index.json +50 -0
- data/public/api/ro/pages/cv/index.json +49 -0
- data/public/api/ro/pages/disco/index.json +117 -0
- data/public/api/ro/pages/index/index.json +30 -0
- data/public/api/ro/pages/index-1.json +366 -0
- data/public/api/ro/pages/index.json +356 -0
- data/public/api/ro/pages/jess/index.json +62 -0
- data/public/api/ro/pages/now/index.json +43 -0
- data/public/api/ro/posts/almost-died-in-an-ice-cave/index.json +265 -0
- data/public/api/ro/posts/facebook-and-global-extremism/index.json +90 -0
- data/public/api/ro/posts/index-1.json +461 -79
- data/public/api/ro/posts/index.json +461 -79
- data/public/api/ro/posts/lemmings-considered-harmful/index.json +49 -0
- data/public/api/ro/posts/lost-in-the-desert/index.json +49 -0
- data/public/api/ro/posts/mission/index.json +49 -0
- data/public/api/ro/posts/return-your-laptop/index.json +61 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/giraffe.jpeg +0 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/let-me-in.jpg +0 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.js +70 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.rs +68 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/terminal.jpg +0 -0
- data/public/ro/nerd/fastest-possible-embeddings/body.md +266 -0
- data/public/ro/nerd/fastest-possible-embeddings.yml +7 -0
- data/public/ro/nerd/ima/assets/og.jpeg +0 -0
- data/public/ro/nerd/ima/body.md +22 -0
- data/public/ro/nerd/ima.yml +8 -0
- data/public/ro/nerd/index/assets/giraffe.jpeg +0 -0
- data/public/ro/nerd/index/assets/let-me-in.jpg +0 -0
- data/public/ro/nerd/index/assets/terminal.jpg +0 -0
- data/public/ro/nerd/index/body.md +130 -0
- data/public/ro/nerd/index.yml +7 -0
- data/public/ro/pages/about/assets/og.jpeg +0 -0
- data/public/ro/pages/about/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/about/body.md +40 -0
- data/public/ro/pages/contact/assets/giraffe.jpeg +0 -0
- data/public/ro/pages/contact/body.md +9 -0
- data/public/ro/pages/contact.yml +7 -0
- data/public/ro/pages/cv/assets/ara.jpg +0 -0
- data/public/ro/pages/cv/body.md +122 -0
- data/public/ro/pages/cv.yml +6 -0
- data/public/ro/pages/disco/assets/disco.jpg +0 -0
- data/public/ro/pages/disco/assets/disco.png +0 -0
- data/public/ro/pages/disco/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/disco/assets/src/environment.md +2354 -0
- data/public/ro/pages/disco/assets/src/fortune-500.md +2518 -0
- data/public/ro/pages/disco/assets/src/greed.md +2703 -0
- data/public/ro/pages/disco/assets/src/up-at-night.md +2337 -0
- data/public/ro/pages/disco/body.md +99 -0
- data/public/ro/pages/disco/samples/environment.md +2354 -0
- data/public/ro/pages/disco/samples/fortune-500.md +2518 -0
- data/public/ro/pages/disco/samples/greed.md +2703 -0
- data/public/ro/pages/disco/samples/up-at-night.md +2337 -0
- data/public/ro/pages/disco.yml +9 -0
- data/public/ro/pages/index/body.md +15 -0
- data/public/ro/pages/index.yml +1 -0
- data/public/ro/pages/jess/assets/og.jpg +0 -0
- data/public/ro/pages/jess/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/jess/body.md +3 -0
- data/public/ro/pages/jess.yml +7 -0
- data/public/ro/pages/now/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/now/body.md +24 -0
- data/public/ro/pages/now.yml +1 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image1.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image10.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image11.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image12.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image13.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image14.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image15.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image2.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image3.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image4.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image5.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image6.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image7.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image8.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image9.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/josh-pointing.jpg +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/levi-rawr.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/og.jpg +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/purple-heart.jpg +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/body.md +419 -0
- data/public/ro/posts/almost-died-in-an-ice-cave.yml +6 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/background.html +125 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/background.md +95 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/og.jpg +0 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/prompt.txt +122 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/results.md +183 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/survey.txt +190 -0
- data/public/ro/posts/facebook-and-global-extremism/body.md +393 -0
- data/public/ro/posts/facebook-and-global-extremism.yml +7 -0
- data/public/ro/posts/lemmings-considered-harmful/assets/lemming.jpeg +0 -0
- data/public/ro/posts/lemmings-considered-harmful/body.md +43 -0
- data/public/ro/posts/lemmings-considered-harmful.yml +6 -0
- data/public/ro/posts/lost-in-the-desert/assets/og.jpg +0 -0
- data/public/ro/posts/lost-in-the-desert/body.md +7 -0
- data/public/ro/posts/lost-in-the-desert.yml +6 -0
- data/public/ro/posts/mission/assets/og.jpg +0 -0
- data/public/ro/posts/mission/body.md +4 -0
- data/public/ro/posts/mission.yml +6 -0
- data/public/ro/posts/return-your-laptop/assets/og.jpg +0 -0
- data/public/ro/posts/return-your-laptop/assets/return-your-laptop.png +0 -0
- data/public/ro/posts/return-your-laptop/body.md +58 -0
- data/public/ro/posts/return-your-laptop.yml +6 -0
- data/ro.gemspec +369 -49
- data/scripts/speedtest.rb +324 -0
- data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
- data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
- data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
- data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
- data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
- data/specs/001-simplify-asset-structure/data-model.md +381 -0
- data/specs/001-simplify-asset-structure/plan.md +90 -0
- data/specs/001-simplify-asset-structure/quickstart.md +575 -0
- data/specs/001-simplify-asset-structure/research.md +333 -0
- data/specs/001-simplify-asset-structure/spec.md +127 -0
- data/specs/001-simplify-asset-structure/tasks.md +349 -0
- data/test/fixtures/new_structure/mixed/test-json.json +5 -0
- data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
- data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
- data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
- data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
- data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
- data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
- data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
- data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
- data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
- data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
- data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
- data/test/integration/ro_integration_test.rb +165 -0
- data/test/test_helper.rb +149 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
- data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
- data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
- data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
- data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
- data/test/unit/asset_test.rb +90 -0
- data/test/unit/collection_test.rb +127 -0
- data/test/unit/migrator_test.rb +209 -0
- data/test/unit/node_test.rb +138 -0
- data/tmp/gem-details.oe +0 -0
- metadata +250 -33
- data/public/api/ro/posts/first_post/index.json +0 -52
- data/public/api/ro/posts/second_post/index.json +0 -51
- data/public/api/ro/posts/third_post/index.json +0 -51
- data/public/ro/posts/first_post/assets/foo/bar/baz.jpg +0 -0
- data/public/ro/posts/first_post/assets/foo.jpg +0 -0
- data/public/ro/posts/first_post/assets/src/foo/bar.rb +0 -3
- data/public/ro/posts/first_post/attributes.yml +0 -2
- data/public/ro/posts/first_post/blurb.erb.md +0 -7
- data/public/ro/posts/first_post/body.md +0 -16
- data/public/ro/posts/first_post/testing.txt +0 -3
- data/public/ro/posts/second_post/assets/foo/bar/baz.jpg +0 -0
- data/public/ro/posts/second_post/assets/foo.jpg +0 -0
- data/public/ro/posts/second_post/assets/src/foo/bar.rb +0 -3
- data/public/ro/posts/second_post/attributes.yml +0 -2
- data/public/ro/posts/second_post/blurb.erb.md +0 -5
- data/public/ro/posts/second_post/body.md +0 -16
- data/public/ro/posts/third_post/assets/foo/bar/baz.jpg +0 -0
- data/public/ro/posts/third_post/assets/foo.jpg +0 -0
- data/public/ro/posts/third_post/assets/src/foo/bar.rb +0 -3
- data/public/ro/posts/third_post/attributes.yml +0 -2
- data/public/ro/posts/third_post/blurb.erb.md +0 -5
- data/public/ro/posts/third_post/body.md +0 -16
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# Contract: Collection API
|
|
2
|
+
|
|
3
|
+
**Version**: 5.0.0 (new structure)
|
|
4
|
+
**Date**: 2025-10-17
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
Defines the programmatic interface for the `Ro::Collection` class in the new simplified asset structure. Collections discover and manage Nodes using the new metadata file-based pattern.
|
|
9
|
+
|
|
10
|
+
## Constructor
|
|
11
|
+
|
|
12
|
+
### `Collection.new(root, name)`
|
|
13
|
+
|
|
14
|
+
**Purpose**: Initialize a collection from a root and collection name.
|
|
15
|
+
|
|
16
|
+
**Parameters**:
|
|
17
|
+
- `root` (Ro::Root): Parent root object
|
|
18
|
+
- `name` (String): Collection name (matches directory name)
|
|
19
|
+
|
|
20
|
+
**Returns**: `Ro::Collection` instance
|
|
21
|
+
|
|
22
|
+
**Example**:
|
|
23
|
+
```ruby
|
|
24
|
+
root = Ro::Root.new('/path/to/ro')
|
|
25
|
+
collection = Ro::Collection.new(root, 'posts')
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Unchanged**: Constructor signature remains the same in both v4.x and v5.0.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Instance Methods
|
|
33
|
+
|
|
34
|
+
### `#name` → String
|
|
35
|
+
|
|
36
|
+
**Purpose**: Returns the collection name.
|
|
37
|
+
|
|
38
|
+
**Returns**: String (e.g., "posts")
|
|
39
|
+
|
|
40
|
+
**Example**:
|
|
41
|
+
```ruby
|
|
42
|
+
collection.name # => "posts"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Unchanged**: Same in both structures.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### `#path` → Pathname
|
|
50
|
+
|
|
51
|
+
**Purpose**: Returns the path to the collection directory.
|
|
52
|
+
|
|
53
|
+
**Returns**: Pathname
|
|
54
|
+
|
|
55
|
+
**Example**:
|
|
56
|
+
```ruby
|
|
57
|
+
collection.path # => #<Pathname:/path/to/ro/posts>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Unchanged**: Same in both structures.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### `#nodes` → Array<Ro::Node>
|
|
65
|
+
|
|
66
|
+
**Purpose**: Returns all nodes in the collection.
|
|
67
|
+
|
|
68
|
+
**Returns**: Array of `Ro::Node` instances
|
|
69
|
+
|
|
70
|
+
**Example**:
|
|
71
|
+
```ruby
|
|
72
|
+
collection.nodes # => [#<Ro::Node id="post-1">, #<Ro::Node id="post-2">]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Behavior Change**:
|
|
76
|
+
- Old structure: Discovers nodes by iterating subdirectories
|
|
77
|
+
- New structure: Discovers nodes by finding metadata files (`.yml`, `.yaml`, `.json`, `.toml`)
|
|
78
|
+
|
|
79
|
+
**Unchanged**: Return type and usage remain the same.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### `#each(&block)` → Enumerator
|
|
84
|
+
|
|
85
|
+
**Purpose**: Iterates over each node in the collection.
|
|
86
|
+
|
|
87
|
+
**Parameters**:
|
|
88
|
+
- `block` (optional): Block to execute for each node
|
|
89
|
+
|
|
90
|
+
**Returns**: Enumerator if no block given
|
|
91
|
+
|
|
92
|
+
**Example**:
|
|
93
|
+
```ruby
|
|
94
|
+
collection.each do |node|
|
|
95
|
+
puts node.id
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Or without block:
|
|
99
|
+
collection.each.map(&:id) # => ["post-1", "post-2"]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Behavior Change**:
|
|
103
|
+
- Old structure: Iterates subdirectories, creates Node from each
|
|
104
|
+
- New structure: Iterates metadata files, creates Node from each
|
|
105
|
+
|
|
106
|
+
**Unchanged**: API remains the same, only discovery mechanism changes.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### `#node_for(identifier)` → Ro::Node | nil
|
|
111
|
+
|
|
112
|
+
**Purpose**: Returns a specific node by identifier.
|
|
113
|
+
|
|
114
|
+
**Parameters**:
|
|
115
|
+
- `identifier` (String): Node ID
|
|
116
|
+
|
|
117
|
+
**Returns**: `Ro::Node` instance, or `nil` if not found
|
|
118
|
+
|
|
119
|
+
**Example**:
|
|
120
|
+
```ruby
|
|
121
|
+
node = collection.node_for('my-post') # => #<Ro::Node id="my-post">
|
|
122
|
+
missing = collection.node_for('nonexistent') # => nil
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Behavior Change**:
|
|
126
|
+
- Old structure: Looks for subdirectory named `identifier`
|
|
127
|
+
- New structure: Looks for metadata file named `identifier.{yml,yaml,json,toml}`
|
|
128
|
+
|
|
129
|
+
**Unchanged**: API remains the same.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### `#[]` (alias: `#get`) → Ro::Node | nil
|
|
134
|
+
|
|
135
|
+
**Purpose**: Access a specific node by identifier (alias for `#node_for`).
|
|
136
|
+
|
|
137
|
+
**Parameters**:
|
|
138
|
+
- `identifier` (String): Node ID
|
|
139
|
+
|
|
140
|
+
**Returns**: `Ro::Node` instance, or `nil` if not found
|
|
141
|
+
|
|
142
|
+
**Example**:
|
|
143
|
+
```ruby
|
|
144
|
+
node = collection['my-post'] # => #<Ro::Node id="my-post">
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Unchanged**: Same in both structures.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### `#size` (alias: `#count`, `#length`) → Integer
|
|
152
|
+
|
|
153
|
+
**Purpose**: Returns the number of nodes in the collection.
|
|
154
|
+
|
|
155
|
+
**Returns**: Integer
|
|
156
|
+
|
|
157
|
+
**Example**:
|
|
158
|
+
```ruby
|
|
159
|
+
collection.size # => 42
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Unchanged**: Same in both structures (just counts discovered nodes).
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Discovery Logic (Internal)
|
|
167
|
+
|
|
168
|
+
### OLD Structure Discovery (v4.x):
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
def each(&block)
|
|
172
|
+
subdirectories.each do |subdir|
|
|
173
|
+
node = Ro::Node.new(self, subdir)
|
|
174
|
+
block.call(node)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def subdirectories
|
|
179
|
+
path.children.select(&:directory?).sort
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Pattern**: Iterate directories → each directory is a node → node loads `attributes.yml` internally
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### NEW Structure Discovery (v5.0):
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
def each(&block)
|
|
191
|
+
metadata_files.each do |metadata_file|
|
|
192
|
+
node = Ro::Node.new(self, metadata_file)
|
|
193
|
+
block.call(node)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def metadata_files
|
|
198
|
+
extensions = %w[yml yaml json toml]
|
|
199
|
+
extensions.flat_map do |ext|
|
|
200
|
+
path.glob("*.#{ext}").select(&:file?)
|
|
201
|
+
end.sort
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Pattern**: Scan for metadata files → each file is a node → node derives ID from filename
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Test Requirements
|
|
210
|
+
|
|
211
|
+
### Unit Tests
|
|
212
|
+
|
|
213
|
+
Must verify for the NEW structure:
|
|
214
|
+
|
|
215
|
+
1. **Initialization**:
|
|
216
|
+
- ✓ Creates collection from root and name
|
|
217
|
+
- ✓ Sets correct path (`root.path / name`)
|
|
218
|
+
|
|
219
|
+
2. **Node Discovery**:
|
|
220
|
+
- ✓ Finds nodes by detecting metadata files
|
|
221
|
+
- ✓ Supports multiple metadata formats (`.yml`, `.yaml`, `.json`, `.toml`)
|
|
222
|
+
- ✓ Ignores non-metadata files
|
|
223
|
+
- ✓ Returns nodes in sorted order (by filename)
|
|
224
|
+
- ✓ Handles empty collection (no metadata files)
|
|
225
|
+
|
|
226
|
+
3. **Node Access**:
|
|
227
|
+
- ✓ `#node_for` returns correct node by ID
|
|
228
|
+
- ✓ `#node_for` returns `nil` for missing nodes
|
|
229
|
+
- ✓ `#[]` works as alias for `#node_for`
|
|
230
|
+
|
|
231
|
+
4. **Enumeration**:
|
|
232
|
+
- ✓ `#each` iterates over all nodes
|
|
233
|
+
- ✓ `#each` returns Enumerator when no block given
|
|
234
|
+
- ✓ `#nodes` returns array of all nodes
|
|
235
|
+
- ✓ `#size` returns correct count
|
|
236
|
+
|
|
237
|
+
### Integration Tests
|
|
238
|
+
|
|
239
|
+
Must verify interaction with Root and Node:
|
|
240
|
+
|
|
241
|
+
1. **Collection Discovery**:
|
|
242
|
+
- ✓ Root discovers collections as subdirectories
|
|
243
|
+
- ✓ Collections discover nodes as metadata files within those subdirectories
|
|
244
|
+
|
|
245
|
+
2. **Node Creation**:
|
|
246
|
+
- ✓ Collection passes correct metadata file path to Node constructor
|
|
247
|
+
- ✓ Created nodes have correct collection reference
|
|
248
|
+
- ✓ Created nodes have IDs matching metadata filenames (without extension)
|
|
249
|
+
|
|
250
|
+
3. **Mixed Formats**:
|
|
251
|
+
- ✓ Collection with both `.yml` and `.json` nodes works correctly
|
|
252
|
+
- ✓ Nodes with same ID but different extensions are detected as conflicts
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Edge Cases
|
|
257
|
+
|
|
258
|
+
### Multiple Metadata Files for Same ID
|
|
259
|
+
|
|
260
|
+
**Scenario**: Both `my-post.yml` and `my-post.json` exist
|
|
261
|
+
|
|
262
|
+
**Expected Behavior**:
|
|
263
|
+
- **Strict mode**: Raise error (ambiguous node)
|
|
264
|
+
- **Lenient mode**: Use first found (alphabetically: `.json` < `.yml`)
|
|
265
|
+
|
|
266
|
+
**Recommendation**: Raise error to prevent confusion. Users should have only one metadata file per node.
|
|
267
|
+
|
|
268
|
+
**Test**:
|
|
269
|
+
```ruby
|
|
270
|
+
# Given:
|
|
271
|
+
# posts/my-post.yml
|
|
272
|
+
# posts/my-post.json
|
|
273
|
+
|
|
274
|
+
expect { collection.node_for('my-post') }.to raise_error(Ro::AmbiguousNodeError)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### Metadata File with No Corresponding Directory
|
|
280
|
+
|
|
281
|
+
**Scenario**: `my-post.yml` exists but no `my-post/` directory
|
|
282
|
+
|
|
283
|
+
**Expected Behavior**: Valid (metadata-only node per FR-007)
|
|
284
|
+
|
|
285
|
+
**Test**:
|
|
286
|
+
```ruby
|
|
287
|
+
# Given:
|
|
288
|
+
# posts/my-post.yml
|
|
289
|
+
# (no posts/my-post/ directory)
|
|
290
|
+
|
|
291
|
+
node = collection.node_for('my-post')
|
|
292
|
+
expect(node).to be_present
|
|
293
|
+
expect(node.asset_paths).to be_empty
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### Directory with No Corresponding Metadata File
|
|
299
|
+
|
|
300
|
+
**Scenario**: `my-post/` directory exists but no `my-post.yml`
|
|
301
|
+
|
|
302
|
+
**Expected Behavior**: Not discovered as a node (metadata file is the authority)
|
|
303
|
+
|
|
304
|
+
**Test**:
|
|
305
|
+
```ruby
|
|
306
|
+
# Given:
|
|
307
|
+
# posts/my-post/
|
|
308
|
+
# (no posts/my-post.yml)
|
|
309
|
+
|
|
310
|
+
node = collection.node_for('my-post')
|
|
311
|
+
expect(node).to be_nil
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Rationale**: Metadata file presence is the canonical marker for a node. Orphaned directories should be ignored or flagged as warnings.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### Both Old and New Structure Exist
|
|
319
|
+
|
|
320
|
+
**Scenario**: Both `my-post/attributes.yml` (old) and `my-post.yml` (new) exist
|
|
321
|
+
|
|
322
|
+
**Expected Behavior** (per FR-011): Prefer old structure until migration
|
|
323
|
+
|
|
324
|
+
**Test**:
|
|
325
|
+
```ruby
|
|
326
|
+
# Given:
|
|
327
|
+
# posts/my-post/attributes.yml (old structure)
|
|
328
|
+
# posts/my-post.yml (new structure)
|
|
329
|
+
|
|
330
|
+
# In v5.0 (post-migration), only new structure should be detected:
|
|
331
|
+
node = collection.node_for('my-post')
|
|
332
|
+
expect(node.metadata_file.to_s).to end_with('my-post.yml') # NEW structure
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**NOTE**: This scenario should only occur during migration. The migration tool should prevent this by removing old structure after verifying new structure.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Breaking Changes from v4.x
|
|
340
|
+
|
|
341
|
+
| Method | v4.x Behavior | v5.0 Behavior | Breaking? |
|
|
342
|
+
|--------|---------------|---------------|-----------|
|
|
343
|
+
| `#each` | Iterates subdirectories | Iterates metadata files | NO (internal change) |
|
|
344
|
+
| `#nodes` | Nodes from subdirectories | Nodes from metadata files | NO (same interface) |
|
|
345
|
+
| `#node_for` | Looks for `id/` directory | Looks for `id.{yml,json,...}` file | NO (same interface) |
|
|
346
|
+
|
|
347
|
+
**Migration Impact**: External API remains unchanged. The only breaking change is in how nodes are discovered internally, which is transparent to library users. However, users must migrate their data from old to new structure before upgrading to v5.0.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Performance Considerations
|
|
352
|
+
|
|
353
|
+
### Discovery Performance
|
|
354
|
+
|
|
355
|
+
**Old structure**:
|
|
356
|
+
```ruby
|
|
357
|
+
path.children.select(&:directory?) # O(N) where N = files + directories
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**New structure**:
|
|
361
|
+
```ruby
|
|
362
|
+
path.glob("*.yml") + path.glob("*.json") + ... # O(M) where M = total files
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Analysis**:
|
|
366
|
+
- Old: Must stat every entry to check if directory
|
|
367
|
+
- New: Must glob for each extension (typically 2-4 globs)
|
|
368
|
+
- **Result**: Similar performance, potentially faster for new structure (globs are optimized)
|
|
369
|
+
|
|
370
|
+
**Benchmark target**: <100ms for collections with 10,000 nodes (per SC-001)
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Migration Compatibility
|
|
375
|
+
|
|
376
|
+
### Transition Strategy
|
|
377
|
+
|
|
378
|
+
During migration from v4.x to v5.0, the Collection class should:
|
|
379
|
+
|
|
380
|
+
1. **Detect structure type**: Check if nodes exist as metadata files (new) or subdirectories (old)
|
|
381
|
+
2. **Raise error for mixed structures**: If some nodes are old and some are new, raise error directing user to run migration tool
|
|
382
|
+
3. **Log deprecation warnings**: In v4.x, log warning if old structure detected
|
|
383
|
+
|
|
384
|
+
**Implementation suggestion**:
|
|
385
|
+
```ruby
|
|
386
|
+
def each(&block)
|
|
387
|
+
if new_structure?
|
|
388
|
+
# Use metadata file discovery
|
|
389
|
+
metadata_files.each { |f| yield Ro::Node.new(self, f) }
|
|
390
|
+
elsif old_structure?
|
|
391
|
+
# Use directory discovery (v4.x compatibility)
|
|
392
|
+
subdirectories.each { |d| yield Ro::Node.new(self, d) }
|
|
393
|
+
else
|
|
394
|
+
raise "Mixed structures detected. Run migration tool first."
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def new_structure?
|
|
399
|
+
metadata_files.any?
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def old_structure?
|
|
403
|
+
subdirectories.any? { |d| (d / 'attributes.yml').exist? }
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**NOTE**: This compatibility code is ONLY for migration period. In final v5.0 release, only new structure should be supported.
|