blob: 472ad769a9551e141de840fb7ef107c2d4b3d51c [file] [log] [blame]
// Copyright 2019 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 lucicfg
import (
"fmt"
"strings"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)
// ruleImpl is a callable representing some concrete rule.
//
// It is a result of `lucicfg.rule(impl = cb)` call. It wraps `cb`, adding
// an additional positional argument to it (in front of other arguments).
//
// TODO(vadimsh): Add attributes too, such as 'defaults' and 'key'.
type ruleImpl struct {
starlark.Callable
defaults *starlarkstruct.Struct // struct with vars passed as 'defaults'
}
// newRuleImpl construct a new rule if arguments pass the validation.
func newRuleImpl(impl starlark.Callable, defaults *starlark.Dict) (*ruleImpl, error) {
pairs := defaults.Items()
for _, pair := range pairs {
k, v := pair[0], pair[1]
if _, ok := k.(starlark.String); !ok {
return nil, fmt.Errorf("lucicfg.rule: keys in \"defaults\" must be strings")
}
if !isNamedStruct(v, "lucicfg.var") {
return nil, fmt.Errorf("lucicfg.rule: values in \"defaults\" must be lucicfg.var")
}
}
return &ruleImpl{
Callable: impl,
defaults: starlarkstruct.FromKeywords(starlark.String("lucicfg.rule.defaults"), pairs),
}, nil
}
// String lets caller know this is a rule now.
func (r *ruleImpl) String() string {
name := r.Callable.Name()
return fmt.Sprintf("<rule %s>", strings.TrimPrefix(name, "_"))
}
// CallInternal prepends `ctx` argument and validates the return value.
//
// Note that we intentionally do not start a new stack frame to declutter stack
// traces: lucicfg.rule(...) wrapping is "transparent" from Starlark's point of
// view.
func (r *ruleImpl) CallInternal(th *starlark.Thread, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
newArgs := make(starlark.Tuple, 0, len(args)+1)
newArgs = append(newArgs, starlarkstruct.FromStringDict(
starlark.String("lucicfg.rule_ctx"),
starlark.StringDict{
"defaults": r.defaults,
},
))
newArgs = append(newArgs, args...)
switch res, err := r.Callable.CallInternal(th, newArgs, kwargs); {
case err != nil:
return nil, err
case !isNamedStruct(res, "graph.keyset"):
return nil, fmt.Errorf("bad rule implementation %s: must return graph.keyset, got %s", r.Callable, res.Type())
default:
return res, nil
}
}
// AttrNames is part of starlark.HasAttrs interface.
func (r *ruleImpl) AttrNames() []string {
return []string{"defaults"}
}
// Attr is part of starlark.HasAttrs interface.
func (r *ruleImpl) Attr(name string) (starlark.Value, error) {
switch name {
case "defaults":
return r.defaults, nil
default:
return nil, nil
}
}
func isNamedStruct(v starlark.Value, name string) bool {
if st, ok := v.(*starlarkstruct.Struct); ok {
return st.Constructor().String() == name
}
return false
}
func init() {
// See //internal/lucicfg.star.
declNative("declare_rule", func(call nativeCall) (starlark.Value, error) {
var impl starlark.Callable
var defaults *starlark.Dict
if err := call.unpack(1, &impl, &defaults); err != nil {
return nil, err
}
return newRuleImpl(impl, defaults)
})
}