@graphty/graph-io 0.3.16 → 0.3.18
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 +1 -1
- package/README.md +29 -3
- package/dist/chunks/{escape-D1f9-cwf.js → escape-D-gZWO26.js} +3 -2
- package/dist/chunks/{escape-D1f9-cwf.js.map → escape-D-gZWO26.js.map} +1 -1
- package/dist/chunks/{importer-D7ZcGCeb.js → importer-Br_QeAeE.js} +4 -3
- package/dist/chunks/{importer-D7ZcGCeb.js.map → importer-Br_QeAeE.js.map} +1 -1
- package/dist/chunks/{importer-CXEiicAN.js → importer-DHagxvDD.js} +4 -3
- package/dist/chunks/{importer-CXEiicAN.js.map → importer-DHagxvDD.js.map} +1 -1
- package/dist/chunks/{importer-C7mnGdr_.js → importer-Du5crN9l.js} +510 -26
- package/dist/chunks/importer-Du5crN9l.js.map +1 -0
- package/dist/chunks/importer-aNJfe0qu.js +1614 -0
- package/dist/chunks/importer-aNJfe0qu.js.map +1 -0
- package/dist/chunks/{importer-B8lsjFWx.js → importer-d0uQxFp6.js} +4 -3
- package/dist/chunks/{importer-B8lsjFWx.js.map → importer-d0uQxFp6.js.map} +1 -1
- package/dist/chunks/ontology-BnrJ4I98.js +113 -0
- package/dist/chunks/ontology-BnrJ4I98.js.map +1 -0
- package/dist/chunks/{records-IHsCfv7s.js → records-Bk9jgodz.js} +2 -2
- package/dist/chunks/{records-IHsCfv7s.js.map → records-Bk9jgodz.js.map} +1 -1
- package/dist/chunks/{writer-BtWpUaiH.js → report-BOk0p5y8.js} +181 -912
- package/dist/chunks/report-BOk0p5y8.js.map +1 -0
- package/dist/chunks/writer-GAdltGmC.js +827 -0
- package/dist/chunks/writer-GAdltGmC.js.map +1 -0
- package/dist/csv.js +4 -3
- package/dist/csv.js.map +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +3 -2
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +3 -2
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +191 -134
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +1 -1
- package/dist/json.js +1 -1
- package/dist/neo4j.js +4 -3
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.d.ts +1 -0
- package/dist/obo.js +6 -0
- package/dist/obo.js.map +1 -0
- package/dist/pajek.js +1 -1
- package/dist/src/common/codes.d.ts +43 -0
- package/dist/src/common/codes.d.ts.map +1 -1
- package/dist/src/common/codes.js +43 -0
- package/dist/src/common/codes.js.map +1 -1
- package/dist/src/common/input.d.ts +9 -0
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +16 -0
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/ontology.d.ts +59 -0
- package/dist/src/common/ontology.d.ts.map +1 -0
- package/dist/src/common/ontology.js +147 -0
- package/dist/src/common/ontology.js.map +1 -0
- package/dist/src/common/options.d.ts +12 -1
- package/dist/src/common/options.d.ts.map +1 -1
- package/dist/src/common/options.js +51 -1
- package/dist/src/common/options.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts +8 -6
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +26 -3
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +324 -7
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +154 -23
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/obographs.d.ts +21 -0
- package/dist/src/formats/json/obographs.d.ts.map +1 -0
- package/dist/src/formats/json/obographs.js +476 -0
- package/dist/src/formats/json/obographs.js.map +1 -0
- package/dist/src/formats/obo/importer.d.ts +90 -0
- package/dist/src/formats/obo/importer.d.ts.map +1 -0
- package/dist/src/formats/obo/importer.js +1248 -0
- package/dist/src/formats/obo/importer.js.map +1 -0
- package/dist/src/formats/obo/index.d.ts +7 -0
- package/dist/src/formats/obo/index.d.ts.map +1 -0
- package/dist/src/formats/obo/index.js +7 -0
- package/dist/src/formats/obo/index.js.map +1 -0
- package/dist/src/formats/obo/syntax.d.ts +121 -0
- package/dist/src/formats/obo/syntax.d.ts.map +1 -0
- package/dist/src/formats/obo/syntax.js +424 -0
- package/dist/src/formats/obo/syntax.js.map +1 -0
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry.d.ts +21 -3
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +32 -1
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts +1 -1
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js +23 -3
- package/dist/src/sniff.js.map +1 -1
- package/dist/src/types.d.ts +31 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/package.json +6 -1
- package/src/common/codes.ts +58 -0
- package/src/common/input.ts +16 -0
- package/src/common/ontology.ts +169 -0
- package/src/common/options.ts +78 -2
- package/src/formats/json/dialect.ts +37 -7
- package/src/formats/json/importer.ts +206 -28
- package/src/formats/json/obographs.ts +563 -0
- package/src/formats/obo/importer.ts +1695 -0
- package/src/formats/obo/index.ts +7 -0
- package/src/formats/obo/syntax.ts +466 -0
- package/src/index.ts +6 -0
- package/src/registry.ts +38 -3
- package/src/sniff.ts +35 -5
- package/src/types.ts +40 -1
- package/dist/chunks/importer-C7mnGdr_.js.map +0 -1
- package/dist/chunks/writer-BtWpUaiH.js.map +0 -1
package/src/common/codes.ts
CHANGED
|
@@ -108,6 +108,64 @@ export const BAD_OPTIONS_CODE = "W_BAD_OPTIONS";
|
|
|
108
108
|
/** A repeated edge id in a format whose edge ids are unique; the second edge is skipped. */
|
|
109
109
|
export const DUPLICATE_EDGE_ID_CODE = "E_DUPLICATE_EDGE_ID";
|
|
110
110
|
|
|
111
|
+
/** A value does not parse as its declared type; the cell is left unset. */
|
|
112
|
+
export const BAD_VALUE_CODE = "E_BAD_VALUE";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A column's dtype was widened (design section 5.1) because a later value did not fit: an i32
|
|
116
|
+
* column meeting a value above 2^31, two declared types for one attribute.
|
|
117
|
+
*/
|
|
118
|
+
export const WIDENED_CODE = "W_WIDENED";
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A reference that is not a containment link (an attribute's element id, a layout or bypass
|
|
122
|
+
* entry, a subnetwork member, a view's network, an undeclared target) names nothing; reported
|
|
123
|
+
* once per kind with the count.
|
|
124
|
+
*/
|
|
125
|
+
export const DANGLING_REFERENCE_CODE = "W_DANGLING_REFERENCE";
|
|
126
|
+
|
|
127
|
+
/** The same attribute twice on one element; one value is kept (the later, unless the format's specification says the first). */
|
|
128
|
+
export const DUPLICATE_ATTRIBUTE_CODE = "W_DUPLICATE_ATTRIBUTE";
|
|
129
|
+
|
|
130
|
+
/** A containment link would close a parent cycle; that one link is dropped. */
|
|
131
|
+
export const PARENT_CYCLE_CODE = "E_PARENT_CYCLE";
|
|
132
|
+
|
|
133
|
+
/** A formula (Cytoscape's `=ABS($x)`) is kept as its text; it is never evaluated. */
|
|
134
|
+
export const EQUATION_AS_TEXT_CODE = "W_EQUATION_AS_TEXT";
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The file's style rules (defaults, mappings, dependencies, visual property aspects) are not
|
|
138
|
+
* applied to the snapshot; recorded once per import, the message names what was not applied.
|
|
139
|
+
*/
|
|
140
|
+
export const STYLES_NOT_IMPORTED_CODE = "W_STYLES_NOT_IMPORTED";
|
|
141
|
+
|
|
142
|
+
/** A CX array member that is not a one-key object holding an array or an object; the block is skipped. */
|
|
143
|
+
export const BAD_ASPECT_BLOCK_CODE = "E_BAD_ASPECT_BLOCK";
|
|
144
|
+
|
|
145
|
+
/** A CX aspect out of its place (after the post-metadata, a status that is not last, a block buffered for what it depends on). */
|
|
146
|
+
export const ASPECT_ORDER_CODE = "W_ASPECT_ORDER";
|
|
147
|
+
|
|
148
|
+
/** A declared element count disagrees with what was read. */
|
|
149
|
+
export const COUNT_MISMATCH_CODE = "W_COUNT_MISMATCH";
|
|
150
|
+
|
|
151
|
+
/** The producer marked the document as failed (CX `status.success: false`): it is incomplete (fatal). */
|
|
152
|
+
export const STATUS_FAILED_CODE = "E_STATUS_FAILED";
|
|
153
|
+
|
|
154
|
+
/** The producer marked the document as successful but attached an error text. */
|
|
155
|
+
export const STATUS_WARNING_CODE = "W_STATUS_WARNING";
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The input is beyond a size limit (a zip's uncompressed total or ratio, a document longer than
|
|
159
|
+
* one string); the same string as graph-format's E_TOO_LARGE, recorded with category "unsupported".
|
|
160
|
+
*/
|
|
161
|
+
export const TOO_LARGE_CODE = "E_TOO_LARGE";
|
|
162
|
+
|
|
163
|
+
/** `graphIndex` is beyond the graphs of the input, or `graphName` names none of them (fatal). */
|
|
164
|
+
export const GRAPH_NOT_FOUND_CODE = "E_GRAPH_NOT_FOUND";
|
|
165
|
+
|
|
166
|
+
/** `graphName` names more than one graph of the input; the message lists their indexes (fatal). */
|
|
167
|
+
export const AMBIGUOUS_GRAPH_NAME_CODE = "E_AMBIGUOUS_GRAPH_NAME";
|
|
168
|
+
|
|
111
169
|
// ============================================================ exporter loss notes
|
|
112
170
|
|
|
113
171
|
/** A role column the format has no slot for is written as a plain attribute (the role is lost). */
|
package/src/common/input.ts
CHANGED
|
@@ -262,6 +262,22 @@ function startsWithUtf8(bytes: Uint8Array): boolean {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
/**
|
|
266
|
+
* A zip entry name as text (APPNOTE 4.4.17): UTF-8 when the bytes are valid UTF-8, whether or not
|
|
267
|
+
* the entry sets the language-encoding flag (bit 11), else windows-1252 (WHATWG has no CP437, and
|
|
268
|
+
* session entry names are ASCII in practice). Never throws: a name is a matching key, never a path,
|
|
269
|
+
* so an undecodable one still has to name its entry.
|
|
270
|
+
* @param bytes - the name bytes from the central directory or a local header
|
|
271
|
+
* @returns the name
|
|
272
|
+
*/
|
|
273
|
+
export function decodeEntryName(bytes: Uint8Array): string {
|
|
274
|
+
try {
|
|
275
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
276
|
+
} catch {
|
|
277
|
+
return new TextDecoder("windows-1252").decode(bytes);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
265
281
|
/**
|
|
266
282
|
* Join byte chunks.
|
|
267
283
|
* @param chunks - the chunks
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the OBO importer and the JSON importer's `obographs` dialect share (design section 1.0, the
|
|
3
|
+
* `ontology.ts` building block): the OBO column vocabulary (names, dtypes, roles), so the `.obo` and
|
|
4
|
+
* the `.json` of one ontology give the same columns, and the OBO 1.4 rule for turning the IRIs
|
|
5
|
+
* OBO Graphs writes back into the identifiers the `.obo` file writes (section 4.6).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type ColumnDecl } from "@graphty/graph-format";
|
|
9
|
+
|
|
10
|
+
/** The dtypes the OBO vocabulary uses. */
|
|
11
|
+
type OboDtype = "string" | "dict" | "bool" | "list" | "json";
|
|
12
|
+
|
|
13
|
+
/** One column of the OBO vocabulary. */
|
|
14
|
+
interface OboColumnSpec {
|
|
15
|
+
/** The dtype. */
|
|
16
|
+
readonly dtype: OboDtype;
|
|
17
|
+
/** The role, when the column carries one. */
|
|
18
|
+
readonly role?: "label" | "kind" | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The node columns of the OBO vocabulary, keyed by column name (design section 4.2). The names are
|
|
23
|
+
* the OBO tag names, so a Gene Ontology user finds `namespace`, `def` and `is_obsolete` under the
|
|
24
|
+
* names the GO documentation uses.
|
|
25
|
+
*/
|
|
26
|
+
export const OBO_NODE_COLUMNS: Readonly<Record<string, OboColumnSpec>> = Object.freeze({
|
|
27
|
+
type: { dtype: "dict" },
|
|
28
|
+
name: { dtype: "string", role: "label" },
|
|
29
|
+
namespace: { dtype: "dict" },
|
|
30
|
+
def: { dtype: "string" },
|
|
31
|
+
"def.xrefs": { dtype: "list" },
|
|
32
|
+
comment: { dtype: "string" },
|
|
33
|
+
synonym: { dtype: "json" },
|
|
34
|
+
xref: { dtype: "list" },
|
|
35
|
+
"xref.descriptions": { dtype: "json" },
|
|
36
|
+
alt_id: { dtype: "list" },
|
|
37
|
+
subset: { dtype: "list" },
|
|
38
|
+
replaced_by: { dtype: "list" },
|
|
39
|
+
consider: { dtype: "list" },
|
|
40
|
+
is_obsolete: { dtype: "bool" },
|
|
41
|
+
is_anonymous: { dtype: "bool" },
|
|
42
|
+
builtin: { dtype: "bool" },
|
|
43
|
+
created_by: { dtype: "string" },
|
|
44
|
+
creation_date: { dtype: "string" },
|
|
45
|
+
intersection_of: { dtype: "json" },
|
|
46
|
+
union_of: { dtype: "list" },
|
|
47
|
+
equivalent_to: { dtype: "list" },
|
|
48
|
+
disjoint_from: { dtype: "list" },
|
|
49
|
+
property_value: { dtype: "json" },
|
|
50
|
+
// Typedef frames, under typedefs: "nodes"
|
|
51
|
+
domain: { dtype: "string" },
|
|
52
|
+
range: { dtype: "string" },
|
|
53
|
+
inverse_of: { dtype: "string" },
|
|
54
|
+
transitive_over: { dtype: "list" },
|
|
55
|
+
disjoint_over: { dtype: "list" },
|
|
56
|
+
holds_over_chain: { dtype: "json" },
|
|
57
|
+
equivalent_to_chain: { dtype: "json" },
|
|
58
|
+
expand_assertion_to: { dtype: "json" },
|
|
59
|
+
expand_expression_to: { dtype: "json" },
|
|
60
|
+
is_cyclic: { dtype: "bool" },
|
|
61
|
+
is_reflexive: { dtype: "bool" },
|
|
62
|
+
is_symmetric: { dtype: "bool" },
|
|
63
|
+
is_anti_symmetric: { dtype: "bool" },
|
|
64
|
+
is_asymmetric: { dtype: "bool" },
|
|
65
|
+
is_transitive: { dtype: "bool" },
|
|
66
|
+
is_functional: { dtype: "bool" },
|
|
67
|
+
is_inverse_functional: { dtype: "bool" },
|
|
68
|
+
is_metadata_tag: { dtype: "bool" },
|
|
69
|
+
is_class_level: { dtype: "bool" },
|
|
70
|
+
// OBO Graphs only
|
|
71
|
+
propertyType: { dtype: "dict" },
|
|
72
|
+
// what has no column of its own
|
|
73
|
+
"obo.qualifiers": { dtype: "json" },
|
|
74
|
+
"obo.unrecognized": { dtype: "json" },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** The edge columns of the OBO vocabulary. */
|
|
78
|
+
const OBO_EDGE_COLUMNS: Readonly<Record<string, OboColumnSpec>> = Object.freeze({
|
|
79
|
+
relation: { dtype: "dict", role: "kind" },
|
|
80
|
+
qualifiers: { dtype: "json" },
|
|
81
|
+
meta: { dtype: "json" },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** The node column that marks a node made for an undeclared reference (design section 4.2). */
|
|
85
|
+
export const PLACEHOLDER_COLUMN = "graphty.placeholder";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The declaration of an OBO vocabulary column.
|
|
89
|
+
* @param domain - node or edge
|
|
90
|
+
* @param name - the column name (a key of OBO_NODE_COLUMNS / OBO_EDGE_COLUMNS, or the placeholder column)
|
|
91
|
+
* @returns the declaration, nullable, with origin `{ format: "obo", id: name }`
|
|
92
|
+
*/
|
|
93
|
+
export function oboColumnDecl(domain: "node" | "edge", name: string): ColumnDecl {
|
|
94
|
+
if (name === PLACEHOLDER_COLUMN) {
|
|
95
|
+
return { name, dtype: "bool", nullable: true, origin: { format: "graphty", id: "placeholder" } };
|
|
96
|
+
}
|
|
97
|
+
const spec = (domain === "node" ? OBO_NODE_COLUMNS : OBO_EDGE_COLUMNS)[name];
|
|
98
|
+
const decl: ColumnDecl = { name, dtype: spec.dtype, nullable: true, origin: { format: "obo", id: name } };
|
|
99
|
+
if (spec.dtype === "list") {
|
|
100
|
+
decl.itemDtype = "string";
|
|
101
|
+
}
|
|
102
|
+
if (spec.role !== undefined) {
|
|
103
|
+
decl.role = spec.role;
|
|
104
|
+
}
|
|
105
|
+
return decl;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The OBO PURL base every OBO Foundry IRI starts with. */
|
|
109
|
+
const OBO_PURL = "http://purl.obolibrary.org/obo/";
|
|
110
|
+
|
|
111
|
+
/** The oboInOwl namespace of the OBO-to-OWL mapping's annotation properties. */
|
|
112
|
+
const OBO_IN_OWL = "http://www.geneontology.org/formats/oboInOwl#";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* An IRI as the identifier the `.obo` file writes (the OBO 1.4 mapping, section 5.9, read
|
|
116
|
+
* backwards): `http://purl.obolibrary.org/obo/GO_0008150` is `GO:0008150` (the prefix is the text
|
|
117
|
+
* before the first underscore), and `http://purl.obolibrary.org/obo/go#regulates` (how the OWL
|
|
118
|
+
* translation writes an unprefixed OBO id: subsets, relations, synonym types) is `regulates`.
|
|
119
|
+
* Every other IRI, and an OBO PURL that fits neither form (`.../obo/T/Female`), is kept.
|
|
120
|
+
* @param iri - the IRI
|
|
121
|
+
* @returns the CURIE or local id, or the IRI unchanged
|
|
122
|
+
*/
|
|
123
|
+
export function compactOboIri(iri: string): string {
|
|
124
|
+
if (!iri.startsWith(OBO_PURL)) {
|
|
125
|
+
return iri;
|
|
126
|
+
}
|
|
127
|
+
const rest = iri.slice(OBO_PURL.length);
|
|
128
|
+
const hash = rest.indexOf("#");
|
|
129
|
+
if (hash > 0) {
|
|
130
|
+
const local = rest.slice(hash + 1);
|
|
131
|
+
return local.length > 0 && !rest.slice(0, hash).includes("/") ? local : iri;
|
|
132
|
+
}
|
|
133
|
+
const underscore = rest.indexOf("_");
|
|
134
|
+
if (underscore <= 0 || rest.includes("/") || underscore === rest.length - 1) {
|
|
135
|
+
return iri;
|
|
136
|
+
}
|
|
137
|
+
return `${rest.slice(0, underscore)}:${rest.slice(underscore + 1)}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The OBO synonym scopes. */
|
|
141
|
+
export const SYNONYM_SCOPES: ReadonlySet<string> = new Set(["EXACT", "BROAD", "NARROW", "RELATED"]);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The OBO synonym scope of an OBO Graphs synonym predicate (`hasExactSynonym`, with or without the
|
|
145
|
+
* oboInOwl namespace).
|
|
146
|
+
* @param pred - the predicate
|
|
147
|
+
* @returns the scope, or null for a predicate that is not one of the four
|
|
148
|
+
*/
|
|
149
|
+
export function synonymScopeOf(pred: string): string | null {
|
|
150
|
+
const local = pred.startsWith(OBO_IN_OWL) ? pred.slice(OBO_IN_OWL.length) : pred;
|
|
151
|
+
const match = /^has(Exact|Broad|Narrow|Related)Synonym$/.exec(local);
|
|
152
|
+
return match === null ? null : match[1].toUpperCase();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The OBO tag a `basicPropertyValues` predicate of OBO Graphs came from (the OBO-to-OWL mapping of
|
|
157
|
+
* the tags that are annotations), so the `.json` of an ontology fills the same columns as its
|
|
158
|
+
* `.obo`. `shorthand` is the relation's short name; every predicate not listed is a
|
|
159
|
+
* `property_value`.
|
|
160
|
+
*/
|
|
161
|
+
export const OBOGRAPHS_PREDICATE_TAGS: ReadonlyMap<string, string> = new Map([
|
|
162
|
+
[`${OBO_IN_OWL}hasOBONamespace`, "namespace"],
|
|
163
|
+
[`${OBO_IN_OWL}hasAlternativeId`, "alt_id"],
|
|
164
|
+
[`${OBO_IN_OWL}created_by`, "created_by"],
|
|
165
|
+
[`${OBO_IN_OWL}creation_date`, "creation_date"],
|
|
166
|
+
[`${OBO_PURL}IAO_0100001`, "replaced_by"],
|
|
167
|
+
[`${OBO_IN_OWL}consider`, "consider"],
|
|
168
|
+
[`${OBO_IN_OWL}shorthand`, "shorthand"],
|
|
169
|
+
]);
|
package/src/common/options.ts
CHANGED
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { type DuplicatePolicy, GraphFormatError, type GraphSink, type IdCoercion } from "@graphty/graph-format";
|
|
9
9
|
|
|
10
|
-
import { type CommonExportOptions, type CommonImportOptions } from "../types.js";
|
|
11
|
-
import {
|
|
10
|
+
import { type CommonExportOptions, type CommonImportOptions, type GraphChoiceOptions } from "../types.js";
|
|
11
|
+
import {
|
|
12
|
+
AMBIGUOUS_GRAPH_NAME_CODE,
|
|
13
|
+
GRAPH_NOT_FOUND_CODE,
|
|
14
|
+
NO_GRAPH_CODE,
|
|
15
|
+
OPTION_IGNORED_CODE,
|
|
16
|
+
SINK_OPTION_CODE,
|
|
17
|
+
} from "./codes.js";
|
|
12
18
|
import { canonicalEncoding } from "./input.js";
|
|
13
19
|
import { type ImportReportBuilder } from "./report.js";
|
|
14
20
|
|
|
@@ -188,6 +194,76 @@ export function reportUnusedOptions(
|
|
|
188
194
|
return recorded;
|
|
189
195
|
}
|
|
190
196
|
|
|
197
|
+
/**
|
|
198
|
+
* The graph an importer reads from an input that holds several: the one `graphIndex` or
|
|
199
|
+
* `graphName` names, else the first. A choice that names no graph is E_GRAPH_NOT_FOUND, a name two
|
|
200
|
+
* graphs share is E_AMBIGUOUS_GRAPH_NAME, and an input with no graph at all is E_NO_GRAPH, each
|
|
201
|
+
* fatal (the report fails with ImportError).
|
|
202
|
+
* @param names - each graph's name (null for an unnamed graph), in document order
|
|
203
|
+
* @param options - the caller's graphIndex / graphName
|
|
204
|
+
* @param report - the report a failure is recorded in
|
|
205
|
+
* @returns the index of the graph to read; E_UNSUPPORTED for an option of the wrong type, or both
|
|
206
|
+
*/
|
|
207
|
+
export function chooseGraph(
|
|
208
|
+
names: readonly (string | null)[],
|
|
209
|
+
options: GraphChoiceOptions | undefined,
|
|
210
|
+
report: ImportReportBuilder,
|
|
211
|
+
): number {
|
|
212
|
+
const { graphIndex, graphName } = options ?? {};
|
|
213
|
+
if (
|
|
214
|
+
graphIndex !== undefined &&
|
|
215
|
+
(typeof graphIndex !== "number" || !Number.isInteger(graphIndex) || graphIndex < 0)
|
|
216
|
+
) {
|
|
217
|
+
throw new GraphFormatError(
|
|
218
|
+
"E_UNSUPPORTED",
|
|
219
|
+
`option graphIndex: ${describe(graphIndex)} is not a non-negative integer`,
|
|
220
|
+
{
|
|
221
|
+
option: "graphIndex",
|
|
222
|
+
found: graphIndex,
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (graphName !== undefined && typeof graphName !== "string") {
|
|
227
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option graphName: ${describe(graphName)} is not a string`, {
|
|
228
|
+
option: "graphName",
|
|
229
|
+
found: graphName,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (graphIndex !== undefined && graphName !== undefined) {
|
|
233
|
+
throw new GraphFormatError("E_UNSUPPORTED", "options graphIndex and graphName both choose a graph; pass one", {
|
|
234
|
+
option: "graphName",
|
|
235
|
+
found: graphName,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (names.length === 0) {
|
|
239
|
+
return report.fail(NO_GRAPH_CODE, "the input holds no graph");
|
|
240
|
+
}
|
|
241
|
+
if (graphName !== undefined) {
|
|
242
|
+
const matches = names.flatMap((name, i) => (name === graphName ? [i] : []));
|
|
243
|
+
if (matches.length > 1) {
|
|
244
|
+
return report.fail(
|
|
245
|
+
AMBIGUOUS_GRAPH_NAME_CODE,
|
|
246
|
+
`graphName ${JSON.stringify(graphName)} names the graphs at indexes ${matches.join(", ")}; pass graphIndex`,
|
|
247
|
+
{ element: graphName },
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (matches.length === 0) {
|
|
251
|
+
return report.fail(
|
|
252
|
+
GRAPH_NOT_FOUND_CODE,
|
|
253
|
+
`graphName ${JSON.stringify(graphName)} names none of the ${names.length} graph(s)`,
|
|
254
|
+
{ element: graphName },
|
|
255
|
+
{ names: [...names] },
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return matches[0];
|
|
259
|
+
}
|
|
260
|
+
const index = graphIndex ?? 0;
|
|
261
|
+
if (index >= names.length) {
|
|
262
|
+
return report.fail(GRAPH_NOT_FOUND_CODE, `graphIndex ${index} is beyond the ${names.length} graph(s)`);
|
|
263
|
+
}
|
|
264
|
+
return index;
|
|
265
|
+
}
|
|
266
|
+
|
|
191
267
|
/**
|
|
192
268
|
* Apply the design section 8.4 defaults to an importer's common options and check every enum
|
|
193
269
|
* value. Format-specific options in the same object are ignored here.
|
|
@@ -32,17 +32,19 @@ export const JSON_DIALECTS: readonly JsonDialect[] = Object.freeze([
|
|
|
32
32
|
]);
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* The dialects the importer reads: every JsonDialect plus
|
|
36
|
-
* adjacency_data (`nodes` plus one neighbour list per node under `adjacency`)
|
|
37
|
-
* nested `id` / `children` record)
|
|
35
|
+
* The dialects the importer reads: every JsonDialect plus three it only reads, NetworkX
|
|
36
|
+
* adjacency_data (`nodes` plus one neighbour list per node under `adjacency`), tree_data (a
|
|
37
|
+
* nested `id` / `children` record) and OBO Graphs (`graphs[]` of `sub` / `pred` / `obj` edges, the
|
|
38
|
+
* JSON form of the Gene Ontology and the OBO Foundry ontologies). The exporter writes none of them.
|
|
38
39
|
*/
|
|
39
|
-
export type JsonImportDialect = JsonDialect | "adjacency" | "tree";
|
|
40
|
+
export type JsonImportDialect = JsonDialect | "adjacency" | "tree" | "obographs";
|
|
40
41
|
|
|
41
42
|
/** Every dialect the importer reads, for option checking and messages. */
|
|
42
43
|
export const JSON_IMPORT_DIALECTS: readonly JsonImportDialect[] = Object.freeze([
|
|
43
44
|
...JSON_DIALECTS,
|
|
44
45
|
"adjacency",
|
|
45
46
|
"tree",
|
|
47
|
+
"obographs",
|
|
46
48
|
]);
|
|
47
49
|
|
|
48
50
|
/** The key under `meta.extra` that holds the shape record (design section 8.5). */
|
|
@@ -116,8 +118,9 @@ export function isJsonImportDialect(value: unknown): value is JsonImportDialect
|
|
|
116
118
|
|
|
117
119
|
/**
|
|
118
120
|
* The dialect of a parsed JSON document, by the shape rules of design section 8.2 (Cytoscape:
|
|
119
|
-
* `elements` or a top-level array of `{ data }` elements;
|
|
120
|
-
*
|
|
121
|
+
* `elements` or a top-level array of `{ data }` elements; OBO Graphs: `graphs[]` whose first graph
|
|
122
|
+
* has an edge with `sub` (or `subj`) and `obj` or a `pred`, or a node with `lbl`, `meta` or an OWL
|
|
123
|
+
* `type`; JGF: `graph.nodes` / `graph.edges` or any other `graphs[]`; graphology: `options.type` / `options.multi`, `key` nodes without `id`, edges with
|
|
121
124
|
* `undirected` or an `attributes` record; vis: edges with `from` / `to`; d3: `links` without
|
|
122
125
|
* `directed` / `multigraph` / `graph`; NetworkX adjacency_data: `nodes` and `adjacency` without
|
|
123
126
|
* `links` / `edges`; NetworkX tree_data: `children` without `nodes` / `links` / `edges`; else
|
|
@@ -141,7 +144,7 @@ export function sniffJsonDialect(root: unknown): JsonImportDialect | null {
|
|
|
141
144
|
return "jgf";
|
|
142
145
|
}
|
|
143
146
|
if (Array.isArray(root.graphs)) {
|
|
144
|
-
return "jgf";
|
|
147
|
+
return isOboGraph(firstJsonObject(root.graphs)) ? "obographs" : "jgf";
|
|
145
148
|
}
|
|
146
149
|
if (!hasKey(root, "edges") && !hasKey(root, "links")) {
|
|
147
150
|
if (hasKey(root, "nodes") && hasKey(root, "adjacency")) {
|
|
@@ -171,6 +174,31 @@ export function sniffJsonDialect(root: unknown): JsonImportDialect | null {
|
|
|
171
174
|
return bare && hasKey(root, "links") ? "d3" : "node-link";
|
|
172
175
|
}
|
|
173
176
|
|
|
177
|
+
/** The node types of OBO Graphs (the OWL entity kinds). */
|
|
178
|
+
const OBOGRAPHS_NODE_TYPES: ReadonlySet<unknown> = new Set(["CLASS", "INDIVIDUAL", "PROPERTY"]);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Whether a graph of a `graphs[]` document is an OBO Graphs graph rather than JGF: anywhere in its
|
|
182
|
+
* nodes or edges, an edge with `sub` (or the outdated `subj`) and `obj`, or a `pred`, or a node
|
|
183
|
+
* with `lbl`, `meta` or a `type` of CLASS / INDIVIDUAL / PROPERTY (design 1.6: looking only at the
|
|
184
|
+
* first node and edge misses graphs with no edges and graphs whose first node has no `lbl`).
|
|
185
|
+
* @param graph - the first graph, or null
|
|
186
|
+
* @returns true for OBO Graphs
|
|
187
|
+
*/
|
|
188
|
+
function isOboGraph(graph: Record<string, unknown> | null): boolean {
|
|
189
|
+
if (graph === null) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const isEdge = (e: unknown): boolean =>
|
|
193
|
+
isJsonObject(e) && ((hasKey(e, "obj") && (hasKey(e, "sub") || hasKey(e, "subj"))) || hasKey(e, "pred"));
|
|
194
|
+
const isNode = (n: unknown): boolean =>
|
|
195
|
+
isJsonObject(n) && (hasKey(n, "lbl") || hasKey(n, "meta") || OBOGRAPHS_NODE_TYPES.has(n.type));
|
|
196
|
+
return (
|
|
197
|
+
(Array.isArray(graph.edges) && graph.edges.some(isEdge)) ||
|
|
198
|
+
(Array.isArray(graph.nodes) && graph.nodes.some(isNode))
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
174
202
|
/**
|
|
175
203
|
* The first object of an array, or null.
|
|
176
204
|
* @param value - maybe an array
|
|
@@ -223,6 +251,8 @@ export const DIALECT_DEFAULT_DIRECTED: Readonly<Record<JsonImportDialect, boolea
|
|
|
223
251
|
// networkx adjacency_data declares `directed`; tree_graph always builds a DiGraph
|
|
224
252
|
adjacency: false,
|
|
225
253
|
tree: true,
|
|
254
|
+
// OBO Graphs edges point from the subject (child) to the object (parent)
|
|
255
|
+
obographs: true,
|
|
226
256
|
});
|
|
227
257
|
|
|
228
258
|
/**
|