// N-Quads interchange for Snapshots (task 031): the RDF sibling of the// RCPG functions in serialize.go. WriteNQuads serializes a snapshot as a// deterministic N-Quads document under the urn:chickpeas: vocabulary;// ReadNQuads maps any N-Quads/N-Triples document -- ours or foreign --// into a Snapshot. Parsing and term escaping are github.com/freeeve/// libcodex/rdf.//// Write vocabulary:// - node subject: urn:chickpeas:node/<id> (decimal; sparse ids survive)// - label: <node> rdf:type urn:chickpeas:label/<enc(name)>// - node prop: <node> urn:chickpeas:prop/<enc(key)> <literal>// - rel: <u> urn:chickpeas:rel/<enc(type)> <v> urn:chickpeas:relid/<pos>// -- the graph term is the rel's identity, so parallel rels stay// distinct statements and rel properties have an anchor// - rel prop: <relid> urn:chickpeas:prop/<enc(key)> <literal>// - version: <urn:chickpeas:graph> <urn:chickpeas:version> "<v>"//// Read mapping (generic, with the chickpeas vocabulary as a special// spelling of each rule):// - rdf:type with an IRI object labels the subject (chickpeas label// prefix decoded, otherwise the local name after '#'/'/');// - a literal object becomes a property of the subject, first value// wins, typed from the xsd datatype;// - a resource object becomes a rel typed by the predicate's local// or decoded name;// - a non-default graph term tagging exactly one rel quad, never used// as a rel/label subject or object, anchors that rel: literal// statements about it attach as rel properties (the named-graph-// per-edge pattern);// - urn:chickpeas:node/<n> terms keep id n; other entities intern// sequentially in document order, and foreign IRI entities carry a// `uri` property (blank nodes don't).//// A bare node (no labels, props, or rels) appears in no quad and does// not survive a write/read cycle; RCPG remains the fidelity format.package chickpeasimport ()const ( nqNodePrefix = "urn:chickpeas:node/" nqLabelPrefix = "urn:chickpeas:label/" nqPropPrefix = "urn:chickpeas:prop/" nqRelPrefix = "urn:chickpeas:rel/" nqRelIDPrefix = "urn:chickpeas:relid/" nqGraphIRI = "urn:chickpeas:graph" nqVersionIRI = "urn:chickpeas:version" xsdInteger = "http://www.w3.org/2001/XMLSchema#integer" xsdDouble = "http://www.w3.org/2001/XMLSchema#double" xsdBoolean = "http://www.w3.org/2001/XMLSchema#boolean" nqFlushSize = 1 << 20)// nqUnescape decodes %XX sequences; malformed sequences pass through// verbatim.func nqUnescape( string) string {if !strings.ContainsRune(, '%') {return } := make([]byte, 0, len())for := 0; < len(); {if [] == '%' && +2 < len() {if , := nqHexVal([+1]), nqHexVal([+2]); >= 0 && >= 0 { = append(, byte(<<4|)) += 3continue } } = append(, []) ++ }returnstring()}func nqHexVal( byte) int {switch {case >= '0' && <= '9':returnint( - '0')case >= 'a' && <= 'f':returnint(-'a') + 10case >= 'A' && <= 'F':returnint(-'A') + 10 }return -1}// ReadNQuadsFile reads an N-Quads/N-Triples file (transparently// gunzipping) into a Snapshot via ReadNQuads.func ( string) (*Snapshot, error) { , := os.ReadFile()if != nil {returnnil, }returnReadNQuads()}// ReadNQuads builds a Snapshot from an N-Quads or N-Triples document// using the read mapping documented at the top of this file. Gzipped// input (the 1f 8b magic) is decompressed transparently.func ( []byte) (*Snapshot, error) {iflen() >= 2 && [0] == 0x1f && [1] == 0x8b { , := gzip.NewReader(bytes.NewReader())if != nil {returnnil, } , := io.ReadAll()if != nil {returnnil, }if := .Close(); != nil {returnnil, } = } , := rdf.ParseNQuadsShared()if != nil {returnnil, }returnbuildFromQuads(.Quads)}// Quad classification for buildFromQuads.const ( nqQSkip uint8 = iota nqQLabel nqQProp nqQRel)// buildFromQuads maps parsed quads into a Snapshot per the read mapping.func buildFromQuads( []rdf.Quad) (*Snapshot, error) {// Pass 1: classify each quad, count rel quads per named graph term, // and note which terms act as rel/label subjects or objects (those // can never anchor rel properties). := make([]uint8, len()) := map[rdf.Term]int{} := map[rdf.Term]bool{}varstringvarboolfor := range { := &[]switch {case .S.Kind == rdf.IRI && .S.Value == nqGraphIRI && .P.Value == nqVersionIRI && .O.Kind == rdf.Literal: [] = nqQSkip , = .O.Value, truecase .P.Value == rdf.TypeIRI && .O.Kind == rdf.IRI: [] = nqQLabel [.S] = truecase .O.Kind == rdf.Literal: [] = nqQPropdefault: [] = nqQRel [.S] = true [.O] = trueif .G != (rdf.Term{}) { [.G]++ } } } := map[rdf.Term]bool{}for , := range {if == 1 && ![] { [] = true } }// Pass 2: assign node ids. Native urn:chickpeas:node/<n> terms claim // their id first (spelling decides, independent of position); the // rest intern sequentially in document order, skipping claimed ids. := map[rdf.Term]NodeID{} := map[NodeID]bool{} := func( func( rdf.Term)) {for := range { := &[]switch [] {casenqQLabel: (.S)casenqQProp:if ![.S] { (.S) }casenqQRel: (.S) (.O) } } } (func( rdf.Term) {if , := []; {return }if , := nqNativeID(); { [] = [] = true } })varNodeID (func( rdf.Term) {if , := []; {return }for [] { ++ } [] = [] = true })// Pass 3: collect labels, then create nodes in ascending id order. // Foreign IRI entities carry their IRI as a `uri` property. := map[NodeID]map[string]bool{}for := range {if [] != nqQLabel {continue } := &[] := [.S]if [] == nil { [] = map[string]bool{} } [][nqDecodeName(.O.Value, nqLabelPrefix)] = true } := map[NodeID]string{}for , := range {if .Kind == rdf.IRI {if , := nqNativeID(); ! { [] = .Value } } } := make([]NodeID, 0, len())for := range { = append(, ) }slices.Sort() := NewBuilder(len(), len()) := map[uint64]bool{} := .InternPropertyKey("uri")for , := range { := make([]string, 0, len([]))for := range [] { = append(, ) }sort.Strings()if , := .AddNodeWithID(, ...); != nil {returnnil, }if , := []; {if := .SetPropByKey(, , ); != nil {returnnil, } [uint64()<<32|uint64()] = true } }// Pass 4: rels in document order, remembering which builder index // each anchoring graph term produced. := map[rdf.Term]int{}for := range {if [] != nqQRel {continue } := &[] , := .AddRel([.S], [.O], nqDecodeName(.P.Value, nqRelPrefix))if != nil {returnnil, }if [.G] { [.G] = } }// Pass 5: properties in document order, first value wins per // (entity, key); anchor subjects address their rel instead of a node. := map[uint64]bool{}for := range {if [] != nqQProp {continue } := &[] := .InternPropertyKey(nqDecodeName(.P.Value, nqPropPrefix)) := nqLiteralValue(.O)if [.S] { := [.S] := uint64()<<32 | uint64()if [] {continue } [] = trueif := .SetRelPropAt(, nqDecodeName(.P.Value, nqPropPrefix), ); != nil {returnnil, }continue } := [.S] := uint64()<<32 | uint64()if [] {continue } [] = trueif := .SetPropByKey(, , ); != nil {returnnil, } }if { .SetVersion() }return .Finalize(), nil}// nqNativeID matches urn:chickpeas:node/<decimal> and returns the id.func nqNativeID( rdf.Term) (NodeID, bool) {if .Kind != rdf.IRI || !strings.HasPrefix(.Value, nqNodePrefix) {return0, false } , := strconv.ParseUint(.Value[len(nqNodePrefix):], 10, 32)if != nil {return0, false }returnNodeID(), true}// nqDecodeName maps an IRI to a label/key/type name: the unescaped// remainder for the given chickpeas prefix, the local name otherwise.func nqDecodeName(, string) string {ifstrings.HasPrefix(, ) {returnnqUnescape([len():]) }returnnqLocalName()}// nqLocalName is the substring after the last '#', then after the last// '/' of that remainder (the SPB loader's local-name rule).func nqLocalName( string) string {if := strings.LastIndexByte(, '#'); >= 0 { = [+1:] }if := strings.LastIndexByte(, '/'); >= 0 { = [+1:] }return}// nqLiteralValue maps a literal to the most specific property type its// xsd datatype allows, falling back to the lexical string.func nqLiteralValue( rdf.Term) any {switchnqLocalName(.Datatype) {case"integer", "int", "long", "short", "byte","nonNegativeInteger", "positiveInteger","nonPositiveInteger", "negativeInteger","unsignedLong", "unsignedInt", "unsignedShort", "unsignedByte":if , := strconv.ParseInt(.Value, 10, 64); == nil {return }case"double", "float", "decimal":switch .Value {case"INF", "+INF":returnmath.Inf(1)case"-INF":returnmath.Inf(-1)case"NaN":returnmath.NaN() }if , := strconv.ParseFloat(.Value, 64); == nil {return }case"boolean":switch .Value {case"true":returntruecase"false":returnfalse } }return .Value}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.