Skip to content

Commit 90a7e7e

Browse files
committed
feat: support for external tupples
1 parent abd150d commit 90a7e7e

File tree

2 files changed

+151
-38
lines changed

2 files changed

+151
-38
lines changed

server/src/server.common.ts

Lines changed: 112 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,19 @@ import { TextDocument } from "vscode-languageserver-textdocument";
2121
import { errors, transformer } from "@openfga/syntax-transformer";
2222
import { defaultDocumentationMap } from "./documentation";
2323
import { getDuplicationFix, getMissingDefinitionFix, getReservedTypeNameFix } from "./code-action";
24-
import { LineCounter, YAMLSeq, parseDocument } from "yaml";
24+
import {
25+
LineCounter,
26+
YAMLSeq,
27+
parseDocument,
28+
Range as TokenRange,
29+
isScalar,
30+
visitAsync,
31+
Scalar,
32+
Pair,
33+
Document,
34+
visit,
35+
} from "yaml";
36+
import { stringify } from "json-to-pretty-yaml";
2537
import {
2638
YAMLSourceMap,
2739
YamlStoreValidateResults,
@@ -31,6 +43,7 @@ import {
3143
validateYamlStore,
3244
getFieldPosition,
3345
getRangeFromToken,
46+
DocumentLoc,
3447
} from "./yaml-utils";
3548
import { getRangeOfWord } from "./helpers";
3649
import { getDiagnosticsForDsl as validateDSL } from "./dsl-utils";
@@ -100,16 +113,109 @@ export function startServer(connection: _Connection) {
100113
connection.languages.diagnostics.refresh();
101114
});
102115

103-
async function validateYamlSyntaxAndModel(textDocument: TextDocument): Promise<YamlStoreValidateResults> {
104-
const diagnostics: Diagnostic[] = [];
105-
const modelDiagnostics: Diagnostic[] = [];
106-
116+
async function parseYamlStore(
117+
textDocument: TextDocument,
118+
): Promise<{ yamlDoc: Document; lineCounter: LineCounter; parsedDiagnostics: Diagnostic[] }> {
107119
const lineCounter = new LineCounter();
108120
const yamlDoc = parseDocument(textDocument.getText(), {
109121
lineCounter,
110122
keepSourceTokens: true,
111123
});
112124

125+
const parsedDiagnostics: Diagnostic[] = [];
126+
127+
// Basic syntax errors
128+
for (const err of yamlDoc.errors) {
129+
parsedDiagnostics.push({ message: err.message, range: rangeFromLinePos(err.linePos) });
130+
}
131+
132+
const importedDocs = new Map<string, DocumentLoc>();
133+
134+
await visitAsync(yamlDoc, {
135+
async Pair(_, pair) {
136+
if (!isScalar(pair.key) || !isScalar(pair.value) || pair.key.value !== "tuple_file" || !pair.value.source) {
137+
return;
138+
}
139+
140+
const originalRange = pair.key.range;
141+
try {
142+
const result = await getFileContents(URI.parse(textDocument.uri), pair.value.source);
143+
if (pair.value.source.match(/.yaml$/)) {
144+
const file = parseDocument(result.contents);
145+
146+
const diagnosticFromInclusion: Diagnostic[] = [];
147+
148+
diagnosticFromInclusion.push(
149+
...file.errors.map((err) => {
150+
return {
151+
source: "ParseError",
152+
message: "error with external file: " + err.message,
153+
range: getRangeFromToken(originalRange, textDocument),
154+
};
155+
}),
156+
);
157+
158+
if (diagnosticFromInclusion.length) {
159+
parsedDiagnostics.push(...diagnosticFromInclusion);
160+
return undefined;
161+
}
162+
163+
if (originalRange) {
164+
importedDocs.set(pair.value.source, { range: originalRange, doc: file });
165+
}
166+
return visit.SKIP;
167+
}
168+
} catch (err) {
169+
parsedDiagnostics.push({
170+
range: getRangeFromToken(originalRange, textDocument),
171+
message: "error with external file: " + (err as Error).message,
172+
source: "ParseError",
173+
});
174+
}
175+
},
176+
});
177+
178+
// Override all tuples with new location
179+
for (const p of importedDocs.entries()) {
180+
visit(p[1].doc.contents, {
181+
Scalar(key, node) {
182+
node.range = p[1].range;
183+
},
184+
});
185+
}
186+
187+
// Prepare final virtual doc
188+
visit(yamlDoc, {
189+
Pair(_, pair) {
190+
if (!isScalar(pair.key) || !isScalar(pair.value) || pair.key.value !== "tuple_file" || !pair.value.source) {
191+
return;
192+
}
193+
194+
const value = importedDocs.get(pair.value.source);
195+
196+
if (value) {
197+
// Import tuples, and point range at where file field used to exist
198+
const scalar = new Scalar("tuples");
199+
scalar.source = "tuples";
200+
scalar.range = value?.range;
201+
202+
return new Pair(scalar, value?.doc.contents);
203+
}
204+
},
205+
});
206+
return { yamlDoc, lineCounter, parsedDiagnostics };
207+
}
208+
209+
async function validateYamlSyntaxAndModel(textDocument: TextDocument): Promise<YamlStoreValidateResults> {
210+
const diagnostics: Diagnostic[] = [];
211+
const modelDiagnostics: Diagnostic[] = [];
212+
213+
const { yamlDoc, lineCounter, parsedDiagnostics } = await parseYamlStore(textDocument);
214+
215+
if (parsedDiagnostics.length) {
216+
return { diagnostics: parsedDiagnostics };
217+
}
218+
113219
const map = new YAMLSourceMap();
114220
map.doMap(yamlDoc.contents);
115221

@@ -119,25 +225,6 @@ export function startServer(connection: _Connection) {
119225
return { diagnostics };
120226
}
121227

122-
// Basic syntax errors
123-
for (const err of yamlDoc.errors) {
124-
diagnostics.push({ message: err.message, range: rangeFromLinePos(err.linePos) });
125-
}
126-
127-
const keys = [...map.nodes.keys()].filter((key) => key.includes("tuple_file"));
128-
for (const fileField of keys) {
129-
const fileName = yamlDoc.getIn(fileField.split(".")) as string;
130-
try {
131-
await getFileContents(URI.parse(textDocument.uri), fileName);
132-
} catch (err) {
133-
diagnostics.push({
134-
range: getRangeFromToken(map.nodes.get(fileField), textDocument),
135-
message: "error with external file: " + (err as Error).message,
136-
source: "ParseError",
137-
});
138-
}
139-
}
140-
141228
let model,
142229
modelUri = undefined;
143230

@@ -147,7 +234,7 @@ export function startServer(connection: _Connection) {
147234
diagnostics.push(...parseYamlModel(yamlDoc, lineCounter));
148235
diagnostics.push(...validateYamlStore(yamlDoc.get("model") as string, yamlDoc, textDocument, map));
149236
} else if (yamlDoc.has("model_file")) {
150-
const position = getFieldPosition(yamlDoc, lineCounter, "model_file");
237+
const position = getFieldPosition(yamlDoc, lineCounter, "model_file")[0];
151238
const modelFile = yamlDoc.get("model_file") as string;
152239

153240
try {

server/src/yaml-utils.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
11
import { Range, Position, Diagnostic, DiagnosticSeverity } from "vscode-languageserver";
22

3-
import { Document, LineCounter, Node, Range as TokenRange, isMap, isPair, isScalar, isSeq } from "yaml";
3+
import {
4+
Document,
5+
LineCounter,
6+
Node,
7+
Pair,
8+
Range as TokenRange,
9+
isDocument,
10+
isMap,
11+
isNode,
12+
isPair,
13+
isScalar,
14+
isSeq,
15+
parseDocument,
16+
visit,
17+
} from "yaml";
418
import { LinePos } from "yaml/dist/errors";
5-
import { BlockMap, SourceToken } from "yaml/dist/parse/cst";
619
import { getDiagnosticsForDsl } from "./dsl-utils";
720
import { ErrorObject, ValidateFunction } from "ajv";
821
import { transformer } from "@openfga/syntax-transformer";
922
import { YamlStoreValidator } from "./openfga-yaml-schema";
1023
import { TextDocument } from "vscode-languageserver-textdocument";
1124
import { URI } from "vscode-uri";
1225

26+
export type DocumentLoc = {
27+
range: TokenRange;
28+
doc: Document;
29+
};
30+
1331
export type YamlStoreValidateResults = {
1432
diagnostics: Diagnostic[];
1533
modelUri?: URI;
@@ -32,22 +50,31 @@ export function rangeFromLinePos(linePos: [LinePos] | [LinePos, LinePos] | undef
3250
return { start, end };
3351
}
3452

35-
// Only gets the line of 1st depth. This should be deprecated and replaced.
53+
export function parseDocumentWithFixedRange(contents: string, range: TokenRange): Document {
54+
const doc = parseDocument(contents);
55+
visit(doc, (key, node, path) => {
56+
if (isPair(node) && isScalar(node.key)) {
57+
node.key.range = range;
58+
59+
return new Pair(node);
60+
}
61+
});
62+
return doc;
63+
}
64+
3665
export function getFieldPosition(
3766
yamlDoc: Document,
3867
lineCounter: LineCounter,
3968
field: string,
40-
): { line: number; col: number } {
41-
let position: { line: number; col: number } = { line: 0, col: 0 };
42-
43-
// Get the model token and find its position
44-
(yamlDoc.contents?.srcToken as BlockMap).items.forEach((i) => {
45-
if (i.key?.offset !== undefined && (i.key as SourceToken).source === field) {
46-
position = lineCounter.linePos(i.key?.offset);
69+
): { line: number; col: number }[] {
70+
const positions: { line: number; col: number }[] = [];
71+
visit(yamlDoc, (key, node, path) => {
72+
if (isPair(node) && isScalar(node.key) && node.key.value === field && node.key.srcToken?.offset) {
73+
positions.push(lineCounter.linePos(node.key.srcToken?.offset));
4774
}
4875
});
4976

50-
return position;
77+
return positions;
5178
}
5279

5380
export function validateYamlStore(
@@ -115,7 +142,7 @@ export function validateYamlStore(
115142
}
116143

117144
export function parseYamlModel(yamlDoc: Document, lineCounter: LineCounter): Diagnostic[] {
118-
const position = getFieldPosition(yamlDoc, lineCounter, "model");
145+
const position = getFieldPosition(yamlDoc, lineCounter, "model")[0];
119146

120147
// Shift generated diagnostics by line of model, and indent of 2
121148
let dslDiagnostics = getDiagnosticsForDsl(yamlDoc.get("model") as string);
@@ -172,7 +199,6 @@ export class YAMLSourceMap {
172199

173200
if (isScalar(node) && node.source && node.range) {
174201
this.nodes.set(localPath.join("."), node.range);
175-
return;
176202
}
177203
}
178204
}

0 commit comments

Comments
 (0)