Browse Source

Use new VOLK syntax for term set access.

scossu 1 week ago
parent
commit
938f0edd68

+ 62 - 85
src/generator.lua

@@ -16,6 +16,7 @@ local pkar = require "pocket_archive"
 local logger = pkar.logger
 local model = require "pocket_archive.model"
 local repo = require "pocket_archive.repo"
+local get_single_v = repo.get_single_v
 local transformers = require "pocket_archive.transformers"
 
 local dbg = require "debugger"
@@ -69,8 +70,7 @@ local TN_WEB_PATH = TN_FS_PATH:gsub(pkar.config.htmlgen.out_dir, "")
 
 -- Get model configuration from subject URI.
 local function get_mconf(s)
-    local ctype
-    _, ctype = next(repo.gr:attr(s, model.id_to_uri.content_type))
+    local ctype = get_single_v(s, "content_type")
 
     return model.types[model.uri_to_id[nsm.denormalize_uri(ctype.data)]]
 end
@@ -110,23 +110,20 @@ local function get_tn_url(s, ext)  -- TODO caller needs to pass correct ext
     end
 
     -- If it's a brick, look for its ref.
-    local ref
-    _, ref = next(repo.gr:attr(s, model.id_to_uri.ref))
+    local ref = get_single_v(s, "ref")
     if ref then return get_tn_url(ref, ext) end
 
-    local pref_rep
-    _, pref_rep = next(repo.gr:attr(s, model.id_to_uri.pref_rep))
+    local pref_rep = get_single_v(s, "pref_rep")
     if pref_rep then return get_tn_url(pref_rep, ext) end
 
     -- Recurse through all first children until one with a thumbnail, or a
     -- leaf without children, is found.
-    local t
     -- Look for preferred rep first.
-    _, t = next(repo.gr:attr(s, model.id_to_uri["pref_rep"]))
+    local t = get_single_v(s, "pref_rep")
     if not t then
         -- If not found, look for reference of first child.
-        _, t = next(repo.gr:attr(s, model.id_to_uri.first))
-        if t then _, t = next(repo.gr:attr(t, model.id_to_uri.ref)) end
+        t = get_single_v(s, "first")
+        if t then t = get_single_v(t, "ref") end
     end
     if t then return get_tn_url(t, ext) end
 end
@@ -144,12 +141,10 @@ end
 
 
 local function generate_coll(s, mconf)
-    local pref_rep, pref_rep_url
-    _, pref_rep = next(repo.gr:attr(s, model.id_to_uri.pref_rep))
+    local pref_rep = get_single_v(s, "pref_rep")
 
     local members = {}
-    local child_s
-    _, child_s = next(repo.gr:attr(s, model.id_to_uri.first))
+    local child_s = get_single_v(s, "first")
     --[[ FIXME this should check for the ref attribute of the proxy.
     if not repo.gr:contains(triple.new(
         s, model.id_to_uri.has_member, first
@@ -163,6 +158,7 @@ local function generate_coll(s, mconf)
     -- First child is the alternative pref representation.
     if not pref_rep then pref_rep = child_s end
 
+    local pref_rep_url
     if pref_rep then
         pref_rep_url = pkar.gen_pairtree("/res", pref_rep.data, ".html", true)
         -- Collection page uses full size image, shrunk to size if necessary.
@@ -171,20 +167,20 @@ local function generate_coll(s, mconf)
 
     local child_ref, child_label, child_mconf
     while child_s do
-        _, child_ref = next(repo.gr:attr(child_s, model.id_to_uri.ref))
+        child_ref = get_single_v(child_s, "ref")
         -- Skip relationship with long description doc.
         if repo.gr:contains(
             triple.new(s, model.id_to_uri.long_description, child_ref)
         ) then goto skip end
         child_mconf = get_mconf(child_ref)
         --if not child_ref then child_ref = child_s end
-        _, child_label = next(repo.gr:attr(child_s, model.id_to_uri.label))
+        child_label = get_single_v(child_s, "label")
         if not child_label then
             if child_mconf.types.file then
-                _, child_label = next(repo.gr:attr(child_ref, model.id_to_uri.source_path))
-                child_label = path.basename(child_label.data)
+                child_label = path.basename(
+                        get_single_v(child_ref, "source_path").data)
             else
-                _, child_label = next(repo.gr:attr(child_ref, model.id_to_uri.label))
+                child_label = get_single_v(child_ref, "label")
             end
         end
         if child_label.data then child_label = child_label.data end
@@ -197,19 +193,17 @@ local function generate_coll(s, mconf)
         })
 
         ::skip::
-        _, child_s = next(repo.gr:attr(child_s, model.id_to_uri.next))
+        child_s = get_single_v(child_s, "next")
     end
 
-    local title, description, body_rel
-    _, title = next(repo.gr:attr(s, model.id_to_uri.label))
-    _, description = next(repo.gr:attr(s, model.id_to_uri.description))
-    _, body_rel = next(repo.gr:attr(s, model.id_to_uri.long_description))
+    local title = get_single_v(s, "label")
+    local description = get_single_v(s, "description")
+    local body_rel = get_single_v(s, "long_description")
 
     local body
     if body_rel then
-        local body_res_path
-        _, body_res_path = next(repo.gr:attr(body_rel, model.id_to_uri.archive_path))
-        local bfh = assert(io.open(body_res_path.data, "r"))
+        local body_res_path = get_single_v(body_rel, "archive_path").data
+        local bfh = assert(io.open(body_res_path, "r"))
         body = markdown(bfh:read("a"))
         bfh:close()
     end
@@ -252,10 +246,10 @@ local function generate_dres(s, mconf)
     -- Metadata
     local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
 
-    local pref_rep, pref_rep_url
-    _, pref_rep = next(repo.gr:attr(s, model.id_to_uri.pref_rep))
+    local pref_rep = get_single_v(s, "pref_rep")
+            or get_single_v(s, "has_member")
 
-    for p, ots in pairs(attrs) do
+    for p, ots_it in attrs:iter() do
         local pname = model.uri_to_id[nsm.denormalize_uri(p.data)] or p.data
         logger:debug("DRES pname: " .. pname)
         local pconf = ((mconf.properties or NT)[pname] or NT)
@@ -263,7 +257,7 @@ local function generate_dres(s, mconf)
         if pname == pkar.RDF_TYPE.data then goto skip end
         if pname == "first" then
             -- Build a linked list for every first found.
-            for _, o in pairs(ots) do
+            for o in ots_it do
                 -- Loop through all first children.
                 local child_s = o
                 if not pref_rep then pref_rep = child_s end
@@ -272,41 +266,34 @@ local function generate_dres(s, mconf)
                 local ll = {}
 
                 -- Fallback labels.
-                local label, ref
-                _, ref = next(repo.gr:attr(child_s, model.id_to_uri.ref))
+                local label
+                local ref = get_single_v(child_s, "ref")
                 if ref then
-                    _, label = next(repo.gr:attr(ref, model.id_to_uri.label))
+                    label = (get_single_v(ref, "label") or NT).data
                 else
-                    _, label = next(
-                        repo.gr:attr(child_s, model.id_to_uri.label))
+                    label = (get_single_v(child_s, "label") or NT).data
                 end
-                if label then label = label.data
-                else
-                    _, label = next(repo.gr:attr(
-                            child_s, model.id_to_uri.source_path))
-                    if label then label = path.basename(label.data)
-                        else label = child_s.data end
+                if not label then
+                    label = (get_single_v(child_s, "source_path") or NT).data
+                    if label then label = path.basename(label)
+                    else label = child_s.data end
                 end
 
                 while child_s do
                     -- Loop trough all next nodes for each first child.
-                    local ref
-                    _, ref = next(repo.gr:attr(child_s, model.id_to_uri.ref))
-                    --dbg()
+                    local ref = get_single_v(child_s, "ref")
                     table.insert(ll, {
                         href = pkar.gen_pairtree("/res", ref.data, ".html", true),
                         label = label,
                         tn = get_tn_url(ref),
                     })
-                    -- There can only be one "next"
-                    _, child_s = next(
-                            repo.gr:attr(child_s, model.id_to_uri.next))
+                    child_s = get_single_v(child_s, "next")
                 end
                 table.insert(children, ll)
             end
         elseif pname == "next" then
             -- Sibling.
-            for _, o in pairs(ots) do ls_next = o.data break end
+            for o in ots_it do ls_next = o.data break end
         elseif pconf.type == "resource" then
             -- Relationship.
             rel[pname] = {
@@ -314,7 +301,7 @@ local function generate_dres(s, mconf)
                 description = pconf.description,
                 uri = pconf.uri,
             }
-            for _, o in pairs(ots) do
+            for o in ots_it do
                 table.insert(rel[pname], {
                     href = pkar.gen_pairtree("/res", o.data, ".html", true),
                     label = nsm.denormalize_uri(o.data),
@@ -328,7 +315,7 @@ local function generate_dres(s, mconf)
                 uri = pconf.uri,
             }
             -- TODO differentiate term types
-            for _, o in pairs(ots) do table.insert(attr, o.data) end
+            for o in ots_it do table.insert(attr, o.data) end
             table.sort(attr)
             if p == model.id_to_uri.label then title = attr[1] end
             table.insert(dmd, attr)
@@ -348,6 +335,7 @@ local function generate_dres(s, mconf)
     logger:debug("Children:", pp.write(children))
     logger:debug("Breadcrumbs:", pp.write(get_breadcrumbs(mconf)))
 
+    local pref_rep_url
     if pref_rep then
         pref_rep_url = pkar.gen_pairtree("/res", pref_rep.data, ".html", true)
         pref_rep_file = get_tn_url(pref_rep):gsub(TN_WEB_PATH, MEDIA_WEB_PATH)
@@ -389,14 +377,14 @@ local function generate_ores(s, mconf)
     local rel = {}
     -- Metadata
     local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
-    for p, ots in pairs(attrs) do
+    for p, ots_it in attrs:iter() do
         local pname = model.uri_to_id[nsm.denormalize_uri(p.data)] or p.data
         local pconf = ((mconf.properties or NT)[pname] or NT)
         -- RDF types are shown in in breadcrumbs.
         if pname == pkar.RDF_TYPE.data then goto skip end
         if pname == "next" then
             -- Sibling.
-            for _, o in pairs(ots) do ls_next = o.data break end
+            for o in ots_it do ls_next = o.data break end
         elseif pconf.type == "resource" then
             -- Relationship.
             rel[pname] = {
@@ -404,7 +392,7 @@ local function generate_ores(s, mconf)
                 description = pconf.description,
                 uri = pconf.uri,
             }
-            for _, o in pairs(ots) do
+            for o in ots_it do
                 table.insert(rel[pname], {
                     href = pkar.gen_pairtree("/res", o.data, ".html", true),
                     label = nsm.denormalize_uri(o.data),
@@ -418,7 +406,7 @@ local function generate_ores(s, mconf)
                 uri = pconf.uri,
             }
             -- TODO differentiate term types
-            for _, o in pairs(ots) do table.insert(techmd[pname], o.data) end
+            for o in ots_it do table.insert(techmd[pname], o.data) end
             table.sort(techmd[pname])
         end
         ::skip::
@@ -513,12 +501,12 @@ M.generate_search_idx = function(s, mconf)
     local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
     local fpath
 
-    for p, ots in pairs(attrs) do
+    for p, ots_it, ots_size in attrs:iter() do
         local pname
         if p == model.id_to_uri.content_type then goto skip end
         if p == model.id_to_uri.source_path then
             if mconf.types.file then
-                _, fpath = next(ots)
+                fpath = ots_it()
                 rrep.fname = path.basename(fpath.data)
             end
             goto skip
@@ -531,15 +519,13 @@ M.generate_search_idx = function(s, mconf)
 
         local attr
         -- Quick check if it's multi-valued
-        local o
-        if next(ots, next(ots)) then
+        if ots_size > 1 then
             attr = {}
-            for _, o in pairs(ots) do
+            for o in ots_it do
                 table.insert(attr, format_value(pname, o))
             end
         else
-            _, o = next(ots)
-            attr = format_value(pname, o)
+            attr = format_value(pname, ots_it())
         end
 
         rrep[pname] = attr  -- Add to search index.
@@ -560,12 +546,12 @@ M.generate_ll = function(s)
             content_type = mconf.id,
         },
     }
-    for p, ots in pairs(res_gr:connections(s, term.LINK_OUTBOUND)) do
+    for p, ots_it in pairs(res_gr:connections(s, term.LINK_OUTBOUND)) do
         local pname = model.uri_to_id[nsm.denormalize_uri(p.data)]
         --if p == pkar.RDF_TYPE then goto skip_p end
         if not pname then goto skip_p end
         if pname == "content_type" then goto skip_p end
-        for _, o in pairs (ots) do
+        for o in ots_it do
             -- Find a row where the pname slot has not been occupied.
             if (mconf.properties[pname] or {}).type == "resource" then
                 o = {data = o.data:gsub(nsm.get_ns("par"), "")}
@@ -591,7 +577,7 @@ end
 
 M.generate_resource = function(s)
     local res_type
-    _, res_type = next(repo.gr:attr(s, model.id_to_uri.content_type))
+    res_type = get_single_v(s, "content_type")
     local mconf = model.from_uri(res_type)
 
     -- Generate RDF/Turtle doc.
@@ -628,7 +614,7 @@ M.generate_resources = function()
     ofh:close()
 
     -- TODO parallelize
-    for _, s in pairs(subjects) do assert(M.generate_resource(s)) end
+    for s in subjects:iter() do assert(M.generate_resource(s)) end
 
     -- Close the open list brace in the JSON template after all the resources
     -- have been added.
@@ -655,19 +641,15 @@ M.generate_homepage = function()
         term.new_iriref_ns("pas:Artifact"), triple.POS_O
     )
     local i = 1
-    for _, s in pairs(s_ts) do
+    for s in s_ts:iter() do
         if i > (pkar.config.htmlgen.max_homepage_items or 10) then break end
-        local title, submitted
-        _, title = next(repo.gr:attr(s, model.id_to_uri.label))
-        _, submitted = next(repo.gr:attr(s, model.id_to_uri.submitted))
 
-        local obj = {
+        table.insert(idx_data.objects, {
             href = pkar.gen_pairtree("/res", s.data, ".html", true),
-            title = title,
-            submitted = submitted.data,
+            title = get_single_v(s, "label"),
+            submitted = get_single_v(s, "submitted").data,
             tn = get_tn_url(s),
-        }
-        table.insert(idx_data.objects, obj)
+        })
         i = i + 1
     end
     table.sort(
@@ -679,18 +661,13 @@ M.generate_homepage = function()
         pkar.RDF_TYPE, triple.POS_P,
         term.new_iriref_ns("pas:Collection"), triple.POS_O
     )
-    for _, s in pairs(s_ts) do
-        local title, submitted
-        _, title = next(repo.gr:attr(s, model.id_to_uri.label))
-        _, submitted = next(repo.gr:attr(s, model.id_to_uri.submitted))
-
-        local coll = {
+    for s in s_ts:iter() do
+        table.insert(idx_data.collections, {
             href = pkar.gen_pairtree("/res", s.data, ".html", true),
-            title = title,
-            submitted = submitted.data,
+            title = get_single_v(s, "label"),
+            submitted = get_single_v(s, "submitted").data,
             tn = get_tn_url(s),
-        }
-        table.insert(idx_data.collections, coll)
+        })
     end
     table.sort(
         idx_data.collections, function(a, b)

+ 7 - 9
src/repo.lua

@@ -10,8 +10,8 @@ local triple = require "volksdata.triple"
 local graph = require "volksdata.graph"
 
 local pkar = require "pocket_archive"
+local model = require "pocket_archive.model"
 local logger = pkar.logger
-local validator = require "pocket_archive.validator"
 
 
 -- "nil" table - for missing key fallback in chaining.
@@ -40,18 +40,16 @@ M.serialize_rsrc = function(s, format)
 end
 
 
+-- Return the first value of an assumed single-valued attribute.
+M.get_single_v = function(s, fname)
+    return M.gr:attr(s, model.id_to_uri[fname]):iter()()
+end
+
+
 M.store_updates = function(tmp_gr, s)
     -- TODO use a transaction when volksdata_lua supports it.
     logger:debug("Graph: ", tmp_gr:encode("ttl"))
 
-    local val_report = validator.validate(tmp_gr, s)
-    if val_report.max_level == "ERROR" then error(
-        "Validation raised errors: " .. pp.write(val_report))
-    elseif val_report.max_level == "WARN" then logger:warn(
-        "Validation raised warnings: " .. pp.write(val_report))
-    elseif val_report.max_level == "NOTICE" then logger:warn(
-        "Validation raised notices: " .. pp.write(val_report)) end
-
     local stored_gr = graph.new(pkar.store, term.DEFAULT_CTX)
 
     logger:debug("Removing stored triples.")

+ 14 - 1
src/submission.lua

@@ -35,6 +35,7 @@ local pkar = require "pocket_archive"
 local model = require "pocket_archive.model"
 local mc = require "pocket_archive.monocypher"
 local repo = require "pocket_archive.repo"
+local validator = require "pocket_archive.validator"
 
 local logger = pkar.logger
 local dbg = require "debugger"
@@ -356,7 +357,19 @@ M.deposit = function(sip)
         tstamp = os.date("!%Y-%m-%dT%TZ")
         rsrc.submitted = tstamp
         rsrc.last_modified = tstamp
-        repo.store_updates(M.rsrc_to_graph(rsrc))
+
+        local tmp_gr, s
+        tmp_gr, s = M.rsrc_to_graph(rsrc)
+
+        local val_report = validator.validate(tmp_gr, s)
+        if val_report.max_level == "ERROR" then error(
+            "Validation raised errors: " .. pp.write(val_report))
+        elseif val_report.max_level == "WARN" then logger:warn(
+            "Validation raised warnings: " .. pp.write(val_report))
+        elseif val_report.max_level == "NOTICE" then logger:warn(
+            "Validation raised notices: " .. pp.write(val_report)) end
+
+        repo.store_updates(tmp_gr, s)
     end
 
     -- Remove processing directory.

+ 6 - 8
src/validator.lua

@@ -16,7 +16,7 @@ local M = {}
 
 
 M.validate = function(gr, s)
-    _, ctype = next(gr:attr(s, model.id_to_uri.content_type))
+    ctype = gr:attr(s, model.id_to_uri.content_type):iter()() or NT
     local rmod = model.from_uri(ctype)
     if not rmod then error("No type definition for " .. ctype.data) end
 
@@ -30,29 +30,27 @@ M.validate = function(gr, s)
         values = gr:attr(s, model.id_to_uri[fname])
 
         -- Cardinality
-        local card = 0
-        for _, v in pairs(values) do card = card + 1 end
         if rules.min_cardinality or rules.max_cardinality then
             min_card = rules.min_cardinality or 0
-            if card < min_card then
+            if #values < min_card then
                 table.insert(report.errors, {
                     E_CARD,
                     ("Too few values for %s: expected %d, got %d"):format(
-                            fname, min_card, card)
+                            fname, min_card, #values)
                 })
             end
             max_card = rules.max_cardinality or math.huge
-            if card > max_card then
+            if #values > max_card then
                 table.insert(report.errors, {
                     E_CARD,
                     ("Too many values for %s: expected %d, got %d"):format(
-                            fname, max_card, card)
+                            fname, max_card, #values)
                 })
             end
         end
 
         -- From this point on, if there are no values, skip other criteria.
-        if card == 0 then goto skip_prop end
+        if #values == 0 then goto skip_prop end
 
         -- Type
         if rules.type then

+ 7 - 4
test/sample_submission/demo01/demo_collection/description.md

@@ -8,7 +8,7 @@ It contains numerous wonderous delicierous featurous such as:
 - Another bullet in the bullet list
 - And more free lines
 
-### Quote from an all-time favorite poem:
+### Quotes from some favorite poems:
 
 > There I saw one I knew, and stopped him, crying: 'Stetson!  
 > ‘You who were with me in the ships at Mylae!  
@@ -19,8 +19,11 @@ It contains numerous wonderous delicierous featurous such as:
 > ‘Or with his nails he’ll dig it up again!  
 > ‘You! hypocrite lecteur!—mon semblable,—mon frère!”
 
-### More *Unicode* characters
-
-ستيفانو كوسو كان هنا
+and:
 
+> مَا رَحْلُوا يَوْمَ بَانُوا الْبُزَّلَ الْعِيسَا  
+> &emsp;&emsp;إِلَّا وَقَدْ حَمَلُوا فِيهَا الطَّوَاوِيسَا  
+>   
+> مِنْ كُلِّ فَاتِكَةِ الْأَلْحَاظِ مَالِكَةٍ  
+> &emsp;&emsp;تَخَالُها فَوْقَ عَرْشِ الدُّرِّ بِلْقِيسَ  
 

+ 1 - 1
test/sample_submission/demo01/pkar_submission.csv

@@ -1,5 +1,5 @@
 "source_path","ext_id","content_type","label","alt_label","description","location","date","has_member","pref_rep","long_description"
-"demo_collection","coll0001","collection","My Demo Collection","My Beautiful  Collection","Some random stuff from my hard drive.",,2025-07-28,,"demo_collection/single_image/0685_04.jpg","demo_collection/description.md"
+"demo_collection","coll0001","collection","My Demo Collection","My Beautiful  Collection","Some random stuff from my hard drive.",,2025-07-28,,"demo_collection/demo_postcard/front/54321.jpg","demo_collection/description.md"
 ,,,,"My Aunt's Beautiful Collection","Old B/W photos.",,,,,
 ,,,,,"More description to demonstrate how multi-valued fields are filled.",,,,,
 ,,,,,"""id"" fields have been left blank to let the system auto-generate them.",,,,,