generated from dogmatiq/template-go
-
Notifications
You must be signed in to change notification settings - Fork 2
/
builder_networkport.go
148 lines (123 loc) · 4.36 KB
/
builder_networkport.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package ferrite
import (
"errors"
"strconv"
"github.com/dogmatiq/ferrite/internal/variable"
)
// NetworkPort configures an environment variable as a network port.
//
// name is the name of the environment variable to read. desc is a
// human-readable description of the environment variable.
func NetworkPort(name, desc string) *NetworkPortBuilder {
b := &NetworkPortBuilder{}
b.builder.Name(name)
b.builder.Description(desc)
b.builder.BuiltInConstraint(
"**MUST** be a valid network port",
func(_ variable.ConstraintContext, v string) variable.ConstraintError {
return validatePort(v)
},
)
b.builder.NonNormativeExample("8000", "a port commonly used for private web servers")
b.builder.NonNormativeExample("https", "the IANA service name that maps to port 443")
buildNetworkPortSyntaxDocumentation(b.builder.Documentation())
return b
}
// NetworkPortBuilder builds a specification for a network port variable.
type NetworkPortBuilder struct {
schema variable.TypedString[string]
builder variable.TypedSpecBuilder[string]
}
var _ isBuilderOf[string, *NetworkPortBuilder]
// WithDefault sets the default value of the variable.
//
// It is used when the environment variable is undefined or empty.
func (b *NetworkPortBuilder) WithDefault(v string) *NetworkPortBuilder {
b.builder.Default(v)
return b
}
// Required completes the build process and registers a required variable with
// Ferrite's validation system.
func (b *NetworkPortBuilder) Required(options ...RequiredOption) Required[string] {
return required(b.schema, &b.builder, options...)
}
// Optional completes the build process and registers an optional variable with
// Ferrite's validation system.
func (b *NetworkPortBuilder) Optional(options ...OptionalOption) Optional[string] {
return optional(b.schema, &b.builder, options...)
}
// Deprecated completes the build process and registers a deprecated variable
// with Ferrite's validation system.
func (b *NetworkPortBuilder) Deprecated(options ...DeprecatedOption) Deprecated[string] {
return deprecated(b.schema, &b.builder, options...)
}
// validateHost returns an error of port is not a valid numeric port or IANA
// service name.
func validatePort(port string) error {
if port == "" {
return errors.New("port must not be empty")
}
n, err := strconv.ParseUint(port, 10, 16)
if errors.Is(err, strconv.ErrSyntax) {
return validateIANAServiceName(port)
}
if err != nil || n == 0 {
return errors.New("numeric ports must be between 1 and 65535")
}
return nil
}
// validateIANAServiceName returns an error if name is not a valid IANA service
// name.
//
// See https://www.rfc-editor.org/rfc/rfc6335.html#section-5.1.
func validateIANAServiceName(name string) error {
n := len(name)
// RFC-6335: MUST be at least 1 character and no more than 15 characters
// long.
if n == 0 || n > 15 {
return errors.New("IANA service name must be between 1 and 15 characters")
}
// RFC-6335: MUST NOT begin or end with a hyphen.
if name[0] == '-' || name[n-1] == '-' {
return errors.New("IANA service name must not begin or end with a hyphen")
}
hasLetter := false
for i := range name {
ch := name[i] // iterate by byte (not rune)
// RFC-6335: MUST contain only US-ASCII letters 'A' - 'Z' and 'a' - 'z',
// digits '0' - '9', and hyphens ('-', ASCII 0x2D or decimal 45).
switch {
case ch >= 'A' && ch <= 'Z':
hasLetter = true
case ch >= 'a' && ch <= 'z':
hasLetter = true
case ch >= '0' && ch <= '9':
// digit ok!
case ch == '-':
// RFC-6335: hyphens MUST NOT be adjacent to other hyphens.
if name[i-1] == '-' {
return errors.New("IANA service name must not contain adjacent hyphens")
}
default:
return errors.New("IANA service name must contain only ASCII letters, digits and hyphen")
}
}
// RFC-6335: MUST contain at least one letter ('A' - 'Z' or 'a' - 'z').
if !hasLetter {
return errors.New("IANA service name must contain at least one letter")
}
return nil
}
func buildNetworkPortSyntaxDocumentation(d variable.DocumentationBuilder) {
d.
Summary("Network port syntax").
Paragraph(
"Ports may be specified as a numeric value no greater than `65535`.",
"Alternatively, a service name can be used.",
"Service names are resolved against the system's service database,",
"typically located in the `/etc/service` file on UNIX-like systems.",
"Standard service names are published by IANA.",
).
Format().
Done()
}