blob: 1e4f0dba35182b2b85e6635b3430778f2d6effbc [file] [log] [blame]
// Copyright 2016 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 templateproto
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"reflect"
"regexp"
"strings"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/common/errors"
)
// Normalize will normalize all of the Templates in this message, returning an
// error if any are invalid.
func (f *File) Normalize() error {
me := errors.MultiError(nil)
for tname, t := range f.Template {
if err := t.Normalize(); err != nil {
me = append(me, fmt.Errorf("for template %q: %s", tname, err))
}
}
if len(me) > 0 {
return me
}
return nil
}
// ParamRegex is the regular expression that all parameter names must match.
var ParamRegex = regexp.MustCompile(`^\${[^}]+}$`)
// Normalize will normalize the Template message, returning an error if it is
// invalid.
func (t *File_Template) Normalize() error {
if t.Body == "" {
return errors.New("body is empty")
}
defaultParams := make(map[string]*Value, len(t.Param))
for k, param := range t.Param {
if k == "" {
return fmt.Errorf("param %q: invalid name", k)
}
if !ParamRegex.MatchString(k) {
return fmt.Errorf("param %q: malformed name", k)
}
if !strings.Contains(t.Body, k) {
return fmt.Errorf("param %q: not present in body", k)
}
if err := param.Normalize(); err != nil {
return fmt.Errorf("param %q: %s", k, err)
}
if param.Default != nil {
defaultParams[k] = param.Default
} else {
defaultParams[k] = param.Schema.Zero()
}
}
maybeJSON, err := t.Render(defaultParams)
if err != nil {
return fmt.Errorf("rendering: %s", err)
}
err = json.Unmarshal([]byte(maybeJSON), &map[string]interface{}{})
if err != nil {
return fmt.Errorf("parsing rendered body: %s", err)
}
return nil
}
// Normalize will normalize the Parameter, returning an error if it is invalid.
func (p *File_Template_Parameter) Normalize() error {
if p == nil {
return errors.New("is nil")
}
if err := p.Schema.Normalize(); err != nil {
return fmt.Errorf("schema: %s", err)
}
if p.Default != nil {
if err := p.Default.Normalize(); err != nil {
return fmt.Errorf("default value: %s", err)
}
if err := p.Accepts(p.Default); err != nil {
return fmt.Errorf("default value: %s", err)
}
}
return nil
}
// Accepts returns nil if this Parameter can accept the Value.
func (p *File_Template_Parameter) Accepts(v *Value) error {
if v.IsNull() {
if !p.Nullable {
return errors.New("not nullable")
}
} else if err := p.Schema.Accepts(v); err != nil {
return err
} else if err := v.Check(p.Schema); err != nil {
return err
}
return nil
}
// Normalize will normalize the Schema, returning an error if it is invalid.
func (s *Schema) Normalize() error {
if s == nil {
return errors.New("is nil")
}
if s.Schema == nil {
return errors.New("has no type")
}
if enum := s.GetEnum(); enum != nil {
return enum.Normalize()
}
return nil
}
// Normalize will normalize the Schema_Set, returning an error if it is
// invalid.
func (s *Schema_Set) Normalize() error {
if len(s.Entry) == 0 {
return errors.New("set requires entries")
}
set := stringset.New(len(s.Entry))
for _, entry := range s.Entry {
if entry.Token == "" {
return errors.New("blank token")
}
if !set.Add(entry.Token) {
return fmt.Errorf("duplicate token %q", entry.Token)
}
}
return nil
}
// Has returns true iff the given token is a valid value for this enumeration.
func (s *Schema_Set) Has(token string) bool {
for _, tok := range s.Entry {
if tok.Token == token {
return true
}
}
return false
}
// IsNull returns true if this Value is the null value.
func (v *Value) IsNull() bool {
_, ret := v.Value.(*Value_Null)
return ret
}
// Check ensures that this value conforms to the given schema.
func (v *Value) Check(s *Schema) error {
check, needsCheck := v.Value.(interface {
Check(*Schema) error
})
if !needsCheck {
return nil
}
return check.Check(s)
}
// Normalize returns a non-nil error if the Value is invalid for its nominal type.
func (v *Value) Normalize() error {
norm, needsNormalization := v.Value.(interface {
Normalize() error
})
if !needsNormalization {
return nil
}
return norm.Normalize()
}
// Check returns nil iff this Value meets the max length criteria.
func (v *Value_Bytes) Check(schema *Schema) error {
s := schema.GetBytes()
if s.MaxLength > 0 && uint32(len(v.Bytes)) > s.MaxLength {
return fmt.Errorf("value is too large: %d > %d", len(v.Bytes), s.MaxLength)
}
return nil
}
// Check returns nil iff this Value meets the max length criteria, and/or
// can be used to fill an enumeration value from the provided schema.
func (v *Value_Str) Check(schema *Schema) error {
switch s := schema.Schema.(type) {
case *Schema_Str:
maxLen := s.Str.MaxLength
if maxLen > 0 && uint32(len(v.Str)) > maxLen {
return fmt.Errorf("value is too large: %d > %d", len(v.Str), maxLen)
}
case *Schema_Enum:
if !s.Enum.Has(v.Str) {
return fmt.Errorf("value does not match enum: %q", v.Str)
}
default:
panic(fmt.Errorf("for Value_Str: unknown schema %T", s))
}
return nil
}
// Check returns nil iff this Value correctly parses as a JSON object.
func (v *Value_Object) Check(schema *Schema) error {
s := schema.GetObject()
if s.MaxLength > 0 && uint32(len(v.Object)) > s.MaxLength {
return fmt.Errorf("value is too large: %d > %d", len(v.Object), s.MaxLength)
}
return nil
}
// Normalize returns nil iff this Value correctly parses as a JSON object.
func (v *Value_Object) Normalize() error {
newObj, err := NormalizeJSON(v.Object, true)
if err != nil {
return err
}
v.Object = newObj
return nil
}
// Check returns nil iff this Value correctly parses as a JSON array.
func (v *Value_Array) Check(schema *Schema) error {
s := schema.GetArray()
if s.MaxLength > 0 && uint32(len(v.Array)) > s.MaxLength {
return fmt.Errorf("value is too large: %d > %d", len(v.Array), s.MaxLength)
}
return nil
}
// Normalize returns nil iff this Value correctly parses as a JSON array.
func (v *Value_Array) Normalize() error {
newAry, err := NormalizeJSON(v.Array, false)
if err != nil {
return err
}
v.Array = newAry
return nil
}
var (
typeOfSchemaStr = reflect.TypeOf((*Schema_Str)(nil))
typeOfSchemaEnum = reflect.TypeOf((*Schema_Enum)(nil))
)
// Accepts returns nil if this Schema can accept the Value.
func (s *Schema) Accepts(v *Value) error {
typ := reflect.TypeOf(s.Schema)
if typ == typeOfSchemaEnum {
typ = typeOfSchemaStr
}
if typ != reflect.TypeOf(v.schemaType()) {
return fmt.Errorf("type is %q, expected %q", schemaTypeStr(v.schemaType()), schemaTypeStr(s.Schema))
}
return nil
}
// Zero produces a Value from this schema which is a valid 'zero' value (in the
// go sense)
func (s *Schema) Zero() *Value {
switch sub := s.Schema.(type) {
case *Schema_Int:
return MustNewValue(0)
case *Schema_Uint:
return MustNewValue(uint(0))
case *Schema_Float:
return MustNewValue(0.0)
case *Schema_Bool:
return MustNewValue(false)
case *Schema_Str:
return MustNewValue("")
case *Schema_Bytes:
return MustNewValue([]byte{})
case *Schema_Enum:
return MustNewValue(sub.Enum.Entry[0].Token)
case *Schema_Object:
return &Value{Value: &Value_Object{"{}"}}
case *Schema_Array:
return &Value{Value: &Value_Array{"[]"}}
}
panic(fmt.Errorf("unknown schema type: %v", s))
}
// NormalizeJSON is used to take some free-form JSON and validates that:
// * it only contains a valid JSON object (e.g. `{...stuff...}`); OR
// * it only contains a valid JSON array (e.g. `[...stuff...]`)
//
// If obj is true, this looks for an object, if it's false, it looks for an
// array.
//
// This will also remove all extra whitespace and sort all objects by key.
func NormalizeJSON(data string, obj bool) (string, error) {
buf := bytes.NewBufferString(data)
dec := json.NewDecoder(buf)
dec.UseNumber()
var decoded interface{}
if obj {
decoded = &map[string]interface{}{}
} else {
decoded = &[]interface{}{}
}
err := dec.Decode(decoded)
if err != nil {
return "", err
}
bufdat, err := ioutil.ReadAll(dec.Buffered())
if err != nil {
panic(err)
}
rest := strings.TrimSpace(string(bufdat) + buf.String())
if rest != "" {
return "", fmt.Errorf("got extra junk: %q", rest)
}
buf.Reset()
err = json.NewEncoder(buf).Encode(decoded)
// the TrimSpace chops off an extraneous newline that the json lib adds on.
return string(bytes.TrimSpace(buf.Bytes())), err
}
// Normalize will normalize this Specifier
func (s *Specifier) Normalize() error {
if s.TemplateName == "" {
return errors.New("empty template_name")
}
for k, v := range s.Params {
if k == "" {
return errors.New("empty param key")
}
if err := v.Normalize(); err != nil {
return fmt.Errorf("param %q: %s", k, err)
}
}
return nil
}