Skip to content

Commit ec2f2b0

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

File tree

2 files changed

+137
-38
lines changed

2 files changed

+137
-38
lines changed

server/src/server.common.ts

Lines changed: 100 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ 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 { LineCounter, YAMLSeq, parseDocument, isScalar, visitAsync, Scalar, Pair, Document, visit } from "yaml";
2525
import {
2626
YAMLSourceMap,
2727
YamlStoreValidateResults,
@@ -31,6 +31,7 @@ import {
3131
validateYamlStore,
3232
getFieldPosition,
3333
getRangeFromToken,
34+
DocumentLoc,
3435
} from "./yaml-utils";
3536
import { getRangeOfWord } from "./helpers";
3637
import { getDiagnosticsForDsl as validateDSL } from "./dsl-utils";
@@ -100,16 +101,109 @@ export function startServer(connection: _Connection) {
100101
connection.languages.diagnostics.refresh();
101102
});
102103

103-
async function validateYamlSyntaxAndModel(textDocument: TextDocument): Promise<YamlStoreValidateResults> {
104-
const diagnostics: Diagnostic[] = [];
105-
const modelDiagnostics: Diagnostic[] = [];
106-
104+
async function parseYamlStore(
105+
textDocument: TextDocument,
106+
): Promise<{ yamlDoc: Document; lineCounter: LineCounter; parsedDiagnostics: Diagnostic[] }> {
107107
const lineCounter = new LineCounter();
108108
const yamlDoc = parseDocument(textDocument.getText(), {
109109
lineCounter,
110110
keepSourceTokens: true,
111111
});
112112

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

@@ -119,25 +213,6 @@ export function startServer(connection: _Connection) {
119213
return { diagnostics };
120214
}
121215

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-
141216
let model,
142217
modelUri = undefined;
143218

@@ -147,7 +222,7 @@ export function startServer(connection: _Connection) {
147222
diagnostics.push(...parseYamlModel(yamlDoc, lineCounter));
148223
diagnostics.push(...validateYamlStore(yamlDoc.get("model") as string, yamlDoc, textDocument, map));
149224
} else if (yamlDoc.has("model_file")) {
150-
const position = getFieldPosition(yamlDoc, lineCounter, "model_file");
225+
const position = getFieldPosition(yamlDoc, lineCounter, "model_file")[0];
151226
const modelFile = yamlDoc.get("model_file") as string;
152227

153228
try {

server/src/yaml-utils.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
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+
isMap,
10+
isPair,
11+
isScalar,
12+
isSeq,
13+
parseDocument,
14+
visit,
15+
} from "yaml";
416
import { LinePos } from "yaml/dist/errors";
5-
import { BlockMap, SourceToken } from "yaml/dist/parse/cst";
617
import { getDiagnosticsForDsl } from "./dsl-utils";
718
import { ErrorObject, ValidateFunction } from "ajv";
819
import { transformer } from "@openfga/syntax-transformer";
920
import { YamlStoreValidator } from "./openfga-yaml-schema";
1021
import { TextDocument } from "vscode-languageserver-textdocument";
1122
import { URI } from "vscode-uri";
1223

24+
export type DocumentLoc = {
25+
range: TokenRange;
26+
doc: Document;
27+
};
28+
1329
export type YamlStoreValidateResults = {
1430
diagnostics: Diagnostic[];
1531
modelUri?: URI;
@@ -32,22 +48,31 @@ export function rangeFromLinePos(linePos: [LinePos] | [LinePos, LinePos] | undef
3248
return { start, end };
3349
}
3450

35-
// Only gets the line of 1st depth. This should be deprecated and replaced.
51+
export function parseDocumentWithFixedRange(contents: string, range: TokenRange): Document {
52+
const doc = parseDocument(contents);
53+
visit(doc, (key, node) => {
54+
if (isPair(node) && isScalar(node.key)) {
55+
node.key.range = range;
56+
57+
return new Pair(node);
58+
}
59+
});
60+
return doc;
61+
}
62+
3663
export function getFieldPosition(
3764
yamlDoc: Document,
3865
lineCounter: LineCounter,
3966
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);
67+
): { line: number; col: number }[] {
68+
const positions: { line: number; col: number }[] = [];
69+
visit(yamlDoc, (key, node) => {
70+
if (isPair(node) && isScalar(node.key) && node.key.value === field && node.key.srcToken?.offset) {
71+
positions.push(lineCounter.linePos(node.key.srcToken?.offset));
4772
}
4873
});
4974

50-
return position;
75+
return positions;
5176
}
5277

5378
export function validateYamlStore(
@@ -115,7 +140,7 @@ export function validateYamlStore(
115140
}
116141

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

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

173198
if (isScalar(node) && node.source && node.range) {
174199
this.nodes.set(localPath.join("."), node.range);
175-
return;
176200
}
177201
}
178202
}

0 commit comments

Comments
 (0)