@the-cascade-protocol/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +164 -0
- package/dist/consent/consent-filter.d.ts +21 -0
- package/dist/consent/consent-filter.d.ts.map +1 -0
- package/dist/consent/consent-filter.js +71 -0
- package/dist/consent/consent-filter.js.map +1 -0
- package/dist/consent/index.d.ts +2 -0
- package/dist/consent/index.d.ts.map +1 -0
- package/dist/consent/index.js +2 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/deserializer/index.d.ts +7 -0
- package/dist/deserializer/index.d.ts.map +1 -0
- package/dist/deserializer/index.js +7 -0
- package/dist/deserializer/index.js.map +1 -0
- package/dist/deserializer/turtle-parser.d.ts +47 -0
- package/dist/deserializer/turtle-parser.d.ts.map +1 -0
- package/dist/deserializer/turtle-parser.js +840 -0
- package/dist/deserializer/turtle-parser.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/jsonld/context.d.ts +29 -0
- package/dist/jsonld/context.d.ts.map +1 -0
- package/dist/jsonld/context.js +95 -0
- package/dist/jsonld/context.js.map +1 -0
- package/dist/jsonld/converter.d.ts +50 -0
- package/dist/jsonld/converter.d.ts.map +1 -0
- package/dist/jsonld/converter.js +139 -0
- package/dist/jsonld/converter.js.map +1 -0
- package/dist/jsonld/index.d.ts +8 -0
- package/dist/jsonld/index.d.ts.map +1 -0
- package/dist/jsonld/index.js +8 -0
- package/dist/jsonld/index.js.map +1 -0
- package/dist/models/activity-snapshot.d.ts +49 -0
- package/dist/models/activity-snapshot.d.ts.map +1 -0
- package/dist/models/activity-snapshot.js +13 -0
- package/dist/models/activity-snapshot.js.map +1 -0
- package/dist/models/allergy.d.ts +49 -0
- package/dist/models/allergy.d.ts.map +1 -0
- package/dist/models/allergy.js +13 -0
- package/dist/models/allergy.js.map +1 -0
- package/dist/models/common.d.ts +174 -0
- package/dist/models/common.d.ts.map +1 -0
- package/dist/models/common.js +12 -0
- package/dist/models/common.js.map +1 -0
- package/dist/models/condition.d.ts +59 -0
- package/dist/models/condition.d.ts.map +1 -0
- package/dist/models/condition.js +13 -0
- package/dist/models/condition.js.map +1 -0
- package/dist/models/coverage.d.ts +117 -0
- package/dist/models/coverage.d.ts.map +1 -0
- package/dist/models/coverage.js +16 -0
- package/dist/models/coverage.js.map +1 -0
- package/dist/models/family-history.d.ts +38 -0
- package/dist/models/family-history.d.ts.map +1 -0
- package/dist/models/family-history.js +13 -0
- package/dist/models/family-history.js.map +1 -0
- package/dist/models/health-profile.d.ts +54 -0
- package/dist/models/health-profile.d.ts.map +1 -0
- package/dist/models/health-profile.js +11 -0
- package/dist/models/health-profile.js.map +1 -0
- package/dist/models/immunization.d.ts +78 -0
- package/dist/models/immunization.d.ts.map +1 -0
- package/dist/models/immunization.js +12 -0
- package/dist/models/immunization.js.map +1 -0
- package/dist/models/index.d.ts +20 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +7 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/lab-result.d.ts +83 -0
- package/dist/models/lab-result.d.ts.map +1 -0
- package/dist/models/lab-result.js +12 -0
- package/dist/models/lab-result.js.map +1 -0
- package/dist/models/medication.d.ts +144 -0
- package/dist/models/medication.d.ts.map +1 -0
- package/dist/models/medication.js +13 -0
- package/dist/models/medication.js.map +1 -0
- package/dist/models/patient-profile.d.ts +171 -0
- package/dist/models/patient-profile.d.ts.map +1 -0
- package/dist/models/patient-profile.js +14 -0
- package/dist/models/patient-profile.js.map +1 -0
- package/dist/models/procedure.d.ts +53 -0
- package/dist/models/procedure.d.ts.map +1 -0
- package/dist/models/procedure.js +12 -0
- package/dist/models/procedure.js.map +1 -0
- package/dist/models/sleep-snapshot.d.ts +54 -0
- package/dist/models/sleep-snapshot.d.ts.map +1 -0
- package/dist/models/sleep-snapshot.js +13 -0
- package/dist/models/sleep-snapshot.js.map +1 -0
- package/dist/models/vital-sign.d.ts +74 -0
- package/dist/models/vital-sign.d.ts.map +1 -0
- package/dist/models/vital-sign.js +13 -0
- package/dist/models/vital-sign.js.map +1 -0
- package/dist/pod/index.d.ts +2 -0
- package/dist/pod/index.d.ts.map +1 -0
- package/dist/pod/index.js +2 -0
- package/dist/pod/index.js.map +1 -0
- package/dist/pod/pod-builder.d.ts +63 -0
- package/dist/pod/pod-builder.d.ts.map +1 -0
- package/dist/pod/pod-builder.js +245 -0
- package/dist/pod/pod-builder.js.map +1 -0
- package/dist/serializer/index.d.ts +8 -0
- package/dist/serializer/index.d.ts.map +1 -0
- package/dist/serializer/index.js +8 -0
- package/dist/serializer/index.js.map +1 -0
- package/dist/serializer/turtle-builder.d.ts +93 -0
- package/dist/serializer/turtle-builder.d.ts.map +1 -0
- package/dist/serializer/turtle-builder.js +204 -0
- package/dist/serializer/turtle-builder.js.map +1 -0
- package/dist/serializer/turtle-serializer.d.ts +66 -0
- package/dist/serializer/turtle-serializer.d.ts.map +1 -0
- package/dist/serializer/turtle-serializer.js +404 -0
- package/dist/serializer/turtle-serializer.js.map +1 -0
- package/dist/validator/index.d.ts +2 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +2 -0
- package/dist/validator/index.js.map +1 -0
- package/dist/validator/validator.d.ts +16 -0
- package/dist/validator/validator.d.ts.map +1 -0
- package/dist/validator/validator.js +295 -0
- package/dist/validator/validator.js.map +1 -0
- package/dist/vocabularies/index.d.ts +8 -0
- package/dist/vocabularies/index.d.ts.map +1 -0
- package/dist/vocabularies/index.js +7 -0
- package/dist/vocabularies/index.js.map +1 -0
- package/dist/vocabularies/namespaces.d.ts +125 -0
- package/dist/vocabularies/namespaces.d.ts.map +1 -0
- package/dist/vocabularies/namespaces.js +361 -0
- package/dist/vocabularies/namespaces.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency Turtle parser for deserializing Cascade Protocol records.
|
|
3
|
+
*
|
|
4
|
+
* Uses regex-based parsing to convert Turtle (Terse RDF Triple Language)
|
|
5
|
+
* content back into typed Cascade Protocol model objects.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - @prefix declarations
|
|
9
|
+
* - Subject-predicate-object triples
|
|
10
|
+
* - Typed literals (xsd:dateTime, xsd:date, xsd:integer, xsd:double)
|
|
11
|
+
* - URI references (angle-bracket and prefixed forms)
|
|
12
|
+
* - Boolean literals
|
|
13
|
+
* - RDF lists `( item1 item2 ... )`
|
|
14
|
+
* - Blank nodes `[ ... ]`
|
|
15
|
+
* - Triple-quoted long literals
|
|
16
|
+
* - Multi-value predicates (repeated predicate with different objects)
|
|
17
|
+
*
|
|
18
|
+
* @module deserializer
|
|
19
|
+
*/
|
|
20
|
+
import { NAMESPACES, TYPE_MAPPING, buildReversePredicateMap } from '../vocabularies/namespaces.js';
|
|
21
|
+
// ─── Reverse Predicate Mapping ──────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Type-specific predicate overrides for deserialization.
|
|
24
|
+
* These map full predicate URIs to JSON property names for cases where
|
|
25
|
+
* the same local name is used in different namespaces depending on record type.
|
|
26
|
+
*/
|
|
27
|
+
const ADDITIONAL_REVERSE_MAPPINGS = {
|
|
28
|
+
// VitalSign uses clinical: namespace for these predicates
|
|
29
|
+
[`${NAMESPACES.clinical}snomedCode`]: 'snomedCode',
|
|
30
|
+
[`${NAMESPACES.clinical}interpretation`]: 'interpretation',
|
|
31
|
+
};
|
|
32
|
+
const REVERSE_PREDICATE_MAP = buildReversePredicateMap(ADDITIONAL_REVERSE_MAPPINGS);
|
|
33
|
+
/**
|
|
34
|
+
* Build a reverse mapping from RDF type URI to record type string.
|
|
35
|
+
*/
|
|
36
|
+
function buildReverseTypeMap() {
|
|
37
|
+
const reverseMap = new Map();
|
|
38
|
+
for (const [key, mapping] of Object.entries(TYPE_MAPPING)) {
|
|
39
|
+
const colonIdx = mapping.rdfType.indexOf(':');
|
|
40
|
+
if (colonIdx >= 0) {
|
|
41
|
+
const nsPrefix = mapping.rdfType.slice(0, colonIdx);
|
|
42
|
+
const localName = mapping.rdfType.slice(colonIdx + 1);
|
|
43
|
+
const nsUri = NAMESPACES[nsPrefix];
|
|
44
|
+
if (nsUri) {
|
|
45
|
+
reverseMap.set(`${nsUri}${localName}`, { recordType: localName, mappingKey: key });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return reverseMap;
|
|
50
|
+
}
|
|
51
|
+
const REVERSE_TYPE_MAP = buildReverseTypeMap();
|
|
52
|
+
// ─── Fields requiring special type conversion ───────────────────────────────
|
|
53
|
+
/** Fields that are booleans */
|
|
54
|
+
const BOOLEAN_FIELDS = new Set([
|
|
55
|
+
'isActive', 'asNeeded',
|
|
56
|
+
]);
|
|
57
|
+
/** Fields that are numbers (integers) */
|
|
58
|
+
const INTEGER_TYPE_FIELDS = new Set([
|
|
59
|
+
'computedAge', 'refillsAllowed', 'supplyDurationDays', 'onsetAge',
|
|
60
|
+
'steps', 'activeMinutes', 'calories', 'awakenings',
|
|
61
|
+
'totalSleepMinutes', 'deepSleepMinutes', 'remSleepMinutes', 'lightSleepMinutes',
|
|
62
|
+
]);
|
|
63
|
+
/** Fields that are numbers (possibly float) */
|
|
64
|
+
const NUMBER_FIELDS = new Set([
|
|
65
|
+
'value', 'referenceRangeLow', 'referenceRangeHigh',
|
|
66
|
+
'distance',
|
|
67
|
+
]);
|
|
68
|
+
/** Fields that hold arrays of strings */
|
|
69
|
+
const ARRAY_TYPE_FIELDS = new Set([
|
|
70
|
+
'drugCodes', 'affectsVitalSigns', 'monitoredVitalSigns',
|
|
71
|
+
]);
|
|
72
|
+
// ─── Turtle Tokenizer / Parser ──────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Parse @prefix declarations from Turtle content.
|
|
75
|
+
*/
|
|
76
|
+
function parsePrefixes(content) {
|
|
77
|
+
const prefixes = [];
|
|
78
|
+
const regex = /@prefix\s+(\w+):\s+<([^>]+)>\s*\./g;
|
|
79
|
+
let match;
|
|
80
|
+
while ((match = regex.exec(content)) !== null) {
|
|
81
|
+
prefixes.push({ prefix: match[1] ?? '', uri: match[2] ?? '' });
|
|
82
|
+
}
|
|
83
|
+
return prefixes;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Expand a prefixed name (e.g., "health:medicationName") to a full URI
|
|
87
|
+
* using the parsed prefix declarations.
|
|
88
|
+
*/
|
|
89
|
+
function expandPrefixedName(name, prefixMap) {
|
|
90
|
+
// Already a full URI
|
|
91
|
+
if (name.startsWith('http://') || name.startsWith('https://') || name.startsWith('urn:')) {
|
|
92
|
+
return name;
|
|
93
|
+
}
|
|
94
|
+
const colonIdx = name.indexOf(':');
|
|
95
|
+
if (colonIdx < 0)
|
|
96
|
+
return name;
|
|
97
|
+
const prefix = name.slice(0, colonIdx);
|
|
98
|
+
const local = name.slice(colonIdx + 1);
|
|
99
|
+
const nsUri = prefixMap.get(prefix);
|
|
100
|
+
if (nsUri) {
|
|
101
|
+
return `${nsUri}${local}`;
|
|
102
|
+
}
|
|
103
|
+
return name;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Remove Turtle comments (# to end of line) while respecting
|
|
107
|
+
* angle-bracket URIs and quoted strings where # is a literal character.
|
|
108
|
+
*/
|
|
109
|
+
function removeComments(content) {
|
|
110
|
+
let result = '';
|
|
111
|
+
let inString = false;
|
|
112
|
+
let inTripleQuote = false;
|
|
113
|
+
let inUri = false;
|
|
114
|
+
for (let i = 0; i < content.length; i++) {
|
|
115
|
+
const ch = content[i];
|
|
116
|
+
// Handle triple-quoted strings
|
|
117
|
+
if (!inString && !inTripleQuote && !inUri && content.slice(i, i + 3) === '"""') {
|
|
118
|
+
inTripleQuote = true;
|
|
119
|
+
result += '"""';
|
|
120
|
+
i += 2;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (inTripleQuote) {
|
|
124
|
+
if (content.slice(i, i + 3) === '"""' && (i === 0 || content[i - 1] !== '\\')) {
|
|
125
|
+
inTripleQuote = false;
|
|
126
|
+
result += '"""';
|
|
127
|
+
i += 2;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
result += ch;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
// Handle regular strings
|
|
134
|
+
if (ch === '"' && !inString && !inUri) {
|
|
135
|
+
inString = true;
|
|
136
|
+
result += ch;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === '"' && inString && (i === 0 || content[i - 1] !== '\\')) {
|
|
140
|
+
inString = false;
|
|
141
|
+
result += ch;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (inString) {
|
|
145
|
+
result += ch;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Handle angle-bracket URIs
|
|
149
|
+
if (ch === '<' && !inUri) {
|
|
150
|
+
inUri = true;
|
|
151
|
+
result += ch;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (ch === '>' && inUri) {
|
|
155
|
+
inUri = false;
|
|
156
|
+
result += ch;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (inUri) {
|
|
160
|
+
result += ch;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// At top-level: # starts a comment to end of line
|
|
164
|
+
if (ch === '#') {
|
|
165
|
+
// Skip until newline
|
|
166
|
+
while (i < content.length && content[i] !== '\n') {
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
// Include the newline if present
|
|
170
|
+
if (i < content.length && content[i] === '\n') {
|
|
171
|
+
result += '\n';
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
result += ch;
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Unescape a Turtle string literal (handle \\, \", \n, \r, \t).
|
|
181
|
+
*/
|
|
182
|
+
function unescapeTurtleLiteral(value) {
|
|
183
|
+
return value
|
|
184
|
+
.replace(/\\t/g, '\t')
|
|
185
|
+
.replace(/\\r/g, '\r')
|
|
186
|
+
.replace(/\\n/g, '\n')
|
|
187
|
+
.replace(/\\"/g, '"')
|
|
188
|
+
.replace(/\\\\/g, '\\');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Strip surrounding angle brackets from a URI.
|
|
192
|
+
*/
|
|
193
|
+
function stripAngleBrackets(uri) {
|
|
194
|
+
if (uri.startsWith('<') && uri.endsWith('>')) {
|
|
195
|
+
return uri.slice(1, -1);
|
|
196
|
+
}
|
|
197
|
+
return uri;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Parse Turtle content into a list of parsed triples.
|
|
201
|
+
*
|
|
202
|
+
* This is a lightweight regex-based parser that handles the subset of Turtle
|
|
203
|
+
* used by Cascade Protocol records. It does NOT implement a full Turtle grammar.
|
|
204
|
+
*/
|
|
205
|
+
function parseTurtleContent(content) {
|
|
206
|
+
const prefixes = parsePrefixes(content);
|
|
207
|
+
const prefixMap = new Map();
|
|
208
|
+
for (const p of prefixes) {
|
|
209
|
+
prefixMap.set(p.prefix, p.uri);
|
|
210
|
+
}
|
|
211
|
+
// Add well-known prefixes as fallback
|
|
212
|
+
prefixMap.set('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
|
|
213
|
+
prefixMap.set('xsd', NAMESPACES.xsd);
|
|
214
|
+
const triples = [];
|
|
215
|
+
// Remove prefix declarations
|
|
216
|
+
let body = content
|
|
217
|
+
.replace(/@prefix\s+\w+:\s+<[^>]+>\s*\.\s*/g, '');
|
|
218
|
+
// Remove comments (# to end of line) but NOT when # is inside <...> URIs or "..." strings
|
|
219
|
+
body = removeComments(body).trim();
|
|
220
|
+
// Parse subject blocks: <subject> predicate-list .
|
|
221
|
+
// Match subject URIs or prefixed names
|
|
222
|
+
const subjectRegex = /(<[^>]+>|[a-zA-Z][\w-]*:[\w-]+)\s+/;
|
|
223
|
+
while (body.length > 0) {
|
|
224
|
+
body = body.trim();
|
|
225
|
+
if (body.length === 0)
|
|
226
|
+
break;
|
|
227
|
+
// Match subject
|
|
228
|
+
const subMatch = subjectRegex.exec(body);
|
|
229
|
+
if (!subMatch)
|
|
230
|
+
break;
|
|
231
|
+
let subject = subMatch[1] ?? '';
|
|
232
|
+
if (subject.startsWith('<') && subject.endsWith('>')) {
|
|
233
|
+
subject = subject.slice(1, -1);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
subject = expandPrefixedName(subject, prefixMap);
|
|
237
|
+
}
|
|
238
|
+
// Find the predicate-object list (everything until the closing '.')
|
|
239
|
+
let startIdx = (subMatch.index ?? 0) + subMatch[0].length;
|
|
240
|
+
const predicateObjects = extractPredicateObjectList(body, startIdx);
|
|
241
|
+
if (!predicateObjects)
|
|
242
|
+
break;
|
|
243
|
+
body = body.slice(predicateObjects.endIndex).trim();
|
|
244
|
+
// Parse each predicate-object pair
|
|
245
|
+
parsePredicateObjectPairs(subject, predicateObjects.content, prefixMap, triples);
|
|
246
|
+
}
|
|
247
|
+
return { prefixes, triples };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Extract the predicate-object list from the current position in the body,
|
|
251
|
+
* handling nested blank nodes and lists.
|
|
252
|
+
*/
|
|
253
|
+
function extractPredicateObjectList(body, startIdx) {
|
|
254
|
+
let depth = 0; // for nested [] and ()
|
|
255
|
+
let i = startIdx;
|
|
256
|
+
let inString = false;
|
|
257
|
+
let inTripleQuote = false;
|
|
258
|
+
let inUri = false; // for <...> URI delimiters
|
|
259
|
+
let prevChar = '';
|
|
260
|
+
while (i < body.length) {
|
|
261
|
+
const ch = body[i];
|
|
262
|
+
// Handle triple-quoted strings
|
|
263
|
+
if (!inString && !inTripleQuote && !inUri && body.slice(i, i + 3) === '"""') {
|
|
264
|
+
inTripleQuote = true;
|
|
265
|
+
i += 3;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (inTripleQuote) {
|
|
269
|
+
if (body.slice(i, i + 3) === '"""' && prevChar !== '\\') {
|
|
270
|
+
inTripleQuote = false;
|
|
271
|
+
i += 3;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
prevChar = ch ?? '';
|
|
275
|
+
i++;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
// Handle regular quoted strings
|
|
279
|
+
if (ch === '"' && !inString && !inUri && prevChar !== '\\') {
|
|
280
|
+
inString = true;
|
|
281
|
+
i++;
|
|
282
|
+
prevChar = ch;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (ch === '"' && inString && prevChar !== '\\') {
|
|
286
|
+
inString = false;
|
|
287
|
+
i++;
|
|
288
|
+
prevChar = ch;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (inString) {
|
|
292
|
+
prevChar = ch ?? '';
|
|
293
|
+
i++;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// Handle angle-bracket URIs <...>
|
|
297
|
+
if (ch === '<' && !inUri) {
|
|
298
|
+
inUri = true;
|
|
299
|
+
i++;
|
|
300
|
+
prevChar = ch;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (ch === '>' && inUri) {
|
|
304
|
+
inUri = false;
|
|
305
|
+
i++;
|
|
306
|
+
prevChar = ch;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (inUri) {
|
|
310
|
+
prevChar = ch ?? '';
|
|
311
|
+
i++;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (ch === '[' || ch === '(')
|
|
315
|
+
depth++;
|
|
316
|
+
if (ch === ']' || ch === ')')
|
|
317
|
+
depth--;
|
|
318
|
+
if (ch === '.' && depth === 0) {
|
|
319
|
+
// Check if this dot is followed by whitespace or end-of-string
|
|
320
|
+
// to distinguish from dots in prefixed names (e.g., "foaf:name")
|
|
321
|
+
const nextChar = i + 1 < body.length ? body[i + 1] : '';
|
|
322
|
+
if (nextChar === '' || nextChar === '\n' || nextChar === '\r' || nextChar === ' ' || nextChar === '\t') {
|
|
323
|
+
return { content: body.slice(startIdx, i).trim(), endIndex: i + 1 };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
prevChar = ch ?? '';
|
|
327
|
+
i++;
|
|
328
|
+
}
|
|
329
|
+
// If we reach the end without a dot, treat the rest as the content
|
|
330
|
+
if (body.slice(startIdx).trim().length > 0) {
|
|
331
|
+
return { content: body.slice(startIdx).trim(), endIndex: body.length };
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Parse semicolon-separated predicate-object pairs.
|
|
337
|
+
*/
|
|
338
|
+
function parsePredicateObjectPairs(subject, content, prefixMap, triples) {
|
|
339
|
+
// Split on ';' that are not inside strings, brackets, or parens
|
|
340
|
+
const pairs = splitOnSemicolon(content);
|
|
341
|
+
for (const pair of pairs) {
|
|
342
|
+
const trimmed = pair.trim();
|
|
343
|
+
if (trimmed.length === 0)
|
|
344
|
+
continue;
|
|
345
|
+
// Handle "a <type>" shorthand
|
|
346
|
+
if (trimmed.startsWith('a ')) {
|
|
347
|
+
const typeValue = trimmed.slice(2).trim();
|
|
348
|
+
const expandedType = typeValue.startsWith('<')
|
|
349
|
+
? stripAngleBrackets(typeValue)
|
|
350
|
+
: expandPrefixedName(typeValue, prefixMap);
|
|
351
|
+
triples.push({
|
|
352
|
+
subject,
|
|
353
|
+
predicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
|
|
354
|
+
object: expandedType,
|
|
355
|
+
objectType: 'uri',
|
|
356
|
+
});
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
// Split predicate and object(s)
|
|
360
|
+
const spaceIdx = findFirstWhitespace(trimmed);
|
|
361
|
+
if (spaceIdx < 0)
|
|
362
|
+
continue;
|
|
363
|
+
const predStr = trimmed.slice(0, spaceIdx).trim();
|
|
364
|
+
const objStr = trimmed.slice(spaceIdx + 1).trim();
|
|
365
|
+
const predUri = predStr.startsWith('<')
|
|
366
|
+
? stripAngleBrackets(predStr)
|
|
367
|
+
: expandPrefixedName(predStr, prefixMap);
|
|
368
|
+
// Parse the object value
|
|
369
|
+
parseObjectValue(subject, predUri, objStr, prefixMap, triples);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Find the first whitespace character that is not inside brackets or strings.
|
|
374
|
+
*/
|
|
375
|
+
function findFirstWhitespace(str) {
|
|
376
|
+
let inQuote = false;
|
|
377
|
+
for (let i = 0; i < str.length; i++) {
|
|
378
|
+
const ch = str[i];
|
|
379
|
+
if (ch === '"' && (i === 0 || str[i - 1] !== '\\')) {
|
|
380
|
+
inQuote = !inQuote;
|
|
381
|
+
}
|
|
382
|
+
if (!inQuote && (ch === ' ' || ch === '\t' || ch === '\n')) {
|
|
383
|
+
return i;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return -1;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Split a string on ';' characters that are not inside strings, brackets, or parens.
|
|
390
|
+
*/
|
|
391
|
+
function splitOnSemicolon(content) {
|
|
392
|
+
const result = [];
|
|
393
|
+
let depth = 0;
|
|
394
|
+
let inString = false;
|
|
395
|
+
let inTripleQuote = false;
|
|
396
|
+
let inUri = false;
|
|
397
|
+
let current = '';
|
|
398
|
+
for (let i = 0; i < content.length; i++) {
|
|
399
|
+
const ch = content[i];
|
|
400
|
+
// Handle triple-quoted strings
|
|
401
|
+
if (!inString && !inTripleQuote && !inUri && content.slice(i, i + 3) === '"""') {
|
|
402
|
+
inTripleQuote = true;
|
|
403
|
+
current += '"""';
|
|
404
|
+
i += 2;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (inTripleQuote) {
|
|
408
|
+
if (content.slice(i, i + 3) === '"""' && (i === 0 || content[i - 1] !== '\\')) {
|
|
409
|
+
inTripleQuote = false;
|
|
410
|
+
current += '"""';
|
|
411
|
+
i += 2;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
current += ch;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (ch === '"' && !inString && !inUri && (i === 0 || content[i - 1] !== '\\')) {
|
|
418
|
+
inString = true;
|
|
419
|
+
current += ch;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (ch === '"' && inString && (i === 0 || content[i - 1] !== '\\')) {
|
|
423
|
+
inString = false;
|
|
424
|
+
current += ch;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (inString) {
|
|
428
|
+
current += ch;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
// Handle angle-bracket URIs <...>
|
|
432
|
+
if (ch === '<' && !inUri) {
|
|
433
|
+
inUri = true;
|
|
434
|
+
current += ch;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (ch === '>' && inUri) {
|
|
438
|
+
inUri = false;
|
|
439
|
+
current += ch;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (inUri) {
|
|
443
|
+
current += ch;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (ch === '[' || ch === '(')
|
|
447
|
+
depth++;
|
|
448
|
+
if (ch === ']' || ch === ')')
|
|
449
|
+
depth--;
|
|
450
|
+
if (ch === ';' && depth === 0) {
|
|
451
|
+
result.push(current);
|
|
452
|
+
current = '';
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
current += ch;
|
|
456
|
+
}
|
|
457
|
+
if (current.trim().length > 0) {
|
|
458
|
+
result.push(current);
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Parse an object value string into the appropriate type and add a triple.
|
|
464
|
+
*/
|
|
465
|
+
function parseObjectValue(subject, predicate, objStr, prefixMap, triples) {
|
|
466
|
+
const trimmed = objStr.trim();
|
|
467
|
+
// Boolean literals
|
|
468
|
+
if (trimmed === 'true' || trimmed === 'false') {
|
|
469
|
+
triples.push({
|
|
470
|
+
subject,
|
|
471
|
+
predicate,
|
|
472
|
+
object: trimmed,
|
|
473
|
+
objectType: 'boolean',
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// URI reference (angle brackets)
|
|
478
|
+
if (trimmed.startsWith('<') && trimmed.endsWith('>')) {
|
|
479
|
+
triples.push({
|
|
480
|
+
subject,
|
|
481
|
+
predicate,
|
|
482
|
+
object: stripAngleBrackets(trimmed),
|
|
483
|
+
objectType: 'uri',
|
|
484
|
+
});
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// Prefixed name (e.g., cascade:ClinicalGenerated)
|
|
488
|
+
if (/^[a-zA-Z][\w-]*:[\w-]+$/.test(trimmed)) {
|
|
489
|
+
triples.push({
|
|
490
|
+
subject,
|
|
491
|
+
predicate,
|
|
492
|
+
object: expandPrefixedName(trimmed, prefixMap),
|
|
493
|
+
objectType: 'uri',
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Plain integer (no quotes)
|
|
498
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
499
|
+
triples.push({
|
|
500
|
+
subject,
|
|
501
|
+
predicate,
|
|
502
|
+
object: trimmed,
|
|
503
|
+
objectType: 'integer',
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// Plain double (no quotes, has decimal)
|
|
508
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) {
|
|
509
|
+
triples.push({
|
|
510
|
+
subject,
|
|
511
|
+
predicate,
|
|
512
|
+
object: trimmed,
|
|
513
|
+
objectType: 'double',
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// RDF list ( item1 item2 ... )
|
|
518
|
+
if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
|
|
519
|
+
const listContent = trimmed.slice(1, -1).trim();
|
|
520
|
+
// Parse list items
|
|
521
|
+
const items = parseListItems(listContent);
|
|
522
|
+
triples.push({
|
|
523
|
+
subject,
|
|
524
|
+
predicate,
|
|
525
|
+
object: JSON.stringify(items),
|
|
526
|
+
objectType: 'list',
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// Blank node [ ... ]
|
|
531
|
+
if (trimmed.startsWith('[')) {
|
|
532
|
+
// For blank nodes, we store the inner content as the object
|
|
533
|
+
// and parse it recursively
|
|
534
|
+
const bnodeId = `_:b${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
|
|
535
|
+
triples.push({
|
|
536
|
+
subject,
|
|
537
|
+
predicate,
|
|
538
|
+
object: bnodeId,
|
|
539
|
+
objectType: 'blankNode',
|
|
540
|
+
});
|
|
541
|
+
// Extract inner content between [ and ]
|
|
542
|
+
const innerStart = trimmed.indexOf('[') + 1;
|
|
543
|
+
const innerEnd = trimmed.lastIndexOf(']');
|
|
544
|
+
if (innerEnd > innerStart) {
|
|
545
|
+
const inner = trimmed.slice(innerStart, innerEnd).trim();
|
|
546
|
+
parsePredicateObjectPairs(bnodeId, inner, prefixMap, triples);
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// Triple-quoted string literal """..."""
|
|
551
|
+
if (trimmed.startsWith('"""')) {
|
|
552
|
+
const endIdx = trimmed.indexOf('"""', 3);
|
|
553
|
+
if (endIdx >= 0) {
|
|
554
|
+
const value = trimmed.slice(3, endIdx);
|
|
555
|
+
const afterQuote = trimmed.slice(endIdx + 3).trim();
|
|
556
|
+
let datatype;
|
|
557
|
+
if (afterQuote.startsWith('^^')) {
|
|
558
|
+
const dtStr = afterQuote.slice(2);
|
|
559
|
+
datatype = dtStr.startsWith('<')
|
|
560
|
+
? stripAngleBrackets(dtStr)
|
|
561
|
+
: expandPrefixedName(dtStr, prefixMap);
|
|
562
|
+
}
|
|
563
|
+
triples.push({
|
|
564
|
+
subject,
|
|
565
|
+
predicate,
|
|
566
|
+
object: unescapeTurtleLiteral(value),
|
|
567
|
+
objectType: 'literal',
|
|
568
|
+
datatype,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
// Quoted string literal "..."
|
|
574
|
+
if (trimmed.startsWith('"')) {
|
|
575
|
+
// Find matching closing quote (not escaped)
|
|
576
|
+
let endQuoteIdx = -1;
|
|
577
|
+
for (let i = 1; i < trimmed.length; i++) {
|
|
578
|
+
if (trimmed[i] === '"' && trimmed[i - 1] !== '\\') {
|
|
579
|
+
endQuoteIdx = i;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (endQuoteIdx >= 0) {
|
|
584
|
+
const value = trimmed.slice(1, endQuoteIdx);
|
|
585
|
+
const afterQuote = trimmed.slice(endQuoteIdx + 1).trim();
|
|
586
|
+
let datatype;
|
|
587
|
+
if (afterQuote.startsWith('^^')) {
|
|
588
|
+
const dtStr = afterQuote.slice(2);
|
|
589
|
+
datatype = dtStr.startsWith('<')
|
|
590
|
+
? stripAngleBrackets(dtStr)
|
|
591
|
+
: expandPrefixedName(dtStr, prefixMap);
|
|
592
|
+
}
|
|
593
|
+
triples.push({
|
|
594
|
+
subject,
|
|
595
|
+
predicate,
|
|
596
|
+
object: unescapeTurtleLiteral(value),
|
|
597
|
+
objectType: 'literal',
|
|
598
|
+
datatype,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
// Fallback: treat as literal
|
|
604
|
+
triples.push({
|
|
605
|
+
subject,
|
|
606
|
+
predicate,
|
|
607
|
+
object: trimmed,
|
|
608
|
+
objectType: 'literal',
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Parse items from an RDF list, handling quoted strings.
|
|
613
|
+
*/
|
|
614
|
+
function parseListItems(content) {
|
|
615
|
+
const items = [];
|
|
616
|
+
let remaining = content.trim();
|
|
617
|
+
while (remaining.length > 0) {
|
|
618
|
+
remaining = remaining.trim();
|
|
619
|
+
if (remaining.length === 0)
|
|
620
|
+
break;
|
|
621
|
+
if (remaining.startsWith('"')) {
|
|
622
|
+
// Find closing quote
|
|
623
|
+
let endIdx = -1;
|
|
624
|
+
for (let i = 1; i < remaining.length; i++) {
|
|
625
|
+
if (remaining[i] === '"' && remaining[i - 1] !== '\\') {
|
|
626
|
+
endIdx = i;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (endIdx >= 0) {
|
|
631
|
+
items.push(unescapeTurtleLiteral(remaining.slice(1, endIdx)));
|
|
632
|
+
remaining = remaining.slice(endIdx + 1).trim();
|
|
633
|
+
// Skip optional datatype
|
|
634
|
+
if (remaining.startsWith('^^')) {
|
|
635
|
+
const spaceIdx = remaining.indexOf(' ');
|
|
636
|
+
remaining = spaceIdx >= 0 ? remaining.slice(spaceIdx) : '';
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
else if (remaining.startsWith('<')) {
|
|
644
|
+
// URI
|
|
645
|
+
const endIdx = remaining.indexOf('>');
|
|
646
|
+
if (endIdx >= 0) {
|
|
647
|
+
items.push(remaining.slice(1, endIdx));
|
|
648
|
+
remaining = remaining.slice(endIdx + 1).trim();
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
// Unquoted token
|
|
656
|
+
const spaceIdx = remaining.indexOf(' ');
|
|
657
|
+
if (spaceIdx >= 0) {
|
|
658
|
+
items.push(remaining.slice(0, spaceIdx));
|
|
659
|
+
remaining = remaining.slice(spaceIdx + 1);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
items.push(remaining);
|
|
663
|
+
remaining = '';
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return items;
|
|
668
|
+
}
|
|
669
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
670
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
671
|
+
/**
|
|
672
|
+
* Resolve a Cascade record type string (e.g., "MedicationRecord") to the full
|
|
673
|
+
* RDF type URI used in Turtle.
|
|
674
|
+
*/
|
|
675
|
+
function resolveTypeUri(type) {
|
|
676
|
+
// Try direct match in TYPE_MAPPING values
|
|
677
|
+
for (const mapping of Object.values(TYPE_MAPPING)) {
|
|
678
|
+
const colonIdx = mapping.rdfType.indexOf(':');
|
|
679
|
+
if (colonIdx >= 0) {
|
|
680
|
+
const nsPrefix = mapping.rdfType.slice(0, colonIdx);
|
|
681
|
+
const localName = mapping.rdfType.slice(colonIdx + 1);
|
|
682
|
+
if (localName === type) {
|
|
683
|
+
const nsUri = NAMESPACES[nsPrefix];
|
|
684
|
+
if (nsUri)
|
|
685
|
+
return `${nsUri}${localName}`;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Convert parsed triples for a single subject into a typed record object.
|
|
693
|
+
*/
|
|
694
|
+
function triplesToRecord(subjectUri, triples, recordType) {
|
|
695
|
+
const record = {
|
|
696
|
+
id: subjectUri,
|
|
697
|
+
type: recordType,
|
|
698
|
+
};
|
|
699
|
+
// Group triples by predicate for multi-value handling
|
|
700
|
+
const triplesByPredicate = new Map();
|
|
701
|
+
for (const triple of triples) {
|
|
702
|
+
if (triple.subject !== subjectUri)
|
|
703
|
+
continue;
|
|
704
|
+
if (triple.predicate === RDF_TYPE)
|
|
705
|
+
continue;
|
|
706
|
+
const existing = triplesByPredicate.get(triple.predicate);
|
|
707
|
+
if (existing) {
|
|
708
|
+
existing.push(triple);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
triplesByPredicate.set(triple.predicate, [triple]);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (const [predUri, predTriples] of triplesByPredicate) {
|
|
715
|
+
const jsonKey = REVERSE_PREDICATE_MAP.get(predUri);
|
|
716
|
+
if (!jsonKey)
|
|
717
|
+
continue;
|
|
718
|
+
// Handle array fields
|
|
719
|
+
if (ARRAY_TYPE_FIELDS.has(jsonKey)) {
|
|
720
|
+
const values = [];
|
|
721
|
+
for (const t of predTriples) {
|
|
722
|
+
if (t.objectType === 'list') {
|
|
723
|
+
try {
|
|
724
|
+
const parsed = JSON.parse(t.object);
|
|
725
|
+
values.push(...parsed);
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
values.push(t.object);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
values.push(t.object);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
record[jsonKey] = values;
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
// Single-value fields use the first triple
|
|
739
|
+
const triple = predTriples[0];
|
|
740
|
+
if (!triple)
|
|
741
|
+
continue;
|
|
742
|
+
// dataProvenance: extract local name from cascade namespace
|
|
743
|
+
if (jsonKey === 'dataProvenance') {
|
|
744
|
+
const cascadeNs = NAMESPACES.cascade;
|
|
745
|
+
if (triple.object.startsWith(cascadeNs)) {
|
|
746
|
+
record[jsonKey] = triple.object.slice(cascadeNs.length);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
record[jsonKey] = triple.object;
|
|
750
|
+
}
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
// Boolean fields
|
|
754
|
+
if (triple.objectType === 'boolean' || BOOLEAN_FIELDS.has(jsonKey)) {
|
|
755
|
+
record[jsonKey] = triple.object === 'true';
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
// Integer fields
|
|
759
|
+
if (INTEGER_TYPE_FIELDS.has(jsonKey) || triple.objectType === 'integer' ||
|
|
760
|
+
triple.datatype === NAMESPACES.xsd + 'integer') {
|
|
761
|
+
record[jsonKey] = parseInt(triple.object, 10);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
// Number fields (plain numeric)
|
|
765
|
+
if (NUMBER_FIELDS.has(jsonKey)) {
|
|
766
|
+
const num = parseFloat(triple.object);
|
|
767
|
+
if (!isNaN(num)) {
|
|
768
|
+
record[jsonKey] = num;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
record[jsonKey] = triple.object;
|
|
772
|
+
}
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
// Double/decimal fields
|
|
776
|
+
if (triple.objectType === 'double' ||
|
|
777
|
+
triple.datatype === NAMESPACES.xsd + 'double' ||
|
|
778
|
+
triple.datatype === NAMESPACES.xsd + 'decimal') {
|
|
779
|
+
record[jsonKey] = parseFloat(triple.object);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
// URI fields: keep the full URI string
|
|
783
|
+
if (triple.objectType === 'uri') {
|
|
784
|
+
record[jsonKey] = triple.object;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
// Default: string literal
|
|
788
|
+
record[jsonKey] = triple.object;
|
|
789
|
+
}
|
|
790
|
+
return record;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Parse Turtle content and return typed records matching the specified type.
|
|
794
|
+
*
|
|
795
|
+
* @param turtle - Turtle document content
|
|
796
|
+
* @param type - Record type string (e.g., "MedicationRecord", "VitalSign", "PatientProfile")
|
|
797
|
+
* @returns Array of parsed records of the specified type
|
|
798
|
+
*
|
|
799
|
+
* @example
|
|
800
|
+
* ```typescript
|
|
801
|
+
* import { deserialize } from '@the-cascade-protocol/sdk';
|
|
802
|
+
* import type { Medication } from '@the-cascade-protocol/sdk';
|
|
803
|
+
*
|
|
804
|
+
* const meds = deserialize<Medication>(turtleString, 'MedicationRecord');
|
|
805
|
+
* ```
|
|
806
|
+
*/
|
|
807
|
+
export function deserialize(turtle, type) {
|
|
808
|
+
const { triples } = parseTurtleContent(turtle);
|
|
809
|
+
// Resolve the requested type to a full URI
|
|
810
|
+
const typeUri = resolveTypeUri(type);
|
|
811
|
+
if (!typeUri) {
|
|
812
|
+
throw new Error(`Unknown record type: ${type}. Cannot resolve to RDF type URI.`);
|
|
813
|
+
}
|
|
814
|
+
// Find all subjects with matching rdf:type
|
|
815
|
+
const matchingSubjects = [];
|
|
816
|
+
for (const triple of triples) {
|
|
817
|
+
if (triple.predicate === RDF_TYPE && triple.object === typeUri) {
|
|
818
|
+
matchingSubjects.push(triple.subject);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// Look up the record type name from REVERSE_TYPE_MAP
|
|
822
|
+
const typeInfo = REVERSE_TYPE_MAP.get(typeUri);
|
|
823
|
+
const recordType = typeInfo?.recordType ?? type;
|
|
824
|
+
// Convert each subject to a record
|
|
825
|
+
return matchingSubjects.map((subjectUri) => triplesToRecord(subjectUri, triples, recordType));
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Parse a single record from Turtle content.
|
|
829
|
+
*
|
|
830
|
+
* Returns the first record matching the specified type, or `null` if none found.
|
|
831
|
+
*
|
|
832
|
+
* @param turtle - Turtle document content
|
|
833
|
+
* @param type - Record type string
|
|
834
|
+
* @returns The parsed record, or null
|
|
835
|
+
*/
|
|
836
|
+
export function deserializeOne(turtle, type) {
|
|
837
|
+
const results = deserialize(turtle, type);
|
|
838
|
+
return results[0] ?? null;
|
|
839
|
+
}
|
|
840
|
+
//# sourceMappingURL=turtle-parser.js.map
|