Class: Labimotion::OntologyStore
- Inherits:
-
Object
- Object
- Labimotion::OntologyStore
- Defined in:
- lib/labimotion/libs/ontology_store.rb
Overview
The stored vocabulary behind an "ontology-select" field: one JSON file per ontology root, holding that root's whole subtree.
Both consumers read this one file. The picker renders it as a tree, the AI auto-fill takes the labels out of it. Before the store they resolved the same subtree independently — the browser walking /children node by node, the server asking for the transitive closure — so the only thing keeping the two lists in agreement was somebody checking.
Nothing is ever written straight onto the live path. A concurrent reader must not see a half-written file, and a failed fetch must not replace a good list with an empty one, so a write takes a lock, validates what came back, writes a sibling temp file and renames it into place.
Defined Under Namespace
Constant Summary collapse
- ACCESSION =
The last path segment of an IRI is the term's accession (
BAO_0000019), which is already filename-safe and, unlike a hash, says what the file is when you list the directory. Anything that is not a clean accession — which no real IRI produces — falls back to a digest rather than being trusted into a path. /\A[A-Za-z0-9][A-Za-z0-9_.-]{0,63}\z/- ONTOLOGY =
owl_nameis not the IRI's namespace: CHMO serves OBI and BFO terms, so the accession alone is not unique across ontologies and the prefix stays. /\A[a-z0-9][a-z0-9_-]{0,31}\z/- KEEP_RATIO =
A refresh that comes back with a fraction of what is already stored is a service having a bad day, not an ontology that shrank by half. Keep what is on disk and say so.
0.5- MIN_COMPARABLE =
Below this there is no meaningful proportion to compare against.
8- MIN_REFRESH_SECONDS =
Refresh is a user action on shared state; this is politeness toward the ontology service, not protection of our own data.
60
Class Method Summary collapse
-
.dir ⇒ Object
Read through a method so a spec can stub the constant.
-
.document(root, synced_by: nil) ⇒ Hash?
The subtree for one parsed root, warming the file when it is missing.
- .filename(root) ⇒ Object
-
.labels(root) ⇒ Object
Just the labels, for the auto-fill prompt.
-
.path_for(root) ⇒ Object
<ontology>-<accession>.jsonunder the roots directory. -
.read(root) ⇒ Object
The stored document, or nil when there is no readable file.
-
.refresh!(root, synced_by: nil, interval: true) ⇒ Object
Re-fetch and rewrite.
- .stored?(root) ⇒ Boolean
Class Method Details
.dir ⇒ Object
Read through a method so a spec can stub the constant.
135 136 137 |
# File 'lib/labimotion/libs/ontology_store.rb', line 135 def dir Labimotion::ONTOLOGY_ROOTS_DIR end |
.document(root, synced_by: nil) ⇒ Hash?
The subtree for one parsed root, warming the file when it is missing. Never raises: a root that cannot be stored still answers this request from a live resolve, which is exactly the behaviour it had before.
55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/labimotion/libs/ontology_store.rb', line 55 def document(root, synced_by: nil) stored = read(root) return stored if stored begin refresh!(root, synced_by: synced_by, interval: false) rescue StandardError => e log("could not store #{root[:iri]}: #{e.}") live_document(root) end end |
.filename(root) ⇒ Object
126 127 128 129 130 131 132 |
# File 'lib/labimotion/libs/ontology_store.rb', line 126 def filename(root) ontology = root[:ontology].to_s raise Error, "unusable ontology name #{ontology.inspect}" unless ONTOLOGY.match?(ontology) iri = root[:iri].to_s "#{ontology}-#{accession(iri) || "x#{Digest::SHA256.hexdigest(iri)[0, 16]}"}.json" end |
.labels(root) ⇒ Object
Just the labels, for the auto-fill prompt.
68 69 70 71 72 |
# File 'lib/labimotion/libs/ontology_store.rb', line 68 def labels(root) doc = document(root) terms = doc.is_a?(Hash) ? doc['terms'] : nil Array(terms).filter_map { |term| term['label'].to_s.strip.presence } end |
.path_for(root) ⇒ Object
<ontology>-<accession>.json under the roots directory.
115 116 117 118 119 120 121 122 123 124 |
# File 'lib/labimotion/libs/ontology_store.rb', line 115 def path_for(root) path = File.join(dir, filename(root)) # The root comes from a template a designer controls. Taking the segment # after the last separator cannot produce a traversal, but this makes it # impossible for a later edit to the derivation to reintroduce one. raise Error, 'refusing an ontology path outside the store' unless File.(path).start_with?("#{File.(dir)}#{File::SEPARATOR}") path end |
.read(root) ⇒ Object
The stored document, or nil when there is no readable file.
97 98 99 100 101 102 103 104 105 106 |
# File 'lib/labimotion/libs/ontology_store.rb', line 97 def read(root) path = path_for(root) return nil unless File.exist?(path) doc = JSON.parse(File.read(path)) doc.is_a?(Hash) && doc['terms'].is_a?(Array) ? doc : nil rescue StandardError => e log("unreadable store for #{root[:iri]}: #{e.}") nil end |
.refresh!(root, synced_by: nil, interval: true) ⇒ Object
Re-fetch and rewrite. Raises Error with a message meant to be shown to whoever asked for it.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/labimotion/libs/ontology_store.rb', line 79 def refresh!(root, synced_by: nil, interval: true) existing = read(root) guard_interval!(existing) if interval with_lock(root) do # Re-read inside the lock: whoever held it may have just done the work. fresh = read(root) next fresh if interval == false && fresh terms = Labimotion::OntologyTerms.subtree(root) validate!(terms, existing) doc = build_document(root, terms, synced_by) write_atomically(path_for(root), doc) doc end end |