Skip to content

Commit 516ee35

Browse files
authored
Merge pull request #16 from openfga/feat/model-validation
feat: external annotator for OpenFGA files
2 parents fb531fc + 46428a1 commit 516ee35

File tree

6 files changed

+227
-4
lines changed

6 files changed

+227
-4
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
2+
## v0.1.1
3+
4+
### [0.1.1](https://github.com/openfga/vscode-ext/compare/v0.1.0...v0.1.1) (2024-05-31)
5+
6+
- feat: validation for OpenFGA model files
7+
- feat: validation for OpenFGA models in store files
8+
19
## v0.1.0
210

311
### [0.1.0](https://github.com/openfga/intellij-plugin/releases/tag/v0.1.0) (2024-05-10)

build.gradle.kts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,23 @@ plugins {
77
}
88

99
group = "dev.openfga.intellijplugin"
10-
version = "0.1.0"
10+
version = "0.1.1"
1111
sourceSets["main"].java.srcDirs("src/main/java", "src/generated/java")
1212

1313
repositories {
1414
mavenCentral()
1515
}
1616

1717
dependencies {
18+
implementation("org.antlr:antlr4:4.13.1")
1819
implementation("dev.openfga:openfga-sdk:0.4.2")
1920
implementation("org.dmfs:oauth2-essentials:0.22.1")
2021
implementation("org.dmfs:httpurlconnection-executor:1.22.1")
22+
implementation("org.apache.commons:commons-lang3:3.14.0")
23+
24+
// Until, https://github.com/openfga/language/pkg/java is published,
25+
// the plugin cannot be built without manually building language and providing the jar file
26+
implementation(files("libs/language-0.0.1.jar"))
2127

2228
testImplementation("junit:junit:4.13.2")
2329
}
@@ -28,7 +34,10 @@ intellij {
2834
version.set("2023.3")
2935
type.set("IC") // Target IDE Platform
3036

31-
plugins.set(listOf("org.intellij.intelliLang", "org.jetbrains.plugins.yaml"))
37+
plugins.set(listOf(
38+
"org.intellij.intelliLang",
39+
"org.jetbrains.plugins.yaml",
40+
"com.intellij.java"))
3241
}
3342

3443
grammarKit {

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ org.gradle.caching=true
77

88
pluginGroup=dev.openfga.intellijplugin
99
pluginName=OpenFgaIntellijPlugin
10-
pluginVersion=0.1.0
10+
pluginVersion=0.1.1
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package dev.openfga.intellijplugin.language;
2+
3+
import com.intellij.lang.annotation.AnnotationHolder;
4+
import com.intellij.lang.annotation.ExternalAnnotator;
5+
import com.intellij.lang.annotation.HighlightSeverity;
6+
import com.intellij.openapi.util.TextRange;
7+
import com.intellij.psi.PsiFile;
8+
import dev.openfga.language.errors.DslErrorsException;
9+
import dev.openfga.language.errors.ParsingError;
10+
import dev.openfga.language.errors.StartEnd;
11+
import dev.openfga.language.validation.DslValidator;
12+
import org.jetbrains.annotations.NotNull;
13+
import org.jetbrains.annotations.Nullable;
14+
15+
import java.io.IOException;
16+
import java.util.List;
17+
18+
public class OpenFGAAnnotator extends ExternalAnnotator<String, List<? extends ParsingError>> {
19+
20+
@Override
21+
public @Nullable String collectInformation(@NotNull PsiFile file) {
22+
return file.getText();
23+
}
24+
25+
@Override
26+
public @Nullable List<? extends ParsingError> doAnnotate(@NotNull final String collectedInfo) {
27+
try {
28+
DslValidator.validate(collectedInfo);
29+
} catch (IOException e) {
30+
throw new RuntimeException(e);
31+
} catch (DslErrorsException e) {
32+
return e.getErrors();
33+
}
34+
35+
return null;
36+
}
37+
38+
@Override
39+
public void apply(@NotNull final PsiFile file,
40+
final List<? extends ParsingError> annotationResult,
41+
@NotNull final AnnotationHolder holder) {
42+
43+
final String fileContents = file.getText();
44+
45+
for (ParsingError error : annotationResult) {
46+
final StartEnd startEndLine = error.getLine();
47+
final StartEnd startEndColumn = error.getColumn();
48+
49+
int offsetStart = getOffsetFromRange(fileContents, startEndLine.getStart(), startEndColumn.getStart());
50+
int offsetEnd = getOffsetFromRange(fileContents, startEndLine.getEnd(), startEndColumn.getEnd());
51+
52+
holder.newAnnotation(HighlightSeverity.ERROR, error.getFullMessage())
53+
.range(new TextRange(offsetStart, offsetEnd))
54+
.create();
55+
}
56+
}
57+
58+
private static int getOffsetFromRange(@NotNull final String doc, int line, int character) {
59+
int offset = 0;
60+
61+
final String[] lines = doc.split("\n");
62+
63+
for (int i = 0; i < line; i++) {
64+
offset += lines[i].length() + 1;
65+
}
66+
67+
offset += character;
68+
69+
return offset;
70+
}
71+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package dev.openfga.intellijplugin.language;
2+
3+
import com.intellij.lang.annotation.AnnotationHolder;
4+
import com.intellij.lang.annotation.ExternalAnnotator;
5+
import com.intellij.lang.annotation.HighlightSeverity;
6+
import com.intellij.openapi.util.Pair;
7+
import com.intellij.openapi.util.TextRange;
8+
import com.intellij.psi.PsiElement;
9+
import com.intellij.psi.PsiFile;
10+
import dev.openfga.language.errors.DslErrorsException;
11+
import dev.openfga.language.errors.ParsingError;
12+
import dev.openfga.language.errors.StartEnd;
13+
import dev.openfga.language.validation.DslValidator;
14+
import org.apache.commons.lang3.ObjectUtils;
15+
import org.jetbrains.annotations.NotNull;
16+
import org.jetbrains.annotations.Nullable;
17+
import org.jetbrains.yaml.YAMLUtil;
18+
import org.jetbrains.yaml.psi.YAMLFile;
19+
import org.jetbrains.yaml.psi.impl.YAMLScalarListImpl;
20+
21+
import java.io.IOException;
22+
import java.util.List;
23+
24+
public class OpenFGAStoreAnnotator extends ExternalAnnotator<String, List<? extends ParsingError>> {
25+
26+
@Override
27+
public @Nullable String collectInformation(@NotNull final PsiFile file) {
28+
final Pair<PsiElement, String> modelField = getModelField(file);
29+
30+
// If empty or not a scalar list, return
31+
if (!ObjectUtils.isNotEmpty(modelField) || !(modelField.getFirst() instanceof YAMLScalarListImpl)) {
32+
return null;
33+
}
34+
35+
// When the field is in scalar strip model `getSecond()` returns a trimmed version of the model
36+
// This skews all the original line numbers, so we instead get the unmodified value
37+
if (ObjectUtils.isNotEmpty(modelField) && ObjectUtils.isNotEmpty(modelField.getFirst())) {
38+
// Remove first line which is the scalar notation ('|', '|-', '|+')
39+
return modelField.getFirst().getText().split("\n", 2)[1];
40+
}
41+
42+
return null;
43+
}
44+
45+
@Override
46+
public @Nullable List<? extends ParsingError> doAnnotate(@NotNull final String collectedInfo) {
47+
if (collectedInfo.isEmpty()) {
48+
return null;
49+
}
50+
51+
try {
52+
DslValidator.validate(collectedInfo);
53+
} catch (IOException e) {
54+
throw new RuntimeException("Failure when attempting to validate model", e);
55+
} catch (DslErrorsException e) {
56+
return e.getErrors();
57+
}
58+
59+
return null;
60+
}
61+
62+
// Parsing is difficult: both the trimmed string (model) and the string retaining whitespace (originalString)
63+
// The model is the clean string, whereas the originalString is that from the YAML with extra whitespace
64+
// First the clean string is validated, then the original string is used to determine correct offsets
65+
@Override
66+
public void apply(@NotNull final PsiFile file,
67+
final List<? extends ParsingError> annotationResult,
68+
@NotNull final AnnotationHolder holder) {
69+
final @Nullable Pair<PsiElement, String> fileContents = getModelField(file);
70+
71+
if (ObjectUtils.isEmpty(fileContents)) {
72+
return;
73+
}
74+
75+
final PsiElement key = fileContents.getFirst();
76+
77+
final String originalString = key.getText().split("\n", 2)[1];
78+
// Key offset and newline
79+
int offset = key.getFirstChild().getTextRange().getEndOffset() + 1;
80+
81+
for (ParsingError error : annotationResult) {
82+
final StartEnd startEndLine = error.getLine();
83+
final StartEnd startEndColumn = error.getColumn();
84+
85+
int offsetStart = getOffsetFromRange(
86+
originalString, startEndLine.getStart(), startEndColumn.getStart(), offset);
87+
int offsetEnd = getOffsetFromRange(
88+
originalString, startEndLine.getEnd(), startEndColumn.getEnd(), offset);
89+
90+
holder.newAnnotation(HighlightSeverity.ERROR, error.getMessage())
91+
.range(new TextRange(offsetStart, offsetEnd))
92+
.create();
93+
}
94+
}
95+
96+
private static int getOffsetFromRange(
97+
@NotNull final String doc, int line, int character, int offset) {
98+
final String[] lines = doc.split("\n");
99+
100+
// Count offset
101+
for (int i = 0; i < line; i++) {
102+
// Strip leading spaces to normalize indentation, tabs are banned in YAML.
103+
// This assumes an indent of 1 || 2 spaces, doesn't support 4 as that would bite into indentation
104+
final String replacementLine = lines[i].replaceFirst("^ {1,2}", "");
105+
offset += replacementLine.length() + (lines[i].length() - replacementLine.length()) + 1;
106+
}
107+
108+
return offset + character;
109+
}
110+
111+
private static @Nullable Pair<PsiElement, String> getModelField(@NotNull final PsiFile file) {
112+
final Pair<PsiElement, String> field = YAMLUtil.getValue((YAMLFile) file, "model");
113+
114+
if (ObjectUtils.isNotEmpty(field)) {
115+
return field;
116+
}
117+
return null;
118+
}
119+
}

src/main/resources/META-INF/plugin.xml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@
1515
<li>Generate json file from DSL (requires <a href="https://github.com/openfga/cli">OpenFGA CLI to be installed)</li>
1616
<li>Configure servers in OpenFGA tool window</li>
1717
</ul>
18-
]]></description>
18+
]]></description>
19+
20+
<change-notes><![CDATA[
21+
<h2>New Features</h2>
22+
<ul>
23+
<li>Validation for model files</li>
24+
<li>Validation for the model field in store files</li>
25+
</ul>
26+
]]></change-notes>
1927

2028
<depends>com.intellij.modules.platform</depends>
2129
<depends>com.intellij.modules.lang</depends>
@@ -41,6 +49,14 @@
4149

4250
<highlightVisitor implementation="dev.openfga.intellijplugin.OpenFGAHighlightVisitor"/>
4351

52+
<externalAnnotator
53+
language="OpenFGA"
54+
implementationClass="dev.openfga.intellijplugin.language.OpenFGAAnnotator"/>
55+
56+
<externalAnnotator
57+
language="yaml"
58+
implementationClass="dev.openfga.intellijplugin.language.OpenFGAStoreAnnotator"/>
59+
4460
<colorSettingsPage
4561
implementation="dev.openfga.intellijplugin.OpenFGAColorSettingsPage"/>
4662

0 commit comments

Comments
 (0)