diff --git a/changelog.md b/changelog.md index d67bb4ed..32062bba 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ - structurizr-core: `ViewSet.isEmpty()` was missing a check for image views. - structurizr-dsl: Fixes https://github.com/structurizr/java/issues/252 (DSL parser does not seem to handle curly brackets balance). +- structurizr-dsl: Deprecates `!constant`, adds `!const` and `!var` (see https://github.com/structurizr/java/issues/253). ## 2.0.0 (22nd February 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/Constant.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/Constant.java deleted file mode 100644 index e290a35a..00000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/Constant.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.structurizr.dsl; - -class Constant { - - private String name; - private String value; - - Constant(String name, String value) { - this.name = name; - this.value = value; - } - - String getName() { - return name; - } - - String getValue() { - return value; - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ConstantParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ConstantParser.java deleted file mode 100644 index 4b8c2820..00000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ConstantParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.structurizr.dsl; - -final class ConstantParser extends AbstractParser { - - private static final String GRAMMAR = "!constant "; - - private static final int NAME_INDEX = 1; - private static final int VALUE_INDEX = 2; - - private static final String NAME_REGEX = "[a-zA-Z0-9-_.]+"; - - Constant parse(DslContext context, Tokens tokens) { - // !constant name value - - if (tokens.hasMoreThan(VALUE_INDEX)) { - throw new RuntimeException("Too many tokens, expected: " + GRAMMAR); - } - - if (!tokens.includes(VALUE_INDEX)) { - throw new RuntimeException("Expected: " + GRAMMAR); - } - - String name = tokens.get(NAME_INDEX); - String value = tokens.get(VALUE_INDEX); - - if (!name.matches(NAME_REGEX)) { - throw new RuntimeException("Constant names must only contain the following characters: a-zA-Z0-9-_."); - } - - return new Constant(name, value); - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValuePair.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValuePair.java new file mode 100644 index 00000000..225e7714 --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValuePair.java @@ -0,0 +1,37 @@ +package com.structurizr.dsl; + +class NameValuePair { + + private NameValueType type; + private final String name; + private final String value; + + NameValuePair(String name, String value) { + this.name = name; + this.value = value; + } + + NameValueType getType() { + return type; + } + + void setType(NameValueType type) { + this.type = type; + } + + String getName() { + return name; + } + + String getValue() { + return value; + } + +} + +enum NameValueType { + + Constant, + Variable + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValueParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValueParser.java new file mode 100644 index 00000000..ad16b1fc --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/NameValueParser.java @@ -0,0 +1,49 @@ +package com.structurizr.dsl; + +final class NameValueParser extends AbstractParser { + + private static final String GRAMMAR = "%s "; + + private static final int KEYWORD_INDEX = 0; + private static final int NAME_INDEX = 1; + private static final int VALUE_INDEX = 2; + + private static final String NAME_REGEX = "[a-zA-Z0-9-_.]+"; + + NameValuePair parseConstant(Tokens tokens) { + NameValuePair nvp = parse(tokens); + nvp.setType(NameValueType.Constant); + + return nvp; + } + + NameValuePair parseVariable(Tokens tokens) { + NameValuePair nvp = parse(tokens); + nvp.setType(NameValueType.Variable); + + return nvp; + } + + private NameValuePair parse(Tokens tokens) { + // !const name value + // !var name value + + if (tokens.hasMoreThan(VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: " + String.format(GRAMMAR, tokens.get(KEYWORD_INDEX))); + } + + if (!tokens.includes(VALUE_INDEX)) { + throw new RuntimeException("Expected: " + String.format(GRAMMAR, tokens.get(KEYWORD_INDEX))); + } + + String name = tokens.get(NAME_INDEX); + String value = tokens.get(VALUE_INDEX); + + if (!name.matches(NAME_REGEX)) { + throw new RuntimeException("Constant/variable names must only contain the following characters: a-zA-Z0-9-_."); + } + + return new NameValuePair(name, value); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index b640ac47..080f52f2 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -42,7 +42,7 @@ public final class StructurizrDslParser extends StructurizrDslTokens { private final Stack contextStack; private final Set parsedTokens = new HashSet<>(); private final IdentifiersRegister identifiersRegister; - private final Map constants; + private final Map constantsAndVariables; private final List dslSourceLines = new ArrayList<>(); private Workspace workspace; @@ -56,7 +56,7 @@ public final class StructurizrDslParser extends StructurizrDslTokens { public StructurizrDslParser() { contextStack = new Stack<>(); identifiersRegister = new IdentifiersRegister(); - constants = new HashMap<>(); + constantsAndVariables = new HashMap<>(); } /** @@ -872,11 +872,29 @@ void parse(List lines, File dslFile, boolean include) throws Structurizr } } else if (CONSTANT_TOKEN.equalsIgnoreCase(firstToken)) { - Constant constant = new ConstantParser().parse(getContext(), tokens); - if (constants.containsKey(constant.getName())) { - log.warn("A constant named " + constant.getName() + " already exists"); + log.warn("!constant has been deprecated and will be removed in a future release - please use !const or !var instead"); + NameValuePair nameValuePair = new NameValueParser().parseConstant(tokens); + + if (constantsAndVariables.containsKey(nameValuePair.getName())) { + log.warn("A constant \"" + nameValuePair.getName() + "\" already exists"); + } + constantsAndVariables.put(nameValuePair.getName(), nameValuePair); + + } else if (CONST_TOKEN.equalsIgnoreCase(firstToken)) { + NameValuePair nameValuePair = new NameValueParser().parseConstant(tokens); + + if (constantsAndVariables.containsKey(nameValuePair.getName())) { + throw new StructurizrDslParserException("A constant/variable \"" + nameValuePair.getName() + "\" already exists"); + } + constantsAndVariables.put(nameValuePair.getName(), nameValuePair); + + } else if (VAR_TOKEN.equalsIgnoreCase(firstToken)) { + NameValuePair nameValuePair = new NameValueParser().parseVariable(tokens); + + if (constantsAndVariables.containsKey(nameValuePair.getName()) && constantsAndVariables.get(nameValuePair.getName()).getType() == NameValueType.Constant) { + throw new StructurizrDslParserException("A constant \"" + nameValuePair.getName() + "\" already exists"); } - constants.put(constant.getName(), constant); + constantsAndVariables.put(nameValuePair.getName(), nameValuePair); } else if (IDENTIFIERS_TOKEN.equalsIgnoreCase(firstToken) && (inContext(WorkspaceDslContext.class) || inContext(ModelDslContext.class))) { setIdentifierScope(new IdentifierScopeParser().parse(getContext(), tokens)); @@ -963,8 +981,8 @@ private String substituteStrings(String token) { String before = m.group(0); String after = null; String name = before.substring(2, before.length()-1); - if (constants.containsKey(name)) { - after = constants.get(name).getValue(); + if (constantsAndVariables.containsKey(name)) { + after = constantsAndVariables.get(name).getValue(); } else { if (!restricted) { String environmentVariable = System.getenv().get(name); diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParserException.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParserException.java index 9868ba0a..70dc54d4 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParserException.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParserException.java @@ -23,7 +23,7 @@ public final class StructurizrDslParserException extends Exception { } StructurizrDslParserException(String message, File dslFile, int lineNumber, String line) { - super((message.endsWith(".") ? message.substring(0, message.length()-1) : message) + " at line " + lineNumber + (dslFile != null ? " of " + dslFile.getAbsolutePath() : "") + ": " + line.trim()); + super((message.endsWith(".") ? message.substring(0, message.length()-1) : message) + " at line " + lineNumber + (dslFile != null && dslFile.isFile() ? " of " + dslFile.getAbsolutePath() : "") + ": " + line.trim()); this.lineNumber = lineNumber; this.line = line; } diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java index 5884b022..35ace225 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslTokens.java @@ -100,6 +100,8 @@ class StructurizrDslTokens { static final String ADRS_TOKEN = "!adrs"; static final String DECISIONS_TOKEN = "!decisions"; static final String CONSTANT_TOKEN = "!constant"; + static final String CONST_TOKEN = "!const"; + static final String VAR_TOKEN = "!var"; static final String IDENTIFIERS_TOKEN = "!identifiers"; static final String IMPLIED_RELATIONSHIPS_TOKEN = "!impliedRelationships"; static final String REF_TOKEN = "!ref"; diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ConstantParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ConstantParserTests.java deleted file mode 100644 index 46d645e4..00000000 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ConstantParserTests.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.structurizr.dsl; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - -class ConstantParserTests extends AbstractTests { - - private ConstantParser parser = new ConstantParser(); - - @Test - void test_parse_ThrowsAnException_WhenThereAreTooManyTokens() { - try { - parser.parse(context(), tokens("!constant", "name", "value", "extra")); - fail(); - } catch (Exception e) { - assertEquals("Too many tokens, expected: !constant ", e.getMessage()); - } - } - - @Test - void test_parse_ThrowsAnException_WhenNoNameOrValueIsSpecified() { - try { - parser.parse(context(), tokens("!constant")); - fail(); - } catch (Exception e) { - assertEquals("Expected: !constant ", e.getMessage()); - } - } - - @Test - void test_parse_ThrowsAnException_WhenNoValueIsSpecified() { - try { - parser.parse(context(), tokens("!constant", "name")); - fail(); - } catch (Exception e) { - assertEquals("Expected: !constant ", e.getMessage()); - } - } - - @Test - void test_parse_ThrowsAnException_WhenNameContainsDisallowedCharacters() { - try { - parser.parse(context(), tokens("!constant", "${NAME}", "value")); - fail(); - } catch (Exception e) { - assertEquals("Constant names must only contain the following characters: a-zA-Z0-9-_.", e.getMessage()); - } - } - - @Test - void test_parse_CreatesAConstant() { - Constant constant = parser.parse(context(), tokens("!constant", "name", "value")); - assertEquals("name", constant.getName()); - assertEquals("value", constant.getValue()); - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java index 7cec66f7..e8f7fa5c 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/DslTests.java @@ -364,17 +364,17 @@ void test_includeLocalDirectory() throws Exception { assertEquals("workspace {\n" + "\n" + " model {\n" + - " !constant SOFTWARE_SYSTEM_NAME \"Software System 1\"\n" + + " !var SOFTWARE_SYSTEM_NAME \"Software System 1\"\n" + " softwareSystem \"${SOFTWARE_SYSTEM_NAME}\" {\n" + " !docs ../../docs\n" + " }\n" + "\n" + - " !constant SOFTWARE_SYSTEM_NAME \"Software System 2\"\n" + + " !var SOFTWARE_SYSTEM_NAME \"Software System 2\"\n" + " softwareSystem \"${SOFTWARE_SYSTEM_NAME}\" {\n" + " !docs ../../docs\n" + " }\n" + "\n" + - " !constant SOFTWARE_SYSTEM_NAME \"Software System 3\"\n" + + " !var SOFTWARE_SYSTEM_NAME \"Software System 3\"\n" + " softwareSystem \"${SOFTWARE_SYSTEM_NAME}\" {\n" + " !docs ../../docs\n" + " }\n" + @@ -1076,4 +1076,36 @@ void test_UnbalancedCurlyBraces() { } } + @Test + void test_Const() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(""" + workspace { + !const name value1 + !const name value2 + } + """); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("A constant/variable \"name\" already exists at line 3: !const name value2", e.getMessage()); + } + } + + @Test + void test_Var_CannotOverrideConst() { + try { + StructurizrDslParser parser = new StructurizrDslParser(); + parser.parse(""" + workspace { + !const name value1 + !var name value2 + } + """); + fail(); + } catch (StructurizrDslParserException e) { + assertEquals("A constant \"name\" already exists at line 3: !var name value2", e.getMessage()); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/NameValueParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/NameValueParserTests.java new file mode 100644 index 00000000..7c6a738a --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/NameValueParserTests.java @@ -0,0 +1,68 @@ +package com.structurizr.dsl; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class NameValueParserTests extends AbstractTests { + + private final NameValueParser parser = new NameValueParser(); + + @Test + void test_parseConstant_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + parser.parseConstant(tokens("!const", "name", "value", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: !const ", e.getMessage()); + } + } + + @Test + void test_parseConstant_ThrowsAnException_WhenNoNameOrValueIsSpecified() { + try { + parser.parseConstant(tokens("!const")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !const ", e.getMessage()); + } + } + + @Test + void test_parseConstant_ThrowsAnException_WhenNoValueIsSpecified() { + try { + parser.parseConstant(tokens("!const", "name")); + fail(); + } catch (Exception e) { + assertEquals("Expected: !const ", e.getMessage()); + } + } + + @Test + void test_parseConstant_ThrowsAnException_WhenNameContainsDisallowedCharacters() { + try { + parser.parseConstant(tokens("!const", "${NAME}", "value")); + fail(); + } catch (Exception e) { + assertEquals("Constant/variable names must only contain the following characters: a-zA-Z0-9-_.", e.getMessage()); + } + } + + @Test + void test_parseConstant_CreatesAConstant() { + NameValuePair nameValuePair = parser.parseConstant(tokens("!const", "name", "value")); + assertEquals("name", nameValuePair.getName()); + assertEquals("value", nameValuePair.getValue()); + assertEquals(NameValueType.Constant, nameValuePair.getType()); + } + + @Test + void test_parseVariable_CreatesAVariable() { + NameValuePair nameValuePair = parser.parseVariable(tokens("!var", "name", "value")); + assertEquals("name", nameValuePair.getName()); + assertEquals("value", nameValuePair.getValue()); + assertEquals(NameValueType.Variable, nameValuePair.getType()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl index 140b822a..ec0dc243 100644 --- a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/internet-banking-system.dsl @@ -1,4 +1,4 @@ -!constant INTERNET_BANKING_SYSTEM_INCLUDE "details.dsl" +!const INTERNET_BANKING_SYSTEM_INCLUDE "details.dsl" workspace "Big Bank plc - Internet Banking System" "The software architecture of the Big Bank plc Internet Banking System." { diff --git a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/system-landscape.dsl b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/system-landscape.dsl index f6032960..ac7dbf56 100644 --- a/structurizr-dsl/src/test/resources/dsl/big-bank-plc/system-landscape.dsl +++ b/structurizr-dsl/src/test/resources/dsl/big-bank-plc/system-landscape.dsl @@ -1,4 +1,4 @@ -!constant INTERNET_BANKING_SYSTEM_INCLUDE "summary.dsl" +!const INTERNET_BANKING_SYSTEM_INCLUDE "summary.dsl" workspace "Big Bank plc - System Landscape" "The system landscape for Big Bank plc." { diff --git a/structurizr-dsl/src/test/resources/dsl/include-directory.dsl b/structurizr-dsl/src/test/resources/dsl/include-directory.dsl index 88e4a04b..d402b5e9 100644 --- a/structurizr-dsl/src/test/resources/dsl/include-directory.dsl +++ b/structurizr-dsl/src/test/resources/dsl/include-directory.dsl @@ -1,13 +1,13 @@ workspace { model { - !constant SOFTWARE_SYSTEM_NAME "Software System 1" + !var SOFTWARE_SYSTEM_NAME "Software System 1" !include include/model/software-system/model.dsl - !constant SOFTWARE_SYSTEM_NAME "Software System 2" + !var SOFTWARE_SYSTEM_NAME "Software System 2" !include include/model/software-system - !constant SOFTWARE_SYSTEM_NAME "Software System 3" + !var SOFTWARE_SYSTEM_NAME "Software System 3" !include include/model } diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index 4748da87..3fe2f64d 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -1,5 +1,8 @@ -!constant ORGANISATION_NAME "Organisation" -!constant GROUP_NAME "Group" +!const ORGANISATION_NAME "Organisation" +!const GROUP_NAME "Group" + +!var name abc +!var name ABC workspace "Name" "Description" {