Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion pkg/java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,32 @@ var jsonMod = new FgaModTransformer(modFileContents).transform();

### Transform set of Modules To Model

Not yet implemented, but the [enchancement is in our issue backlog](https://github.com/openfga/language/issues/279).
```java
import dev.openfga.language.ModuleFile;
import dev.openfga.language.ModulesToModelTransformer;

...

var modules = List.of(
new ModuleFile("core.fga", """
module core

type user
"""),
new ModuleFile("wiki.fga", """
module wiki

type page
relations
define owner: [user]
"""));

// Merge the modules into a single AuthorizationModel
var model = ModulesToModelTransformer.transformToModel(modules, "1.2");

// Or get the merged model as a JSON string
var json = ModulesToModelTransformer.transform(modules, "1.2");
```

### Validation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
Expand Down Expand Up @@ -42,6 +43,17 @@ private String cleanLine(String line) {
}

public Result parseDsl(String dsl) {
var listener = walk(dsl);
return new Result(listener.getAuthorizationModel(), listener.errorListener.getErrors());
}

public ModularResult parseModularDsl(String dsl) {
var listener = walk(dsl);
return new ModularResult(
listener.getAuthorizationModel(), listener.errorListener.getErrors(), listener.getTypeDefExtensions());
}

private OpenFgaDslListener walk(String dsl) {
var cleanedDsl = Arrays.stream(dsl.split("\n")).map(this::cleanLine).collect(joining("\n"));

var antlrStream = CharStreams.fromString(cleanedDsl);
Expand All @@ -57,9 +69,10 @@ public Result parseDsl(String dsl) {
parser.addErrorListener(errorListener);

var listener = new OpenFgaDslListener(parser);
listener.errorListener = errorListener;
new ParseTreeWalker().walk(listener, parser.main());

return new Result(listener.getAuthorizationModel(), errorListener.getErrors());
return listener;
}

public static final class Result {
Expand Down Expand Up @@ -87,4 +100,33 @@ public boolean IsFailure() {
return !IsSuccess();
}
}

public static final class ModularResult {
private final AuthorizationModel authorizationModel;
private final List<SyntaxError> errors;
private final Set<String> typeDefExtensions;

public ModularResult(
AuthorizationModel authorizationModel, List<SyntaxError> errors, Set<String> typeDefExtensions) {
this.authorizationModel = authorizationModel;
this.errors = errors;
this.typeDefExtensions = typeDefExtensions;
}

public AuthorizationModel getAuthorizationModel() {
return authorizationModel;
}

public List<SyntaxError> getErrors() {
return errors;
}

public Set<String> getTypeDefExtensions() {
return typeDefExtensions;
}

public boolean isFailure() {
return !errors.isEmpty();
}
}
}
193 changes: 170 additions & 23 deletions pkg/java/src/main/java/dev/openfga/language/JsonToDslTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import dev.openfga.language.errors.UnsupportedDSLNestingException;
import dev.openfga.language.errors.UnsupportedModularModulesException;
import dev.openfga.sdk.api.model.*;
import java.util.*;
import java.util.function.Function;
Expand All @@ -14,10 +15,15 @@ public class JsonToDslTransformer {

private static final String EOL = System.lineSeparator();

private boolean includeSourceInformation = false;

public String transform(String json) throws JsonProcessingException {
return transform(json, false);
}

public String transform(String json, boolean includeSourceInformation) throws JsonProcessingException {
this.includeSourceInformation = includeSourceInformation;
var model = JSON.parse(json, AuthorizationModel.class);

return transformJSONToDSL(model);
}

Expand All @@ -27,24 +33,41 @@ private String transformJSONToDSL(AuthorizationModel model) {
schemaVersion = model.getSchemaVersion();
}

var modular = isModularModel(model);

var formattedTypeDefinitions = new StringBuilder();
if (model != null && model.getTypeDefinitions() != null) {
for (var typeDefinition : model.getTypeDefinitions()) {
formattedTypeDefinitions.append(formatType(typeDefinition)).append(EOL);
var typeDefinitions = new ArrayList<>(model.getTypeDefinitions());
if (modular) {
typeDefinitions.sort((a, b) -> sortByModule(
a.getType(),
b.getType(),
module(a.getMetadata()),
module(b.getMetadata()),
file(a.getMetadata()),
file(b.getMetadata())));
}
for (var typeDefinition : typeDefinitions) {
formattedTypeDefinitions
.append(formatType(typeDefinition, modular))
.append(EOL);
}
}

var fomattedConditions = formatConditions(model);
var fomattedConditions = formatConditions(model, modular);

return "model" + EOL + " schema " + schemaVersion + EOL + formattedTypeDefinitions + fomattedConditions;
}

private String formatType(TypeDefinition typeDef) {
private String formatType(TypeDefinition typeDef, boolean modular) {
var typeName = typeDef.getType();
var formatedTypeBuilder = new StringBuilder(EOL).append("type ").append(typeName);
var metadata = typeDef.getMetadata();
var formatedTypeBuilder = new StringBuilder(EOL)
.append("type ")
.append(typeName)
.append(constructSourceComment(module(metadata), file(metadata), ""));

var relations = requireNonNullElseGet(typeDef.getRelations(), HashMap<String, Userset>::new);
var metadata = typeDef.getMetadata();
var emptyMetadataRelation = new HashMap<String, RelationMetadata>();
var metadataRelations = metadata != null ? metadata.getRelations() : emptyMetadataRelation;
if (metadataRelations == null) {
Expand All @@ -53,12 +76,15 @@ private String formatType(TypeDefinition typeDef) {

if (!relations.isEmpty()) {
formatedTypeBuilder.append(EOL).append(" relations");
for (var relationEntry : relations.entrySet()) {
var relationName = relationEntry.getKey();
var relationDefinition = relationEntry.getValue();
metadataRelations.get(relationName);
var formattedRelationString =
formatRelation(typeName, relationName, relationDefinition, metadataRelations.get(relationName));
var relationNames = new ArrayList<>(relations.keySet());
if (modular) {
var meta = metadataRelations;
relationNames.sort((a, b) -> sortByModule(
a, b, module(meta.get(a)), module(meta.get(b)), file(meta.get(a)), file(meta.get(b))));
}
for (var relationName : relationNames) {
var formattedRelationString = formatRelation(
typeName, relationName, relations.get(relationName), metadataRelations.get(relationName));
formatedTypeBuilder.append(EOL).append(formattedRelationString);
}
Comment on lines +85 to 89
}
Expand Down Expand Up @@ -87,7 +113,9 @@ private String formatRelation(
formatter.format(typeName, relationName, relationDefinition, typeRestrictions, validator);
if (validator.occurences() == 0
|| (validator.occurences() == 1 && validator.isFirstPosition(relationDefinition))) {
return " define " + relationName + ": " + formattedRelation;
var sourceComment =
constructSourceComment(module(relationMetadata), file(relationMetadata), " extended by:");
return " define " + relationName + ": " + formattedRelation + sourceComment;
}

throw new UnsupportedDSLNestingException(typeName, relationName);
Expand Down Expand Up @@ -308,21 +336,28 @@ private CharSequence formatTupleToUserset(Userset relationDefinition) {
return new StringBuilder(computedUserset).append(" from ").append(tupleset);
}

private CharSequence formatConditions(AuthorizationModel model) {
private CharSequence formatConditions(AuthorizationModel model, boolean modular) {
var conditions = model.getConditions();
if (conditions == null || conditions.isEmpty()) {
return "";
}

var formattedConditions = new StringBuilder();
var sortedCondition = new TreeMap<>(conditions);

for (var conditionEntry : sortedCondition.entrySet()) {
var conditionName = conditionEntry.getKey();
var conditionDef = conditionEntry.getValue();
var conditionNames = new ArrayList<>(conditions.keySet());
if (modular) {
conditionNames.sort((a, b) -> sortByModule(
a,
b,
module(conditions.get(a).getMetadata()),
module(conditions.get(b).getMetadata()),
file(conditions.get(a).getMetadata()),
file(conditions.get(b).getMetadata())));
} else {
conditionNames.sort(null);
}

var formattedCondition = formatCondition(conditionName, conditionDef);
formattedConditions.append(EOL).append(formattedCondition);
var formattedConditions = new StringBuilder();
for (var conditionName : conditionNames) {
formattedConditions.append(EOL).append(formatCondition(conditionName, conditions.get(conditionName)));
}

return formattedConditions;
Expand All @@ -344,6 +379,8 @@ private CharSequence formatCondition(String conditionName, Condition conditionDe
.append(conditionDef.getExpression())
.append(EOL)
.append('}')
.append(constructSourceComment(
module(conditionDef.getMetadata()), file(conditionDef.getMetadata()), ""))
.append(EOL);
}

Expand Down Expand Up @@ -385,4 +422,114 @@ CharSequence format(
List<RelationReference> typeRestrictions,
DirectAssignmentValidator validator);
}

private CharSequence constructSourceComment(String module, String file, String leadingString) {
if (!includeSourceInformation || (isBlank(module) && isBlank(file))) {
return "";
}
return " #" + leadingString + " module: " + nullToEmpty(module) + ", file: " + nullToEmpty(file);
}

private static boolean isModularModel(AuthorizationModel model) {
if (model == null || model.getTypeDefinitions() == null) {
return false;
}
return model.getTypeDefinitions().stream().anyMatch(typeDef -> !isBlank(module(typeDef.getMetadata())));
}
Comment on lines +433 to +438

/** Returns the alphabetically sorted set of modules declared in a modular model, or empty if not modular. */
public static List<String> getModulesFromJSON(AuthorizationModel model) {
var schemaVersion = model != null && model.getSchemaVersion() != null ? model.getSchemaVersion() : "1.1";
if (!schemaVersion.equals("1.2")) {
throw new UnsupportedModularModulesException(schemaVersion);
}

if (!isModularModel(model)) {
return List.of();
}

var modules = new TreeSet<String>();
for (var typeDef : model.getTypeDefinitions()) {
var metadata = typeDef.getMetadata();
if (metadata == null) {
continue;
}
if (!isBlank(metadata.getModule())) {
modules.add(metadata.getModule());
}
if (metadata.getRelations() != null) {
metadata.getRelations().values().stream()
.map(RelationMetadata::getModule)
.filter(m -> !isBlank(m))
.forEach(modules::add);
}
}

if (model.getConditions() != null) {
model.getConditions().values().stream()
.map(condition -> module(condition.getMetadata()))
.filter(m -> !isBlank(m))
.forEach(modules::add);
}

return new ArrayList<>(modules);
}

private static int sortByModule(
String aName, String bName, String aModule, String bModule, String aFile, String bFile) {
if (isBlank(aModule) && isBlank(bModule)) {
return aName.compareTo(bName);
}
if (isBlank(aModule)) {
return -1;
}
if (isBlank(bModule)) {
return 1;
}
if (!aModule.equals(bModule)) {
return aModule.compareTo(bModule);
}
if (!nullToEmpty(aFile).equals(nullToEmpty(bFile))) {
return nullToEmpty(aFile).compareTo(nullToEmpty(bFile));
}
return aName.compareTo(bName);
}

private static String module(Metadata metadata) {
return metadata == null ? null : metadata.getModule();
}

private static String module(RelationMetadata metadata) {
return metadata == null ? null : metadata.getModule();
}

private static String module(ConditionMetadata metadata) {
return metadata == null ? null : metadata.getModule();
}

private static String file(Metadata metadata) {
return metadata == null || metadata.getSourceInfo() == null
? null
: metadata.getSourceInfo().getFile();
}

private static String file(RelationMetadata metadata) {
return metadata == null || metadata.getSourceInfo() == null
? null
: metadata.getSourceInfo().getFile();
}

private static String file(ConditionMetadata metadata) {
return metadata == null || metadata.getSourceInfo() == null
? null
: metadata.getSourceInfo().getFile();
}

private static boolean isBlank(String value) {
return value == null || value.isEmpty();
}

private static String nullToEmpty(String value) {
return value == null ? "" : value;
}
}
Loading