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): the global one
under $OKF_HOME (default ~/.okf), or a project-local .okf.json
discovered by walking up from cwd (see .load / .discover), which replaces the
global one while you stand in its tree. Either way 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
Constant Summary collapse
- HOME_ENV =
"OKF_HOME"- DEFAULT_HOME =
"~/.okf"- LOCAL_FILE =
A project-local registry: the same JSON, discovered by walking up from the working directory rather than read from $OKF_HOME. Its presence is the whole state — no stored "local mode" flag — so a bare
okf serverinside a repo serves that repo's bundles with no global setup. ".okf.json"- LEGACY_LOCAL_FILE =
What that file used to be called. It is still discovered, because the file is committed — retiring the name outright would break every repository carrying one, to save eight characters. So the short name became canonical and this one keeps resolving;
okf registrysays so once, where a reader can act on it, and nothing else says anything. ".okf-registry.json"- LOCAL_FILES =
Both names, in the order a single directory prefers them. Discovery reads this list per directory rather than sweeping the whole path for one name and then the other — otherwise a legacy file at the repo root would beat a
.okf.jsontwo levels down, and "the nearest one wins" would quietly mean something else. [ LOCAL_FILE, LEGACY_LOCAL_FILE ].freeze
- NO_DISCOVERY_ENV =
The lever that forces the global registry even when a local one is on the path up from cwd. Set it (inline) and discovery is skipped — the escape hatch for a fixed-cwd caller (CI, a tool, the tests) that wants $OKF_HOME.
"OKF_NO_DISCOVERY"- 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. -
.discover(start) ⇒ Object
Walk up from
startlooking for a local registry; return its absolute path or nil. -
.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, cwd: nil) ⇒ Object
The registry a run resolves to.
-
.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
-
#expand(slug) ⇒ Object
Resolve
slugto its ordered, path-deduped bundle Entries — a group flattens recursively, a bundle slug resolves to itself. - #get(slug) ⇒ Object
-
#group?(slug) ⇒ Boolean
The group registered under
slug(already normalized, like #get), or nil. -
#groups_listing ⇒ Object
One row per group — this registry's own first, then the ones that arrived through a link, each tagged with the
linkit came from (nil for a group this registry owns). -
#import(asks, from:, as: nil) ⇒ Object
Copy bundles and groups out of another registry file into this one, under the slugs they carry there.
-
#initialize(path, relative_base: nil, follow_links: true) ⇒ Registry
constructor
relative_baseis the directory a local registry's relative paths anchor on (see .load). -
#link(slug, target) ⇒ Object
Point this registry at another registry file under
slug: its bundles resolve through the pointer from now on, under their own slugs unless one is already taken here. -
#links_listing ⇒ Object
One row per link for
registry list: the file it points at, how many bundles it contributed, and why it contributed none when it did. -
#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). -
#reopen ⇒ Object
A fresh instance over the same file, anchored the same way.
-
#save ⇒ Object
Persist the current state to disk.
-
#set_group(slug, member_asks) ⇒ Object
Create the group
slug, or addmember_asksto an existing one (a union, order-preserving). - #size ⇒ Object
- #slugs ⇒ Object
-
#unlink(slug) ⇒ Object
Drop the link
slugand every bundle that arrived through it. -
#unset_group_members(slug, member_asks) ⇒ Object
Drop
member_asksfrom the groupslug.
Constructor Details
#initialize(path, relative_base: nil, follow_links: true) ⇒ Registry
relative_base is the directory a local registry's relative paths anchor on
(see .load). nil means an absolute-path registry — the global $OKF_HOME one,
and every library caller — so its behavior is exactly what it was before
relative storage existed.
214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/okf/registry.rb', line 214 def initialize(path, relative_base: nil, follow_links: true) @path = path @relative_base = relative_base @follow_links = follow_links @entries = [] @groups = [] @links = [] @link_groups = [] @link_state = {} @link_of = {} read end |
Instance Attribute Details
#path ⇒ Object (readonly)
Returns the value of attribute path.
208 209 210 |
# File 'lib/okf/registry.rb', line 208 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.
196 197 198 199 200 201 202 203 |
# File 'lib/okf/registry.rb', line 196 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 |
.discover(start) ⇒ Object
Walk up from start looking for a local registry; return its absolute path
or nil. Stops at the filesystem root (parent == self), so it never loops.
149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/okf/registry.rb', line 149 def discover(start) dir = (start.to_s) loop do found = LOCAL_FILES.map { |name| File.join(dir, name) }.find { |candidate| File.file?(candidate) } return found if found parent = File.dirname(dir) break if parent == dir dir = parent end nil 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".
121 122 123 124 125 |
# File 'lib/okf/registry.rb', line 121 def (base) File.(base) rescue ArgumentError => e raise OKF::Error, "cannot expand #{base}: #{e.}" end |
.load(home: nil, cwd: nil) ⇒ Object
The registry a run resolves to. Precedence, highest first: OKF_NO_DISCOVERY
forces the global one; else a .okf.json (or the legacy
.okf-registry.json) discovered on the path up from cwd wins; else the
global $OKF_HOME registry, exactly as before.
cwd nil ⇒ no discovery, so an embedding app that calls load with no
arguments keeps the global-only behavior — only the CLI opts in by passing
cwd: Dir.pwd. $OKF_HOME names where the global registry lives; it does
not veto a nearer local one (it is commonly exported, so letting it would
silently defeat the feature for its own audience).
136 137 138 139 140 141 142 143 144 145 |
# File 'lib/okf/registry.rb', line 136 def load(home: nil, cwd: nil) looking = cwd && ENV[NO_DISCOVERY_ENV].to_s.empty? local = looking ? discover(cwd) : nil # A local registry anchors its relative paths on its own directory; the # global one has no common anchor, so it stays absolute (relative_base nil). # Links are the *global* registry's alone: a discovered local one parses # and preserves them but does not resolve them, so depth is one by # construction rather than by a limit anyone has to enforce. new(local || path(home: home), relative_base: local && File.dirname(local), follow_links: local.nil?) 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.
167 168 169 |
# File 'lib/okf/registry.rb', line 167 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.
110 111 112 113 114 115 |
# File 'lib/okf/registry.rb', line 110 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.
186 187 188 |
# File 'lib/okf/registry.rb', line 186 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.
175 176 177 178 |
# File 'lib/okf/registry.rb', line 175 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.
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
# File 'lib/okf/registry.rb', line 331 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 } refuse_linked(entry, "register") if entry 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
260 261 262 |
# File 'lib/okf/registry.rb', line 260 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.
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/okf/registry.rb', line 273 def default=(slug) normalized = self.class.normalize(slug) if group?(normalized) raise OKF::Error, "cannot default to a group: @#{normalized} names a set of bundles, and the default is one bundle" end entry = get(normalized) raise OKF::Error, "no such bundle: #{slug}" unless entry refuse_linked(entry, "default to") 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
227 228 229 |
# File 'lib/okf/registry.rb', line 227 def each(&block) @entries.each(&block) end |
#empty? ⇒ Boolean
235 236 237 |
# File 'lib/okf/registry.rb', line 235 def empty? @entries.empty? end |
#expand(slug) ⇒ Object
Resolve slug to its ordered, path-deduped bundle Entries — a group flattens
recursively, a bundle slug resolves to itself. Returns leaves even when their
directory has vanished; the caller (search/server) decides whether to skip
one, the way @all tolerates a gap. Raises OKF::Error on a cycle — a
defense-in-depth guard, since #set_group already blocks one at write time but
the file is hand-editable.
447 448 449 450 451 452 |
# File 'lib/okf/registry.rb', line 447 def (slug) entries = [] seen = [] resolve_into(self.class.normalize(slug), entries, seen, []) entries end |
#get(slug) ⇒ Object
243 244 245 |
# File 'lib/okf/registry.rb', line 243 def get(slug) @entries.find { |entry| entry.slug == slug } end |
#group?(slug) ⇒ Boolean
The group registered under slug (already normalized, like #get), or nil.
248 249 250 |
# File 'lib/okf/registry.rb', line 248 def group?(slug) @groups.find { |group| group.slug == slug } || @link_groups.find { |group| group.slug == slug } end |
#groups_listing ⇒ Object
One row per group — this registry's own first, then the ones that arrived
through a link, each tagged with the link it came from (nil for a group
this registry owns). Members, and how many bundles it resolves to
(+resolved+ is nil when a hand-edited cycle makes it unanswerable).
One list, not two. #group? resolves a linked group, so the method that
enumerates groups has to name it: a second listing for the linked half is
how a caller comes to answer about a smaller set than the same object can
resolve — the drift a sibling reading this method would inherit silently.
A caller that wants only the editable ones filters on link.
571 572 573 574 575 576 577 578 579 580 |
# File 'lib/okf/registry.rb', line 571 def groups_listing (@groups + @link_groups).map do |group| resolved = begin (group.slug).size rescue OKF::Error nil end { slug: group.slug, members: group.members.dup, resolved: resolved, link: @link_of[group.slug] } end end |
#import(asks, from:, as: nil) ⇒ Object
Copy bundles and groups out of another registry file into this one, under
the slugs they carry there. asks names bundles or groups in the source
(bare, or as @ref); a group brings everything it reaches and is recreated
here. as renames the single thing asked for. Persists once, and returns
{ bundles: [ Entry… ], groups: [ Group… ] }.
Import is the opposite trade from #link, and the pair is the point: a link holds a live pointer, so the other file keeps owning what it lends and can take it back; an import copies the reference and owns it from then on — the source can be deleted, moved or rewritten and nothing here notices. That is why the two disagree on a collision. A linked name was never chosen here, so #link_slug invents around it; an imported name lands in this file under this registry's own rules, so a collision is refused exactly as #rename's is. The gem may invent a name; it may not substitute one you chose, and naming a slug on the command line is choosing it.
Nothing is applied until everything has been checked. Half an import is a registry the user has to unpick by hand, reported as a success — so every ask is resolved and refused against the current state first, and one #write publishes the lot. Imported rows append, so the default stays where it was.
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 |
# File 'lib/okf/registry.rb', line 509 def import(asks, from:, as: nil) source = open_source(from) # #normalize_members drops what normalizes to nothing, which is right for a # group's members and wrong here: dropping one ask and importing the rest is # the silent partial all-or-nothing exists to rule out, and the report would # count the ones that landed as a success. unusable = asks.find { |ask| self.class.normalize(ask).empty? } raise OKF::Error, "not a usable slug: #{unusable} (letters and digits, please)" if unusable names = normalize_members(asks) raise OKF::Error, "nothing to import (name a bundle or a group from #{source.path})" if names.empty? plan = [] names.each { |name| plan_import(source, name, nil, plan, []) } rename_import(plan, names.first, as) if as plan.each { |step| refuse_import(step, source.path) } apply_import(plan) end |
#link(slug, target) ⇒ Object
Point this registry at another registry file under slug: its bundles
resolve through the pointer from now on, under their own slugs unless one
is already taken here. Re-linking a slug re-points it. Persists, returns
the Link.
The target must exist now — a link is an explicit ask, and one typed at a path that is not a registry file is a typo worth catching at the keyboard rather than a silent empty section later. (A target that vanishes afterwards is a different case, and is tolerated: see #links_listing.)
463 464 465 466 467 468 469 470 471 472 473 474 |
# File 'lib/okf/registry.rb', line 463 def link(slug, target) name = explicit_link_slug(slug) registry = self.class.(target.to_s) raise OKF::Error, "not a registry file: #{target}" unless File.file?(registry) raise OKF::Error, "a registry cannot link itself: #{registry}" if registry == self.class.(@path) existing = @links.find { |candidate| candidate.slug == name } existing ? existing.registry = registry : @links << Link.new(name, registry) write refresh_links @links.find { |candidate| candidate.slug == name } end |
#links_listing ⇒ Object
One row per link for registry list: the file it points at, how many
bundles it contributed, and why it contributed none when it did. A target
that is gone or unreadable is reported, never raised — one bad pointer
must not take down the registry that holds it, the same tolerance a
vanished bundle directory gets.
533 534 535 536 537 538 539 |
# File 'lib/okf/registry.rb', line 533 def links_listing @links.map do |link| state = @link_state[link.slug] || { bundles: 0, missing: false, unreadable: false } { slug: link.slug, registry: link.registry, bundles: state[:bundles], missing: state[:missing], unreadable: state[:unreadable] } end 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.
317 318 319 320 321 322 323 324 |
# File 'lib/okf/registry.rb', line 317 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), link: entry.link, origin: entry.origin } 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.
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/okf/registry.rb', line 359 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))) refuse_linked(target, "remove") if target if target @entries.delete(target) cascade_remove(target.slug) write return target end # Not a bundle — a group answers to its slug only (having no path, it can # never match the path-shaped reading). Removing it, like removing a bundle, # drops the slug from every group that named it. group = self.class.path_shaped?(slug) ? nil : (group?(slug) || group?(self.class.normalize(slug))) return nil unless group refuse_linked(group, "remove") @groups.delete(group) cascade_remove(group.slug) write group 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.
297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/okf/registry.rb', line 297 def rename(old_slug, new_slug) old = self.class.normalize(old_slug) entry = get(old) || group?(old) raise OKF::Error, "no such bundle or group: #{old_slug}" unless entry refuse_linked(entry, "rename") slug = explicit_slug(new_slug, entry) entry.slug = slug # A member list stores slugs, so a rename that stopped at the entry would # orphan every group that named it — cascade the new name across them. cascade_rename(old, slug) write entry end |
#reopen ⇒ Object
A fresh instance over the same file, anchored the same way. The server
re-opens the registry per request (to show an edit made elsewhere) and
after each write; it must keep the relative_base a discovered local
registry carries. A bare Registry.new(path) would drop it — so a local
registry's in-tree paths would resolve against the wrong directory (every
served bundle reads as "folder is gone" in the manager) and a browser
write would flatten a newly-added in-tree bundle to an absolute path,
silently undoing the portability the base exists for.
557 558 559 |
# File 'lib/okf/registry.rb', line 557 def reopen self.class.new(@path, relative_base: @relative_base, follow_links: @follow_links) end |
#save ⇒ Object
Persist the current state to disk. The mutating verbs write as a side effect
of the change; save is the public seam for the one caller that creates a
registry with nothing to change yet — okf registry init, materializing an
empty local file so discovery has something to find.
545 546 547 |
# File 'lib/okf/registry.rb', line 545 def save write end |
#set_group(slug, member_asks) ⇒ Object
Create the group slug, or add member_asks to an existing one (a union,
order-preserving). Members are bundle or group slugs, given bare or as
@ref; each must already name a bundle or a group, and the result must not
reach itself (a cycle is refused before the write). Persists, returns the
Group.
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 |
# File 'lib/okf/registry.rb', line 392 def set_group(slug, member_asks) name = explicit_group_slug(slug) members = normalize_members(member_asks) raise OKF::Error, "a group needs at least one member (okf registry group #{name} <@bundle…>)" if members.empty? members.each do |member| found = get(member) || group?(member) raise OKF::Error, "no such bundle or group: @#{member} (okf registry list)" unless found # A group stores slugs, and a linked slug lives only while its link # resolves — holding one would dangle the group the moment the link goes, # the same foreign key the default rule refused. refuse_linked(found, "group") end group = group?(name) merged = group ? group.members.dup : [] members.each { |member| merged << member unless merged.include?(member) } raise OKF::Error, "group cycle: @#{name} would contain itself" if reaches_self?(name, merged) if group group.members = merged else group = Group.new(name, merged) @groups << group end write group end |
#size ⇒ Object
231 232 233 |
# File 'lib/okf/registry.rb', line 231 def size @entries.size end |
#slugs ⇒ Object
239 240 241 |
# File 'lib/okf/registry.rb', line 239 def slugs @entries.map(&:slug) end |
#unlink(slug) ⇒ Object
Drop the link slug and every bundle that arrived through it. Returns the
removed Link, or nil when nothing matched.
478 479 480 481 482 483 484 485 486 487 |
# File 'lib/okf/registry.rb', line 478 def unlink(slug) name = self.class.normalize(slug) link = @links.find { |candidate| candidate.slug == name } return nil unless link @links.delete(link) write refresh_links link end |
#unset_group_members(slug, member_asks) ⇒ Object
Drop member_asks from the group slug. Removing the last member deletes
the group — an empty group resolves to nothing, so it is not worth keeping.
Returns [removed_members, emptied?]. Raises on an unknown group; a member
that was not there is simply not in the returned list.
426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/okf/registry.rb', line 426 def unset_group_members(slug, member_asks) name = self.class.normalize(slug) group = group?(name) raise OKF::Error, "no such group: #{slug} (okf registry list)" unless group refuse_linked(group, "ungroup") asks = normalize_members(member_asks) removed = group.members & asks group.members -= asks emptied = group.members.empty? @groups.delete(group) if emptied write [ removed, emptied ] end |