Class: OKF::Registry
Overview
A persistent, ordered registry of bundle references — the kernel behind the
multi-bundle server. It is a plain JSON file (no database) under $OKF_HOME
(default ~/.okf), so okf registry set/del and a later bare okf server
share one on-disk list. Part of the shell — it reads and writes a file.
registry = OKF::Registry.load
registry.add("docs") # persists, returns the Entry
registry.default = "docs" # moves docs to the front
registry.rename("docs", "handbook") # new slug, same path
registry.default # => the first Entry
registry.listing # => [{ slug:, title:, path:, default: }]
The first entry is the default — the bundle a bare okf server opens at
/. That is position, not a stored slug: a slug would be a foreign key into
this same list, and every operation would owe it referential integrity —
carry it through a rename, re-point it after an add --as, clear it on a
remove, and survive it dangling. Order is state the registry already keeps,
so default= just moves the entry to the front and there is nothing left to
maintain or to dangle.
On disk: { "bundles" => [ { "slug" => …, "path" => absolute dir, "title" => label } ] }, the first row being the default. A bare array (the original shape) still reads.
Defined Under Namespace
Classes: Entry
Constant Summary collapse
- HOME_ENV =
"OKF_HOME"- DEFAULT_HOME =
"~/.okf"- RESERVED_SLUGS =
Slugs the ref grammar has already spoken for.
@allmeans every registered bundle, so a bundle slugged "all" could never be named — reserve it here, where both slug paths pass, rather than let one register and then be unreachable. %w[all].freeze
Instance Attribute Summary collapse
-
#path ⇒ Object
readonly
Returns the value of attribute path.
Class Method Summary collapse
-
.dedupe(base, taken) ⇒ Object
baseslugified, then suffixed (-2, -3, …) until it avoids every slug intaken. -
.expand(base) ⇒ Object
File.expand_path raises ArgumentError on a "~nosuchuser" (or bare "~" with no HOME) — a bad argument, which the CLI must report as a usage error rather than let escape as a backtrace and an exit code that means "failing bundle".
- .load(home: nil) ⇒ Object
-
.normalize(base) ⇒ Object
Normalize
baseto a url-safe slug (lowercase, dashes) — "" when nothing survives. -
.path(home: nil) ⇒ Object
The registry file: $OKF_HOME/registry.json, $OKF_HOME defaulting to ~/.okf.
-
.path_shaped?(arg) ⇒ Boolean
Does this argument name a location rather than a slug? A separator settles it: #normalize maps one to a dash, so no slug can contain one.
-
.slugify(base) ⇒ Object
basenormalized, with a placeholder when nothing survives — for minting a slug from a directory basename, where some name must come out.
Instance Method Summary collapse
-
#add(dir, as: nil, default: false) ⇒ Object
Register
dir(must be a readable bundle directory). -
#default ⇒ Object
The default bundle a bare
okf serverselects: the first entry still on disk. -
#default=(slug) ⇒ Object
Choose which bundle
/opens, by moving that entry to the front. - #each(&block) ⇒ Object
- #empty? ⇒ Boolean
- #get(slug) ⇒ Object
-
#initialize(path) ⇒ Registry
constructor
A new instance of Registry.
-
#listing ⇒ Object
One row per bundle for the CLI list:
diris the on-disk directory,mountthe server path,defaulttrue for the first row,missingtrue when the registered directory no longer exists on disk. -
#remove(slug) ⇒ Object
Remove the entry named by
slug(or whose path matches). -
#rename(old_slug, new_slug) ⇒ Object
Give the bundle at
old_sluga new slug (its mount path and switcher name). - #size ⇒ Object
- #slugs ⇒ Object
Constructor Details
#initialize(path) ⇒ Registry
Returns a new instance of Registry.
118 119 120 121 122 |
# File 'lib/okf/registry.rb', line 118 def initialize(path) @path = path @entries = [] read end |
Instance Attribute Details
#path ⇒ Object (readonly)
Returns the value of attribute path.
116 117 118 |
# File 'lib/okf/registry.rb', line 116 def path @path end |
Class Method Details
.dedupe(base, taken) ⇒ Object
base slugified, then suffixed (-2, -3, …) until it avoids every slug in
taken. Reserving is the caller's business, not this helper's: the
ephemeral hub mints through here too, and it has no registry and no
@refs, so a name reserved for the ref grammar would suffix it to /b/all-2/
against a /b/all/ that does not exist. #unique_slug adds the reserved
names because the registry is where they mean something.
104 105 106 107 108 109 110 111 |
# File 'lib/okf/registry.rb', line 104 def dedupe(base, taken) slug = slugify(base) return slug unless taken.include?(slug) n = 2 n += 1 while taken.include?("#{slug}-#{n}") "#{slug}-#{n}" end |
.expand(base) ⇒ Object
File.expand_path raises ArgumentError on a "~nosuchuser" (or bare "~" with no HOME) — a bad argument, which the CLI must report as a usage error rather than let escape as a backtrace and an exit code that means "failing bundle".
61 62 63 64 65 |
# File 'lib/okf/registry.rb', line 61 def (base) File.(base) rescue ArgumentError => e raise OKF::Error, "cannot expand #{base}: #{e.}" end |
.load(home: nil) ⇒ Object
67 68 69 |
# File 'lib/okf/registry.rb', line 67 def load(home: nil) new(path(home: home)) end |
.normalize(base) ⇒ Object
Normalize base to a url-safe slug (lowercase, dashes) — "" when nothing
survives. This is the form a lookup wants: "@***" normalizes to nothing
and must stay nothing, so it fails as a bad ref instead of resolving to
whatever #slugify's placeholder happens to name.
75 76 77 |
# File 'lib/okf/registry.rb', line 75 def normalize(base) base.to_s.strip.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "") end |
.path(home: nil) ⇒ Object
The registry file: $OKF_HOME/registry.json, $OKF_HOME defaulting to ~/.okf.
The env var is the only lever the CLI offers; home overrides it for an
embedding app (and the tests), which should not have to mutate a
process-global to say which registry it means. An empty home or env var
counts as unset — expand_path("") would silently plant the registry in
the current directory.
50 51 52 53 54 55 |
# File 'lib/okf/registry.rb', line 50 def path(home: nil) env = ENV.fetch(HOME_ENV, nil) home = nil if home.nil? || home.to_s.empty? base = home || (env.nil? || env.empty? ? DEFAULT_HOME : env) File.join((base), "registry.json") end |
.path_shaped?(arg) ⇒ Boolean
Does this argument name a location rather than a slug? A separator settles it: #normalize maps one to a dash, so no slug can contain one. The reading matters because #remove takes either — and a path that matched no entry must not fall through to a slug lookup, where "./notes" strips to "notes" and deletes an entry pointing somewhere else entirely, reporting success. This is the line between the two readings.
94 95 96 |
# File 'lib/okf/registry.rb', line 94 def path_shaped?(arg) arg.to_s.include?(File::SEPARATOR) end |
.slugify(base) ⇒ Object
base normalized, with a placeholder when nothing survives — for
minting a slug from a directory basename, where some name must come
out. Shared with the server's ephemeral (unregistered) bundles so both
slug the same way.
83 84 85 86 |
# File 'lib/okf/registry.rb', line 83 def slugify(base) slug = normalize(base) slug.empty? ? "bundle" : slug end |
Instance Method Details
#add(dir, as: nil, default: false) ⇒ Object
Register dir (must be a readable bundle directory). Re-registering the same
path refreshes its title in place (and its slug when as is given). A
basename-derived slug is deduped with a suffix; an explicit as raises on
collision instead — the same "explicit is explicit" rule as #rename.
default: true moves it to the front. Persists, then returns the entry.
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/okf/registry.rb', line 210 def add(dir, as: nil, default: false) root = self.class.(dir.to_s) raise OKF::Error, "not a directory: #{dir}" unless File.directory?(root) # The label is path arithmetic; Folder.load would parse every markdown # file in the bundle to hand back its own basename. title = Bundle::Folder.label(root) entry = @entries.find { |candidate| candidate.path == root } if entry entry.title = title entry.slug = explicit_slug(as, entry) if as else slug = as ? explicit_slug(as, nil) : unique_slug(File.basename(root), nil) entry = Entry.new(slug, root, title) @entries << entry end if default @entries.delete(entry) @entries.unshift(entry) end write entry end |
#default ⇒ Object
The default bundle a bare okf server selects: the first entry still on
disk. Position decides it, but a position the hub cannot serve decides
nothing — it drops a vanished directory rather than serving a hole, so the
default has to skip the same ones or registry list would star a bundle
/ never opens. Falling back to the first entry when every one has
vanished keeps a bare @ failing with "points to
152 153 154 |
# File 'lib/okf/registry.rb', line 152 def default @entries.find { |entry| File.directory?(entry.path) } || @entries.first end |
#default=(slug) ⇒ Object
Choose which bundle / opens, by moving that entry to the front. Persists;
raises on an unknown slug. The ask is normalized the way registration
normalized it, so the name the user typed at --as is the name that resolves
here.
A directory that is gone is refused, exactly as #add refuses to register
one: both are explicit asks, and #default skips a vanished entry, so
allowing the move would answer default bundle → <some other slug> to
someone who named this one.
165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/okf/registry.rb', line 165 def default=(slug) entry = get(self.class.normalize(slug)) raise OKF::Error, "no such bundle: #{slug}" unless entry unless File.directory?(entry.path) raise OKF::Error, "cannot default to #{entry.slug}: #{entry.path} is not a directory " \ "(okf registry del #{entry.slug}, or restore it)" end @entries.delete(entry) @entries.unshift(entry) write end |
#each(&block) ⇒ Object
124 125 126 |
# File 'lib/okf/registry.rb', line 124 def each(&block) @entries.each(&block) end |
#empty? ⇒ Boolean
132 133 134 |
# File 'lib/okf/registry.rb', line 132 def empty? @entries.empty? end |
#get(slug) ⇒ Object
140 141 142 |
# File 'lib/okf/registry.rb', line 140 def get(slug) @entries.find { |entry| entry.slug == slug } end |
#listing ⇒ Object
One row per bundle for the CLI list: dir is the on-disk directory, mount
the server path, default true for the first row, missing true when the
registered directory no longer exists on disk. default stays in the row
even though it is now derivable from position — a consumer reading the JSON
should not have to know the rule to find the bundle / opens.
197 198 199 200 201 202 203 |
# File 'lib/okf/registry.rb', line 197 def listing chosen = default @entries.map do |entry| { slug: entry.slug, title: entry.title, dir: entry.path, mount: "/b/#{entry.slug}/", default: entry.equal?(chosen), missing: !File.directory?(entry.path) } end end |
#remove(slug) ⇒ Object
Remove the entry named by slug (or whose path matches). Returns the removed
entry, or nil when nothing matched. Removing the default needs no cleanup —
the next entry is first, and so is the default. Persists on change.
237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/okf/registry.rb', line 237 def remove(slug) # Slug-or-dir, so the normalized reading comes *last*: "./docs" must mean # the directory while one is registered under that path, and only fall # back to naming the "docs" slug when no path matches. target = get(slug) || @entries.find { |entry| entry.path == self.class.(slug.to_s) } || (self.class.path_shaped?(slug) ? nil : get(self.class.normalize(slug))) return nil unless target @entries.delete(target) write target end |
#rename(old_slug, new_slug) ⇒ Object
Give the bundle at old_slug a new slug (its mount path and switcher name).
The new name is slugified; a collision with another entry raises rather than
silently suffixing — a rename is explicit. Position is untouched, so a
renamed default stays the default with no bookkeeping.
182 183 184 185 186 187 188 189 190 |
# File 'lib/okf/registry.rb', line 182 def rename(old_slug, new_slug) entry = get(self.class.normalize(old_slug)) raise OKF::Error, "no such bundle: #{old_slug}" unless entry slug = explicit_slug(new_slug, entry) entry.slug = slug write entry end |
#size ⇒ Object
128 129 130 |
# File 'lib/okf/registry.rb', line 128 def size @entries.size end |
#slugs ⇒ Object
136 137 138 |
# File 'lib/okf/registry.rb', line 136 def slugs @entries.map(&:slug) end |