Class: OKF::Registry

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/okf/registry.rb

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. @all means 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

Class Method Summary collapse

Instance Method Summary collapse

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

#pathObject (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 expand(base)
  File.expand_path(base)
rescue ArgumentError => e
  raise OKF::Error, "cannot expand #{base}: #{e.message}"
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(expand(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.

Returns:

  • (Boolean)


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.

Raises:



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.expand(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

#defaultObject

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 , which is not a directory" instead of the much worse "not a registered bundle". nil only when nothing is registered.



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.

Raises:



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

Returns:

  • (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

#listingObject

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.expand(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.

Raises:



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

#sizeObject



128
129
130
# File 'lib/okf/registry.rb', line 128

def size
  @entries.size
end

#slugsObject



136
137
138
# File 'lib/okf/registry.rb', line 136

def slugs
  @entries.map(&:slug)
end