Skip to content

Commit

Permalink
Add new Endpoint and Calls DSL (#503)
Browse files Browse the repository at this point in the history
* Add new `Endpoint` and `Calls` DSL

These make it possible to provide information on endpoints used on
containers (services). The generated expressions contain the information
which also gets written to the serialized JSON.

Note that the endpoint information is not currently used by the
diagramming tool.

* Code review fixes
  • Loading branch information
raphael authored Sep 1, 2023
1 parent 4c1dc02 commit 1f704bd
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 1 deletion.
48 changes: 48 additions & 0 deletions dsl/elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,54 @@ func Component(name string, args ...interface{}) *expr.Component {
return container.AddComponent(c)
}

// Endpoint defines an endpoint on a container. The endpoint may be a REST
// endpoint, a gRPC endpoint or any other kind of endpoint.
//
// Endpoint must appear in a Container expression.
//
// Endpoint takes 1 to 2 arguments: the endpoint name and an optional
// description.
//
// The valid syntax for Endpoint is thus:
//
// Endpoint("<name>", "[description]")
//
// Example:
//
// var _ = Design(func() {
// SoftwareSystem("My system", "A system with a great architecture", func() {
// Container("My container", "A container with a great architecture", "Go and Goa", func() {
// Endpoint("MyEndpoint", "An endpoint")
// })
// })
// })
func Endpoint(name string, args ...interface{}) {
container, ok := eval.Current().(*expr.Container)
if !ok {
eval.IncompatibleDSL()
return
}
if len(args) > 1 {
eval.ReportError("Endpoint: too many arguments")
return
}
description, _, _, err := parseElementArgs(args...)
if err != nil {
eval.ReportError("Endpoint: " + err.Error())
return
}
for _, e := range container.Endpoints {
if e.Name == name {
eval.ReportError("Endpoint %q already defined", name)
return
}
}
container.Endpoints = append(container.Endpoints, &expr.Endpoint{
Name: name,
Description: description,
})
}

// parseElement is a helper function that parses the given element DSL
// arguments. Accepted syntax are:
//
Expand Down
50 changes: 49 additions & 1 deletion dsl/relationship.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ func Uses(element interface{}, description string, args ...interface{}) {
eval.IncompatibleDSL()
return
}
uses(src, element, description, args...)
if err := uses(src, element, description, args...); err != nil {
eval.ReportError("Uses: %s", err.Error())
}
}

// InteractsWith adds an interaction between a person and another.
Expand Down Expand Up @@ -238,6 +240,52 @@ func Delivers(person interface{}, description string, args ...interface{}) {

}

// Calls specifies a list of endpoint names that the relationship source calls.
// The target of the relationship must be a container.
//
// Calls must appear in Uses.
//
// Calls takes one or more arguments each of which is the name of an endpoint
// defined in the target container.
//
// Usage:
//
// Calls("endpoint")
//
// Calls("endpoint1", "endpoint2")
//
// Example:
//
// var _ = Design("my workspace", "a great architecture model", func() {
// SoftwareSystem("SystemA", func() {
// Container("ContainerA", func() {
// Uses("ContainerB", "Uses", func() {
// Calls("endpoint1", "endpoint2")
// })
// })
// })
// })
func Calls(endpoints ...string) {
v, ok := eval.Current().(*expr.Relationship)
if !ok {
eval.IncompatibleDSL()
return
}
for _, e := range endpoints {
if e == "" {
eval.ReportError("Calls: endpoint name cannot be empty")
return
}
for _, ep := range v.Endpoints {
if ep == e {
eval.ReportError("Calls: endpoint %q already defined", e)
return
}
}
v.Endpoints = append(v.Endpoints, e)
}
}

// Description provides a short description for a relationship displayed in a
// dynamic view.
//
Expand Down
17 changes: 17 additions & 0 deletions examples/json/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# JSON Model Example

This example `model` package contains a valid DSL definition of a simple
software system that makes use of the `Endpoint` and `Calls` DSLs. The
main function in the `main` package serializes the underlying model as
JSON and prints it to standard output.

## Usage

To run this example, execute the following from the examples/json
directory:

```
go run main.go
```

This will print the model as JSON to standard output.
18 changes: 18 additions & 0 deletions examples/json/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import (
"fmt"

"goa.design/model/codegen"
)

// Executes the DSL and serializes the resulting model to JSON.
func main() {
// Run the model DSL
js, err := codegen.JSON("goa.design/model/examples/json/model", true)
if err != nil {
panic(err)
}
// Print the JSON
fmt.Println(string(js))
}
47 changes: 47 additions & 0 deletions examples/json/model/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package design

import . "goa.design/model/dsl"

var _ = Design("Getting Started", "This is a model of my software system.", func() {
var System = SoftwareSystem("Software System", "My software system.", func() {
Container("Application Database", "Stores application data.", func() {
Tag("database")
})
Container("Web Application", "Delivers content to users.", func() {
Endpoint("Web", "Delivers HTML pages.")
Uses("Application Database", "Reads from and writes to", "MySQL", Synchronous)
})
Container("Load Balancer", "Distributes requests across the Web Application instances.", func() {
Uses("Web Application", "Routes requests to", "HTTPS", Synchronous, func() {
Calls("Web")
})
})
Tag("system")
})

Person("User", "A user of my software system.", func() {
Uses(System, "Uses", Synchronous)
Tag("person")
})

Views(func() {
SystemContextView(System, "SystemContext", "An example of a System Context diagram.", func() {
AddAll()
AutoLayout(RankLeftRight)
})
Styles(func() {
ElementStyle("system", func() {
Background("#1168bd")
Color("#ffffff")
})
ElementStyle("person", func() {
Background("#08427b")
Color("#ffffff")
Shape(ShapePerson)
})
ElementStyle("database", func() {
Shape(ShapeCylinder)
})
})
})
})
11 changes: 11 additions & 0 deletions expr/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@ type (
// Container represents a container.
Container struct {
*Element
Endpoints []*Endpoint
Components Components
System *SoftwareSystem
}

// Endpoint describes a container endpoint.
//
// Note: Endpoint information is not used directly in diagrams instead
// it is serialized in the system JSON representation for other tools to
// consume.
Endpoint struct {
Name string
Description string
}

// Containers is a slice of containers that can be easily
// converted into a slice of ElementHolder.
Containers []*Container
Expand Down
1 change: 1 addition & 0 deletions expr/relationship.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type (
InteractionStyle InteractionStyleKind
Tags string
URL string
Endpoints []string

// DestinationPath is used to compute the destination after all DSL has
// completed execution.
Expand Down
12 changes: 12 additions & 0 deletions mdl/elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ type (
Relationships []*Relationship `json:"relationships,omitempty"`
// Components list the components within the container.
Components []*Component `json:"components,omitempty"`
// Endpoints list the endpoints exposed by the container.
Endpoints []*Endpoint `json:"endpoints,omitempty"`
}

// Component represents a component.
Expand All @@ -98,6 +100,16 @@ type (
Relationships []*Relationship `json:"relationships,omitempty"`
}

// Endpoint describes a container endpoint.
//
// Note: Endpoint information is not used directly in diagrams instead
// it is serialized in the system JSON representation for other tools to
// consume.
Endpoint struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
}

// LocationKind is the enum for possible locations.
LocationKind int
)
Expand Down
13 changes: 13 additions & 0 deletions mdl/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func modelizeRelationships(rels []*expr.Relationship) []*Relationship {
Description: r.Description,
Tags: r.Tags,
URL: r.URL,
Endpoints: r.Endpoints,
SourceID: r.Source.ID,
DestinationID: r.Destination.ID,
Technology: r.Technology,
Expand Down Expand Up @@ -179,6 +180,7 @@ func modelizeContainers(cs []*expr.Container) []*Container {
Properties: c.Properties,
Relationships: modelizeRelationships(c.Relationships),
Components: modelizeComponents(c.Components),
Endpoints: modelizeEndpoints(c.Endpoints),
}
}
return res
Expand All @@ -201,6 +203,17 @@ func modelizeComponents(cs []*expr.Component) []*Component {
return res
}

func modelizeEndpoints(es []*expr.Endpoint) []*Endpoint {
res := make([]*Endpoint, len(es))
for i, e := range es {
res[i] = &Endpoint{
Name: e.Name,
Description: e.Description,
}
}
return res
}

func modelizeDeploymentNodes(dns []*expr.DeploymentNode) []*DeploymentNode {
res := make([]*DeploymentNode, len(dns))
for i, dn := range dns {
Expand Down
2 changes: 2 additions & 0 deletions mdl/relationship.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type (
Tags string `json:"tags,omitempty"`
// URL where more information can be found.
URL string `json:"url,omitempty"`
// Endpoints of relationship if any.
Endpoints []string `json:"endpoints,omitempty"`
// SourceID is the ID of the source element.
SourceID string `json:"sourceId"`
// DestinationID is ID the destination element.
Expand Down

0 comments on commit 1f704bd

Please sign in to comment.