diff --git a/docs/changelog.md b/docs/changelog.md index c1770424..ad8485f4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,7 @@ - Adds a flag to determine whether automatic layout has been applied or not. - Adds support for perspective values. +- Adds the ability to scope and validate a workspace. ## 1.27.0 (23rd October 2023) diff --git a/structurizr-core/src/com/structurizr/Workspace.java b/structurizr-core/src/com/structurizr/Workspace.java index 3c2dc033..e95cdd75 100644 --- a/structurizr-core/src/com/structurizr/Workspace.java +++ b/structurizr-core/src/com/structurizr/Workspace.java @@ -23,6 +23,8 @@ public final class Workspace extends AbstractWorkspace implements Documentable { private static final Log log = LogFactory.getLog(Workspace.class); + private WorkspaceScope scope = null; + private Model model = createModel(); private ViewSet viewSet; private Documentation documentation; @@ -44,6 +46,24 @@ public Workspace(String name, String description) { documentation = new Documentation(); } + /** + * Gets the scope of this workspace + * + * @return a WorkspaceScope enum, or null if undefined + */ + public WorkspaceScope getScope() { + return scope; + } + + /** + * Sets the workspace scope. + * + * @param scope a WorkspaceScope enum, or null if undefined + */ + public void setScope(WorkspaceScope scope) { + this.scope = scope; + } + /** * Gets the software architecture model. * diff --git a/structurizr-core/src/com/structurizr/WorkspaceScope.java b/structurizr-core/src/com/structurizr/WorkspaceScope.java new file mode 100644 index 00000000..2f6ebdad --- /dev/null +++ b/structurizr-core/src/com/structurizr/WorkspaceScope.java @@ -0,0 +1,8 @@ +package com.structurizr; + +public enum WorkspaceScope { + + Landscape, + SoftwareSystem + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java b/structurizr-core/src/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java new file mode 100644 index 00000000..feb6b4ab --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/LandscapeWorkspaceScopeValidator.java @@ -0,0 +1,26 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.model.Model; +import com.structurizr.model.SoftwareSystem; + +/** + * Validates that the workspace does not define containers and software system level documentation. + */ +public class LandscapeWorkspaceScopeValidator implements WorkspaceScopeValidator { + + @Override + public void validate(Workspace workspace) throws WorkspaceScopeValidationException { + Model model = workspace.getModel(); + for (SoftwareSystem softwareSystem : model.getSoftwareSystems()) { + if (softwareSystem.getContainers().size() > 0) { + throw new WorkspaceScopeValidationException("Workspace is landscape scoped, but the software system named " + softwareSystem.getName() + " has containers."); + } + + if (!softwareSystem.getDocumentation().isEmpty()) { + throw new WorkspaceScopeValidationException("Workspace is landscape scoped, but the software system named " + softwareSystem.getName() + " has documentation."); + } + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java b/structurizr-core/src/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java new file mode 100644 index 00000000..d2f7ce32 --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidator.java @@ -0,0 +1,21 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.model.Model; + +/** + * Validates that the workspace only defines detail (containers and documentation) for a single software system. + */ +public class SoftwareSystemWorkspaceScopeValidator implements WorkspaceScopeValidator { + + @Override + public void validate(Workspace workspace) throws WorkspaceScopeValidationException { + Model model = workspace.getModel(); + long softwareSystemsWithContainersOrDocumentation = model.getSoftwareSystems().stream().filter(ss -> ss.getContainers().size() > 0 || !ss.getDocumentation().isEmpty()).count(); + + if (softwareSystemsWithContainersOrDocumentation > 1) { + throw new WorkspaceScopeValidationException("Workspace is software system scoped, but multiple software systems have containers and/or documentation defined."); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java b/structurizr-core/src/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java new file mode 100644 index 00000000..ffd4d386 --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/UndefinedWorkspaceScopeValidator.java @@ -0,0 +1,12 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; + +public class UndefinedWorkspaceScopeValidator implements WorkspaceScopeValidator { + + @Override + public void validate(Workspace workspace) throws WorkspaceScopeValidationException { + // no-op + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidationException.java b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidationException.java new file mode 100644 index 00000000..5183f3d3 --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidationException.java @@ -0,0 +1,9 @@ +package com.structurizr.validation; + +public class WorkspaceScopeValidationException extends Exception { + + public WorkspaceScopeValidationException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidator.java b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidator.java new file mode 100644 index 00000000..c5436173 --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidator.java @@ -0,0 +1,9 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; + +public interface WorkspaceScopeValidator { + + void validate(Workspace workspace) throws WorkspaceScopeValidationException; + +} \ No newline at end of file diff --git a/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java new file mode 100644 index 00000000..b0149178 --- /dev/null +++ b/structurizr-core/src/com/structurizr/validation/WorkspaceScopeValidatorFactory.java @@ -0,0 +1,18 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.WorkspaceScope; + +public final class WorkspaceScopeValidatorFactory { + + public static WorkspaceScopeValidator getValidator(Workspace workspace) { + if (workspace.getScope() == WorkspaceScope.Landscape) { + return new LandscapeWorkspaceScopeValidator(); + } else if (workspace.getScope() == WorkspaceScope.SoftwareSystem) { + return new SoftwareSystemWorkspaceScopeValidator(); + } else { + return new UndefinedWorkspaceScopeValidator(); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java index 651057bb..055868a2 100644 --- a/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java +++ b/structurizr-core/test/unit/com/structurizr/WorkspaceTests.java @@ -73,4 +73,16 @@ void hydrate_DoesNotCrash() { workspace.hydrate(); } + @Test + void scope() { + Workspace workspace = new Workspace("Name", "Description"); + assertNull(workspace.getScope()); // default scope is undefined + + workspace.setScope(WorkspaceScope.SoftwareSystem); + assertEquals(WorkspaceScope.SoftwareSystem, workspace.getScope()); + + workspace.setScope(null); + assertNull(workspace.getScope()); + } + } \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java b/structurizr-core/test/unit/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java new file mode 100644 index 00000000..9e473da2 --- /dev/null +++ b/structurizr-core/test/unit/com/structurizr/validation/LandscapeWorkspaceScopeValidatorTests.java @@ -0,0 +1,56 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.validation.LandscapeWorkspaceScopeValidator; +import com.structurizr.validation.WorkspaceScopeValidationException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class LandscapeWorkspaceScopeValidatorTests { + + private final LandscapeWorkspaceScopeValidator validator = new LandscapeWorkspaceScopeValidator(); + + @Test + void validate() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + validator.validate(workspace); + + workspace.getModel().addPerson("User"); + validator.validate(workspace); + + workspace.getModel().addSoftwareSystem("A"); + validator.validate(workspace); + + workspace.getModel().addSoftwareSystem("B"); + validator.validate(workspace); + } + + @Test + void validate_ThrowsAnException_WhenContainersAreDefined() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").addContainer("AA"); + try { + validator.validate(workspace); + fail(); + } catch (WorkspaceScopeValidationException e) { + assertEquals("Workspace is landscape scoped, but the software system named A has containers.", e.getMessage()); + } + } + + @Test + void validate_ThrowsAnException_WhenSoftwareSystemDocumentationIsDefined() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + try { + validator.validate(workspace); + fail(); + } catch (WorkspaceScopeValidationException e) { + assertEquals("Workspace is landscape scoped, but the software system named A has documentation.", e.getMessage()); + } + } + +} diff --git a/structurizr-core/test/unit/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java b/structurizr-core/test/unit/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java new file mode 100644 index 00000000..be87ea63 --- /dev/null +++ b/structurizr-core/test/unit/com/structurizr/validation/SoftwareSystemWorkspaceScopeValidatorTests.java @@ -0,0 +1,61 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.documentation.Format; +import com.structurizr.documentation.Section; +import com.structurizr.model.SoftwareSystem; +import com.structurizr.validation.SoftwareSystemWorkspaceScopeValidator; +import com.structurizr.validation.WorkspaceScopeValidationException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class SoftwareSystemWorkspaceScopeValidatorTests { + + private final SoftwareSystemWorkspaceScopeValidator validator = new SoftwareSystemWorkspaceScopeValidator(); + + @Test + void validate() throws Exception { + Workspace workspace = new Workspace("Name", "Description"); + validator.validate(workspace); + + workspace.getModel().addPerson("User"); + validator.validate(workspace); + + SoftwareSystem a = workspace.getModel().addSoftwareSystem("A"); + a.addContainer("AA"); + a.getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + validator.validate(workspace); + + workspace.getModel().addSoftwareSystem("B"); + validator.validate(workspace); + } + + @Test + void validate_ThrowsAnException_WhenMultipleSoftwareSystemsDefineContainers() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").addContainer("AA"); + workspace.getModel().addSoftwareSystem("B").addContainer("BB"); + try { + validator.validate(workspace); + fail(); + } catch (WorkspaceScopeValidationException e) { + assertEquals("Workspace is software system scoped, but multiple software systems have containers and/or documentation defined.", e.getMessage()); + } + } + + @Test + void validate_ThrowsAnException_WhenMultipleSoftwareSystemsDefineDocumentation() { + Workspace workspace = new Workspace("Name", "Description"); + workspace.getModel().addSoftwareSystem("A").getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + workspace.getModel().addSoftwareSystem("B").getDocumentation().addSection(new Section(Format.Markdown, "## Heading 1")); + try { + validator.validate(workspace); + fail(); + } catch (WorkspaceScopeValidationException e) { + assertEquals("Workspace is software system scoped, but multiple software systems have containers and/or documentation defined.", e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java b/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java new file mode 100644 index 00000000..a8b9e4a8 --- /dev/null +++ b/structurizr-core/test/unit/com/structurizr/validation/WorkspaceScopeValidatorFactoryTests.java @@ -0,0 +1,23 @@ +package com.structurizr.validation; + +import com.structurizr.Workspace; +import com.structurizr.WorkspaceScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class WorkspaceScopeValidatorFactoryTests { + + @Test + void getValidator() { + Workspace workspace = new Workspace("Name", "Description"); + assertTrue(WorkspaceScopeValidatorFactory.getValidator(workspace) instanceof UndefinedWorkspaceScopeValidator); + + workspace.setScope(WorkspaceScope.Landscape); + assertTrue(WorkspaceScopeValidatorFactory.getValidator(workspace) instanceof LandscapeWorkspaceScopeValidator); + + workspace.setScope(WorkspaceScope.SoftwareSystem); + assertTrue(WorkspaceScopeValidatorFactory.getValidator(workspace) instanceof SoftwareSystemWorkspaceScopeValidator); + } + +} \ No newline at end of file