blob: 67b7730a8774213ad442cf5824f1eb4ee259ffc5 [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"unicode/utf8"
)
const (
// StreamNameSep is the separator rune for stream name tokens.
StreamNameSep = '/'
// StreamNameSepStr is a string containing a single rune, StreamNameSep.
StreamNameSepStr = string(StreamNameSep)
// StreamPathSep is the separator rune between a stream prefix and its
// name.
StreamPathSep = '+'
// StreamPathSepStr is a string containing a single rune, StreamPathSep.
StreamPathSepStr = string(StreamPathSep)
// MaxStreamNameLength is the maximum size, in bytes, of a StreamName. Since
// stream names must be valid ASCII, this is also the maximum string length.
MaxStreamNameLength = 4096
)
// StreamName is a structured stream name.
//
// A valid stream name is composed of segments internally separated by a
// StreamNameSep (/).
//
// Each segment:
// - Consists of the following character types:
// - Alphanumeric characters [a-zA-Z0-9]
// - Colon (:)
// - Underscore (_)
// - Hyphen (-)
// - Period (.)
// - Must begin with an alphanumeric character.
type StreamName string
func isAlnum(r rune) bool {
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')
}
// Construct builds a path string from a series of individual path components.
// Any leading and trailing separators will be stripped from the components.
//
// The result value will be a valid StreamName if all of the parts are
// valid StreamName strings. Likewise, it may be a valid StreamPath if
// StreamPathSep is one of the parts.
func Construct(parts ...string) string {
pidx := 0
for _, v := range parts {
if v == "" {
continue
}
parts[pidx] = strings.Trim(v, StreamNameSepStr)
pidx++
}
return strings.Join(parts[:pidx], StreamNameSepStr)
}
// MakeStreamName constructs a new stream name from its segments.
//
// This method is guaranteed to return a valid stream name. In order to ensure
// that the arbitrary input can meet this standard, the following
// transformations will be applied as needed:
// - If the segment doesn't begin with an alphanumeric character, the fill
// string will be prepended.
// - Any character disallowed in the segment will be replaced with an
// underscore. This includes segment separators within a segment string.
//
// An empty string can be passed for "fill" to just error if a replacement is
// needed.
func MakeStreamName(fill string, s ...string) (StreamName, error) {
if len(s) == 0 {
return "", errors.New("at least one segment must be provided")
}
fillValidated := false
for idx, v := range s {
v = strings.Map(func(r rune) rune {
switch {
case r >= 'A' && r <= 'Z':
fallthrough
case r >= 'a' && r <= 'z':
fallthrough
case r >= '0' && r <= '9':
fallthrough
case r == '.':
fallthrough
case r == '_':
fallthrough
case r == '-':
fallthrough
case r == ':':
return r
default:
return '_'
}
}, v)
if r, _ := utf8.DecodeRuneInString(v); !isAlnum(r) {
// We will prepend the fill sequence to make this a valid stream name.
// We only need to validate this sequence once.
if !fillValidated {
if err := StreamName(fill).Validate(); err != nil {
return "", fmt.Errorf("fill string must be a valid stream name: %s", err)
}
fillValidated = true
}
v = fill + v
}
s[idx] = v
}
result := StreamName(Construct(s...))
if err := result.Validate(); err != nil {
return "", err
}
return result, nil
}
// String implements flag.String.
func (s *StreamName) String() string {
return string(*s)
}
// Set implements flag.Value.
func (s *StreamName) Set(value string) error {
v := StreamName(value)
if err := v.Validate(); err != nil {
return err
}
*s = v
return nil
}
// Trim trims separator characters from the beginning and end of a StreamName.
//
// While such a StreamName is not Valid, this method helps correct small user
// input errors.
func (s StreamName) Trim() StreamName {
return StreamName(trimString(string(s)))
}
// Join concatenates a stream name onto the end of the current name, separating
// it with a separator character.
func (s StreamName) Join(o StreamName) StreamPath {
return StreamPath(fmt.Sprintf("%s%c%c%c%s",
s.Trim(), StreamNameSep, StreamPathSep, StreamNameSep, o.Trim()))
}
// Concat constructs a StreamName by concatenating several StreamName components
// together.
func (s StreamName) Concat(o ...StreamName) StreamName {
parts := make([]string, len(o)+1)
parts[0] = string(s)
for i, c := range o {
parts[i+1] = string(c)
}
return StreamName(Construct(parts...))
}
// AsPathPrefix uses s as the prefix component of a StreamPath and constructs
// the remainder of the path with the supplied name.
//
// If name is empty, the resulting path will end in the path separator. For
// example, if s is "foo/bar" and name is "", the path will be "foo/bar/+".
//
// If both s and name are valid StreamNames, this will construct a valid
// StreamPath. If s is a valid StreamName and name is empty, this will construct
// a valid partial StreamPath.
func (s StreamName) AsPathPrefix(name StreamName) StreamPath {
return StreamPath(Construct(string(s), StreamPathSepStr, string(name)))
}
// Validate tests whether the stream name is valid.
func (s StreamName) Validate() error {
if len(s) == 0 {
return errors.New("must contain at least one character")
}
if len(s) > MaxStreamNameLength {
return fmt.Errorf("stream name is too long (%d > %d)", len(s), MaxStreamNameLength)
}
var lastRune rune
var segmentIdx int
for idx, r := range s {
// Alphanumeric.
if !isAlnum(r) {
// The stream name must begin with an alphanumeric character.
if idx == segmentIdx {
return fmt.Errorf("segment (at %d) must begin with alphanumeric character", segmentIdx)
}
// Test forward slash, and ensure no adjacent forward slashes.
if r == StreamNameSep {
segmentIdx = idx + utf8.RuneLen(r)
} else if !(r == '.' || r == '_' || r == '-' || r == ':') {
// Test remaining allowed characters.
return fmt.Errorf("illegal character (%c) at index %d", r, idx)
}
}
lastRune = r
}
// The last rune may not be a separator.
if lastRune == StreamNameSep {
return errors.New("name may not end with a separator")
}
return nil
}
// Segments returns the individual StreamName segments by splitting splitting
// the StreamName with StreamNameSep.
func (s StreamName) Segments() []string {
if len(s) == 0 {
return nil
}
return strings.Split(string(s), string(StreamNameSep))
}
// SegmentCount returns the total number of segments in the StreamName.
func (s StreamName) SegmentCount() int {
return segmentCount(string(s))
}
// Split splits a StreamName on its last path element, into a prefix (everything
// before that element) and the last element.
//
// Split assumes that s is a valid stream name.
//
// If there is only one element in the stream name, prefix will be empty.
//
// For example:
//
// - Split("foo/bar/baz") ("foo/bar", "baz")
// - Split("foo") ("", "foo")
// - Split("") ("", "")
func (s StreamName) Split() (prefix, last StreamName) {
lidx := strings.LastIndex(string(s), StreamNameSepStr)
if lidx < 0 {
last = s
return
}
prefix, last = s[:lidx], s[lidx+len(StreamNameSepStr):]
return
}
// UnmarshalJSON implements json.Unmarshaler.
func (s *StreamName) UnmarshalJSON(data []byte) error {
v := ""
if err := json.Unmarshal(data, &v); err != nil {
return err
}
if err := StreamName(v).Validate(); err != nil {
return err
}
*s = StreamName(v)
return nil
}
// MarshalJSON implements json.Marshaler.
func (s StreamName) MarshalJSON() ([]byte, error) {
v := string(s)
return json.Marshal(&v)
}
// Namespaces returns a slice of all the namespaces this StreamName is within.
//
// For example:
// "a/b/c".Namespaces() ->
// "a/b/"
// "a/"
// ""
func (s StreamName) Namespaces() (ret []StreamName) {
var namespace StreamName
for {
lidx := strings.LastIndex(string(s), StreamNameSepStr)
if lidx < 0 {
ret = append(ret, "")
return
}
namespace = s[:lidx+len(StreamNameSepStr)]
ret = append(ret, namespace)
s = namespace[:lidx]
}
}
// AsNamespace returns this streamname in namespace form.
//
// As such, the returned value may always be directly concatenated with another
// StreamName (i.e. with `+`) to form a valid stream name (assuming that both
// StreamNames are, in fact, valid in the first place).
//
// For example:
//
// "".AsNamespace() -> ""
// "foo".AsNamespace() -> "foo/"
// "bar/".AsNamespace() -> "bar/"
func (s StreamName) AsNamespace() StreamName {
if len(s) == 0 || strings.HasSuffix(string(s), StreamNameSepStr) {
return s
}
return s + StreamName(StreamNameSep)
}
// A StreamPath consists of two StreamName, joined via a StreamPathSep (+)
// separator.
type StreamPath string
// Split splits a StreamPath into its prefix and name components.
//
// If there is no divider present (e.g., foo/bar/baz), the result will parse
// as the stream prefix with an empty name component.
func (p StreamPath) Split() (prefix StreamName, name StreamName) {
prefix, _, name = p.SplitParts()
return
}
// SplitParts splits a StreamPath into its prefix and name components.
//
// If there is no separator present (e.g., foo/bar/baz), the result will parse
// as the stream prefix with an empty name component. If there is a separator
// present but no name component, separator will be returned as true with an
// empty name.
func (p StreamPath) SplitParts() (prefix StreamName, sep bool, name StreamName) {
inside := false
hasPrefix := false
segIdx := 0
for i, r := range p {
// We have found our separator, and just started iterating the name
// component.
if hasPrefix {
name = StreamName(p[i:])
return
}
switch r {
case StreamPathSep:
// If we're at the beginning of a component, this is a potentially-valid
// separator. We'll identify it as valid if the next character is "/" and
// break the loop on the character after that.
sep = !inside
inside = true
case StreamNameSep:
if sep {
// A component beginning with "+" has been marked, and we have hit
// another "/", so this is a valid separator. Carve our prefix.
//
// We use a separate "hasPrefix" boolean in case the path begin with
// the separator (e.g., "+/foo").
prefix = StreamName(p[:segIdx])
hasPrefix = true
}
inside = false
segIdx = i
default:
// Non-special character, mark that we're no longer at the beginning of
// a component and that this is no longer a valid separator.
sep = false
inside = true
}
}
// We finished iterating through our string and didn't find a separator.
// Either we ended on the separator (prefix is assigned), or we didn't
// encounter one. In the latter case, the entire path is used as the prefix.
if !hasPrefix {
prefix = StreamName(p)
if sep {
// If the string ends in a separator, discard and split (e.g., "foo/+").
prefix = prefix[:segIdx]
}
}
return
}
// Validate checks whether a StreamPath is valid. A valid stream path must have
// a valid prefix and name components.
func (p StreamPath) Validate() error {
return p.validateImpl(true)
}
// ValidatePartial checks whether a partial StreamPath is valid. A partial
// stream path if appending additional components to it can form a fully-valid
// StreamPath.
func (p StreamPath) ValidatePartial() error {
return p.validateImpl(false)
}
func (p StreamPath) validateImpl(complete bool) error {
prefix, name := p.Split()
if complete || prefix != "" {
if err := prefix.Validate(); err != nil {
return err
}
}
if complete || name != "" {
if err := name.Validate(); err != nil {
return err
}
}
return nil
}
// Trim trims separator characters from the beginning and end of a StreamPath.
//
// While such a StreamPath is not Valid, this method helps correct small user
// input errors.
func (p StreamPath) Trim() StreamPath {
return StreamPath(trimString(string(p)))
}
// Append returns a StreamPath consisting of the current StreamPath with the
// supplied StreamName appended to the end.
//
// Append will return a valid StreamPath if p and n are both valid.
func (p StreamPath) Append(n string) StreamPath {
return StreamPath(Construct(string(p), n))
}
// SplitLast splits the rightmost component from a StreamPath, returning the
// intermediate StreamPath.
//
// If the path begins with a leading separator, it will be included in the
// leftmost token. Note that such a path is invalid.
//
// If there are no components in the path, ("", p) will be returned.
func (p StreamPath) SplitLast() (StreamPath, string) {
if idx := strings.LastIndex(string(p), StreamNameSepStr); idx > 0 {
return p[:idx], string(p[idx+len(StreamNameSepStr):])
}
return "", string(p)
}
// SegmentCount returns the total number of segments in the StreamName.
func (p StreamPath) SegmentCount() int {
return segmentCount(string(p))
}
// UnmarshalJSON implements json.Unmarshaler.
func (p *StreamPath) UnmarshalJSON(data []byte) error {
v := ""
if err := json.Unmarshal(data, &v); err != nil {
return err
}
if err := StreamPath(v).Validate(); err != nil {
return err
}
*p = StreamPath(v)
return nil
}
// MarshalJSON implements json.Marshaler.
func (p StreamPath) MarshalJSON() ([]byte, error) {
v := string(p)
return json.Marshal(&v)
}
// StreamNameSlice is a slice of StreamName entries. It implements
// sort.Interface.
type StreamNameSlice []StreamName
var _ sort.Interface = StreamNameSlice(nil)
func (s StreamNameSlice) Len() int { return len(s) }
func (s StreamNameSlice) Less(i, j int) bool { return s[i] < s[j] }
func (s StreamNameSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func trimString(s string) string {
for {
r, l := utf8.DecodeRuneInString(s)
if r != StreamNameSep {
break
}
s = s[l:]
}
for {
r, l := utf8.DecodeLastRuneInString(s)
if r != StreamNameSep {
break
}
s = s[:len(s)-l]
}
return s
}
func segmentCount(s string) int {
if len(s) == 0 {
return 0
}
return strings.Count(s, string(StreamNameSep)) + 1
}