Skip to content

Commit 3bcdace

Browse files
Create common/routing package for HTTP routes (temporalio#5539)
## What changed? <!-- Describe what has changed in this PR --> I added a `common/routing` package for defining HTTP routes which clients can use to construct URLs, and servers can use to parse requests, all in a type-safe manner. ## Why? <!-- Tell your future self why have you made these changes --> I was starting to write some functional tests for Nexus, and I realized that I didn't know what the routes were for it, and it felt flimsy to copy/paste what I found in other places. Instead, I felt like having a common set of "routes" similar to what you find in common web frameworks would be a good idea. It's a small package, and the only use case is Nexus, but I think it makes it a bit more ergonomic / safer. ## How did you test it? <!-- How have you verified this change? Tested locally? Added a unit test? Checked in staging env? --> I added unit tests for the new package. ## Potential risks <!-- Assuming the worst case, what can be broken when deploying this change to production? --> ## Documentation <!-- Have you made sure this change doesn't falsify anything currently stated in `docs/`? If significant new behavior is added, have you described that in `docs/`? --> ## Is hotfix candidate? <!-- Is this PR a hotfix candidate or does it require a notification to be sent to the broader community? (Yes/No) -->
1 parent e08d74c commit 3bcdace

9 files changed

Lines changed: 448 additions & 25 deletions

File tree

common/nexus/routes.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// The MIT License
2+
//
3+
// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved.
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in
13+
// all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
// THE SOFTWARE.
22+
23+
package nexus
24+
25+
import "go.temporal.io/server/common/routing"
26+
27+
// Routes returns a RouteSet for the Nexus HTTP API. These routes can be used by both the server and clients for
28+
// type-safe URL construction and parsing. It's a function instead of a variable so that the underlying
29+
// set cannot be modified by the caller.
30+
func Routes() RouteSet {
31+
return routes
32+
}
33+
34+
type RouteSet struct {
35+
DispatchNexusTaskByNamespaceAndTaskQueue routing.Route[NamespaceAndTaskQueue]
36+
DispatchNexusTaskByService routing.Route[string]
37+
}
38+
39+
type NamespaceAndTaskQueue struct {
40+
Namespace string
41+
TaskQueue string
42+
}
43+
44+
var routes = RouteSet{
45+
DispatchNexusTaskByNamespaceAndTaskQueue: routing.NewBuilder[NamespaceAndTaskQueue]().
46+
Constant("api", "v1", "namespaces").
47+
Variable("namespace", func(params *NamespaceAndTaskQueue) *string { return &params.Namespace }).
48+
Constant("task-queues").
49+
Variable("task_queue", func(params *NamespaceAndTaskQueue) *string { return &params.TaskQueue }).
50+
Constant("dispatch-nexus-task").
51+
Build(),
52+
DispatchNexusTaskByService: routing.NewBuilder[string]().
53+
Constant("api", "v1", "services").
54+
Variable("service", func(service *string) *string { return service }).
55+
Build(),
56+
}

common/nexus/routes_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// The MIT License
2+
//
3+
// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved.
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in
13+
// all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
// THE SOFTWARE.
22+
23+
package nexus_test
24+
25+
import (
26+
"fmt"
27+
28+
"go.temporal.io/server/common/nexus"
29+
)
30+
31+
func ExampleRouteSet_DispatchNexusTaskByNamespaceAndTaskQueue() {
32+
path := nexus.Routes().DispatchNexusTaskByNamespaceAndTaskQueue.
33+
Path(nexus.NamespaceAndTaskQueue{
34+
Namespace: "TEST-NAMESPACE",
35+
TaskQueue: "TEST-TASK-QUEUE",
36+
})
37+
fmt.Println(path)
38+
// Output: api/v1/namespaces/TEST-NAMESPACE/task-queues/TEST-TASK-QUEUE/dispatch-nexus-task
39+
}
40+
41+
func ExampleRouteSet_DispatchNexusTaskByService() {
42+
path := nexus.Routes().DispatchNexusTaskByService.
43+
Path("TEST-SERVICE")
44+
fmt.Println(path)
45+
// Output: api/v1/services/TEST-SERVICE
46+
}

common/persistence/cassandra/execution_store.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,27 @@ import (
4242
const (
4343
// Special Run IDs
4444
permanentRunID = "30000000-0000-f000-f000-000000000001"
45-
// Row Constants for Shard Row
45+
// Row Constant for Shard Row
4646
rowTypeShardNamespaceID = "10000000-1000-f000-f000-000000000000"
4747
rowTypeShardWorkflowID = "20000000-1000-f000-f000-000000000000"
4848
rowTypeShardRunID = "30000000-1000-f000-f000-000000000000"
49-
// Row Constants for Transfer Task Row
49+
// Row Constant for Transfer Task Row
5050
rowTypeTransferNamespaceID = "10000000-3000-f000-f000-000000000000"
5151
rowTypeTransferWorkflowID = "20000000-3000-f000-f000-000000000000"
5252
rowTypeTransferRunID = "30000000-3000-f000-f000-000000000000"
53-
// Row Constants for Timer Task Row
53+
// Row Constant for Timer Task Row
5454
rowTypeTimerNamespaceID = "10000000-4000-f000-f000-000000000000"
5555
rowTypeTimerWorkflowID = "20000000-4000-f000-f000-000000000000"
5656
rowTypeTimerRunID = "30000000-4000-f000-f000-000000000000"
57-
// Row Constants for Replication Task Row
57+
// Row Constant for Replication Task Row
5858
rowTypeReplicationNamespaceID = "10000000-5000-f000-f000-000000000000"
5959
rowTypeReplicationWorkflowID = "20000000-5000-f000-f000-000000000000"
6060
rowTypeReplicationRunID = "30000000-5000-f000-f000-000000000000"
6161
// Row constants for visibility task row.
6262
rowTypeVisibilityTaskNamespaceID = "10000000-6000-f000-f000-000000000000"
6363
rowTypeVisibilityTaskWorkflowID = "20000000-6000-f000-f000-000000000000"
6464
rowTypeVisibilityTaskRunID = "30000000-6000-f000-f000-000000000000"
65-
// Row Constants for Replication Task DLQ Row. Source cluster name will be used as WorkflowID.
65+
// Row Constant for Replication Task DLQ Row. Source cluster name will be used as WorkflowID.
6666
rowTypeDLQNamespaceID = "10000000-6000-f000-f000-000000000000"
6767
rowTypeDLQRunID = "30000000-6000-f000-f000-000000000000"
6868
// Row constants for History task row.

common/routing/route.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// The MIT License
2+
//
3+
// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved.
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in
13+
// all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
// THE SOFTWARE.
22+
23+
// Package routing provides utilities to define a number of [Route] instances, which can be...
24+
//
25+
// 1. Used by servers to...
26+
//
27+
// 1.a. Register with [github.com/gorilla/mux.Router] instances via the [Route.Representation] method.
28+
//
29+
// 1.b. Deserialize HTTP path variables into a struct with the [Route.Deserialize] method.
30+
//
31+
// 2. Used by clients to construct HTTP paths for requests by calling the [Route.Path] method.
32+
package routing
33+
34+
import (
35+
"strings"
36+
)
37+
38+
// Route represents a series of HTTP path components.
39+
type Route[T any] struct {
40+
components []Component[T]
41+
}
42+
43+
// Component represents a single HTTP path component, either a constant slug or a variable parameter.
44+
type Component[T any] interface {
45+
// Representation is the string representation of the component for usage in a path definition, e.g. "v1" for a
46+
// constant slug or "{namespace}" for a variable. This should be compatible with the format specified in
47+
// the [github.com/gorilla/mux] package.
48+
Representation() string
49+
// Serialize returns the actual value of the slug when used in an HTTP path, e.g. "v1" for a constant slug or
50+
// "test-namespace" for a variable.
51+
Serialize(params T) string
52+
// Deserialize mutates the given params object with the value of the component when parsing an HTTP path, e.g.
53+
// setting the value of a variable to "test-namespace". If the component is a constant slug, this method should
54+
// be a no-op.
55+
Deserialize(vars map[string]string, t *T)
56+
}
57+
58+
// NewRoute returns a new [Route] instance with the given components.
59+
func NewRoute[T any](components ...Component[T]) Route[T] {
60+
return Route[T]{components: components}
61+
}
62+
63+
// RouteBuilder is a builder for the [Route] interface.
64+
type RouteBuilder[T any] struct {
65+
components []Component[T]
66+
}
67+
68+
// NewBuilder creates a new [RouteBuilder] instance, which can be used to define a new [Route] via a fluent API.
69+
func NewBuilder[T any]() *RouteBuilder[T] {
70+
return &RouteBuilder[T]{}
71+
}
72+
73+
// With adds a series of [Component] instances to the [Route].
74+
func (r *RouteBuilder[T]) With(c ...Component[T]) *RouteBuilder[T] {
75+
r.components = append(r.components, c...)
76+
return r
77+
}
78+
79+
// Constant adds a [Constant] component to the [Route].
80+
func (r *RouteBuilder[T]) Constant(values ...string) *RouteBuilder[T] {
81+
return r.With(Constant[T](values...))
82+
}
83+
84+
// Variable adds a [Variable] component to the [Route].
85+
func (r *RouteBuilder[T]) Variable(name string, getter func(*T) *string) *RouteBuilder[T] {
86+
return r.With(Variable[T](name, getter))
87+
}
88+
89+
// Build returns a read-only [Route].
90+
func (r *RouteBuilder[T]) Build() Route[T] {
91+
return NewRoute[T](r.components...)
92+
}
93+
94+
// Representation returns the [github.com/gorilla/mux] compatible string representation of the route for usage in a
95+
// path definition. It does not add a leading or trailing slash to the representation, but it won't remove them if
96+
// they're present in the components. We do this because it's easier to add a slash depending on the context than to
97+
// remove it.
98+
func (r Route[T]) Representation() string {
99+
return r.serialize(func(c Component[T]) string {
100+
return c.Representation()
101+
})
102+
}
103+
104+
// Path returns the serialized path of the route with the given params. There will be no leading or trailing slashes,
105+
// similar to the behavior of the [Route.Representation] method.
106+
func (r Route[T]) Path(t T) string {
107+
return r.serialize(func(c Component[T]) string {
108+
return c.Serialize(t)
109+
})
110+
}
111+
112+
func (r Route[T]) serialize(f func(c Component[T]) string) string {
113+
var sb strings.Builder
114+
for i, c := range r.components {
115+
if i > 0 {
116+
sb.WriteString("/")
117+
}
118+
sb.WriteString(f(c))
119+
}
120+
return sb.String()
121+
}
122+
123+
// Deserialize the given vars into a new instance of the params type, T.
124+
func (r Route[T]) Deserialize(vars map[string]string) *T {
125+
var t T
126+
for _, c := range r.components {
127+
c.Deserialize(vars, &t)
128+
}
129+
return &t
130+
}
131+
132+
// Constant returns a [Component] that represents a series of constant HTTP path components in a Route.
133+
// They will be joined via strings when used to construct a path or path representation.
134+
func Constant[T any](values ...string) constants[T] {
135+
return values
136+
}
137+
138+
type constants[T any] []string
139+
140+
func (s constants[T]) Representation() string {
141+
return strings.Join(s, "/")
142+
}
143+
144+
func (s constants[T]) Serialize(T) string {
145+
return strings.Join(s, "/")
146+
}
147+
148+
func (s constants[T]) Deserialize(map[string]string, *T) {}
149+
150+
// Variable returns a [Component] that represents a string variable in a Route.
151+
func Variable[T any](name string, getter func(*T) *string) stringVariable[T] {
152+
return stringVariable[T]{name, getter}
153+
}
154+
155+
type stringVariable[T any] struct {
156+
name string
157+
getter func(*T) *string
158+
}
159+
160+
func (s stringVariable[T]) Representation() string {
161+
return "{" + s.name + "}"
162+
}
163+
164+
func (s stringVariable[T]) Serialize(t T) string {
165+
return *s.getter(&t)
166+
}
167+
168+
func (s stringVariable[T]) Deserialize(vars map[string]string, t *T) {
169+
*s.getter(t) = vars[s.name]
170+
}

0 commit comments

Comments
 (0)