github.com/emcfarlane/larking@v0.0.0-20220605172417-1704b45ee6c3/starlib/net/starlarkopenapi/openapi.go (about)

     1  // Copyright 2022 Edward McFarlane. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package starlarkopenapi
     6  
     7  // OpenAPI spec:
     8  // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#dataTypeType
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"encoding/base64"
    14  	"encoding/json"
    15  	"fmt"
    16  	"hash/crc32"
    17  	"io"
    18  	"io/ioutil"
    19  	"mime/multipart"
    20  	"net/http"
    21  	"net/url"
    22  	"path"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  	"unicode"
    28  
    29  	"github.com/emcfarlane/larking/starlib/net/starlarkhttp"
    30  	"github.com/emcfarlane/larking/starlib/starext"
    31  	"github.com/emcfarlane/larking/starlib/starlarkstruct"
    32  	"github.com/emcfarlane/larking/starlib/starlarkthread"
    33  	"github.com/go-openapi/spec"
    34  	"github.com/iancoleman/strcase"
    35  	starlarkjson "go.starlark.net/lib/json"
    36  	starlarktime "go.starlark.net/lib/time"
    37  	"go.starlark.net/starlark"
    38  	"gocloud.dev/runtimevar"
    39  )
    40  
    41  func NewModule() *starlarkstruct.Module {
    42  	return &starlarkstruct.Module{
    43  		Name: "openapi",
    44  		Members: starlark.StringDict{
    45  			"open": starext.MakeBuiltin("openapi.open", Open),
    46  		},
    47  	}
    48  }
    49  
    50  type Client struct {
    51  	// service encoding...
    52  	name     string
    53  	variable *runtimevar.Variable
    54  	client   *starlarkhttp.Client
    55  
    56  	val  []byte // snapshot.Value
    57  	doc  *spec.Swagger
    58  	svcs map[string]*Service //starlark.Value
    59  }
    60  
    61  var defaultClient = starlarkhttp.NewClient(http.DefaultClient)
    62  
    63  func Open(thread *starlark.Thread, fnname string, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
    64  	var (
    65  		addr   string
    66  		name   string
    67  		client = defaultClient
    68  	)
    69  	if err := starlark.UnpackArgs(fnname, args, kwargs, "name", &name, "addr?", &addr, "client?", &client); err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	ctx := starlarkthread.GetContext(thread)
    74  
    75  	variable, err := runtimevar.OpenVariable(ctx, name)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	c := &Client{
    81  		name:     name,
    82  		variable: variable,
    83  		client:   client,
    84  	}
    85  	if _, err := c.load(ctx); err != nil {
    86  		variable.Close() //nolint
    87  		return nil, err
    88  	}
    89  	if err := starlarkthread.AddResource(thread, c); err != nil {
    90  		variable.Close() //nolint
    91  		return nil, err
    92  	}
    93  	return c, nil
    94  }
    95  
    96  func toSnakeCase(s string) string {
    97  	s = strings.Map(func(r rune) rune {
    98  		if unicode.IsLetter(r) || unicode.IsNumber(r) {
    99  			return r
   100  		}
   101  		// ignore variables
   102  		if r == '{' || r == '}' {
   103  			return -1
   104  		}
   105  		return '_'
   106  	}, s)
   107  	s = strcase.ToSnake(s)
   108  	s = strings.Trim(s, "_")
   109  	return s
   110  }
   111  
   112  func (c *Client) do(
   113  	thread *starlark.Thread,
   114  	fnname string,
   115  	req *starlarkhttp.Request,
   116  ) (*starlarkhttp.Response, error) {
   117  	return c.client.Do(thread, fnname, req)
   118  }
   119  
   120  func (c *Client) load(ctx context.Context) (*spec.Swagger, error) {
   121  	ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
   122  	defer cancel()
   123  
   124  	snap, err := c.variable.Latest(ctx)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	var b []byte
   130  	switch v := snap.Value.(type) {
   131  	case []byte:
   132  		b = v
   133  	case string:
   134  		b = []byte(v)
   135  	default:
   136  		return nil, fmt.Errorf("unhandled type: %v", v)
   137  	}
   138  
   139  	var doc spec.Swagger
   140  	if err := json.Unmarshal(b, &doc); err != nil {
   141  		return nil, err
   142  	}
   143  	c.val = b
   144  	c.doc = &doc
   145  
   146  	if err := spec.ExpandSpec(&doc, &spec.ExpandOptions{}); err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	// build attrs
   151  	if doc.Paths == nil {
   152  		return &doc, nil
   153  	}
   154  	//attrs := make(map[string]*Service)
   155  	//attrNames := make([]string, 0, len(doc.Tags))
   156  	//tagNames := make(map[string]string)
   157  	services := make(map[string]*Service)
   158  
   159  	for path, item := range doc.Paths.Paths {
   160  		key := toSnakeCase(path)
   161  
   162  		var count int
   163  		addMethod := func(op *spec.Operation, method string) {
   164  			count++
   165  			var svcNames []string
   166  			for _, tag := range op.Tags {
   167  				svcNames = append(svcNames, strcase.ToSnake(tag))
   168  			}
   169  			if len(svcNames) == 0 {
   170  				svcNames = append(svcNames, key)
   171  			}
   172  
   173  			mdName := strings.ToLower(method) + "_" + key
   174  			if id := op.ID; id != "" {
   175  				mdName = strcase.ToSnake(id)
   176  			}
   177  
   178  			m := &Method{
   179  				c:    c,
   180  				name: mdName,
   181  				path: path,
   182  				op:   op,
   183  				//params: item.Parameters,
   184  				method: method,
   185  			}
   186  
   187  			for _, svcName := range svcNames {
   188  				svc, ok := services[svcName]
   189  				if !ok {
   190  					svc = &Service{
   191  						name:    svcName,
   192  						methods: make(map[string]*Method),
   193  					}
   194  					services[svcName] = svc
   195  				}
   196  				svc.methods[mdName] = m
   197  			}
   198  		}
   199  
   200  		if v := item.Get; v != nil {
   201  			addMethod(v, http.MethodGet)
   202  		}
   203  		if v := item.Put; v != nil {
   204  			addMethod(v, http.MethodPut)
   205  		}
   206  		if v := item.Post; v != nil {
   207  			addMethod(v, http.MethodPost)
   208  		}
   209  		if v := item.Delete; v != nil {
   210  			addMethod(v, http.MethodDelete)
   211  		}
   212  		if v := item.Options; v != nil {
   213  			addMethod(v, http.MethodOptions)
   214  		}
   215  		if v := item.Head; v != nil {
   216  			addMethod(v, http.MethodHead)
   217  		}
   218  		if v := item.Patch; v != nil {
   219  			addMethod(v, http.MethodPatch)
   220  		}
   221  
   222  		if count == 0 {
   223  			return nil, fmt.Errorf("missing operations for path: %s", path)
   224  		}
   225  	}
   226  
   227  	c.svcs = services
   228  	return &doc, nil
   229  }
   230  
   231  func (c *Client) makeURL(urlPath string, urlQuery url.Values) url.URL {
   232  	scheme := "http"
   233  	if x := c.doc.Schemes; len(x) > 0 {
   234  		scheme = x[0]
   235  	}
   236  	return url.URL{
   237  		Scheme:   scheme,
   238  		Host:     c.doc.Host,
   239  		Path:     path.Join(c.doc.BasePath, urlPath),
   240  		RawQuery: urlQuery.Encode(),
   241  	}
   242  }
   243  
   244  func (c *Client) String() string        { return fmt.Sprintf("<client %q>", c.name) }
   245  func (c *Client) Type() string          { return "openapi.client" }
   246  func (c *Client) Freeze()               {} // immutable?
   247  func (c *Client) Truth() starlark.Bool  { return c.variable.CheckHealth() == nil }
   248  func (c *Client) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable type: %s", c.Type()) }
   249  func (c *Client) Close() error {
   250  	return c.variable.Close()
   251  }
   252  
   253  func (c *Client) Attr(name string) (starlark.Value, error) {
   254  	if s, ok := c.svcs[name]; ok {
   255  		return s, nil
   256  	}
   257  	if name == "schema" {
   258  		return starlark.String(string(c.val)), nil
   259  	}
   260  	return nil, nil
   261  }
   262  func (c *Client) AttrNames() []string {
   263  	names := make([]string, 0, len(c.svcs))
   264  	for name := range c.svcs {
   265  		names = append(names, name)
   266  	}
   267  	sort.Strings(names)
   268  	return names
   269  }
   270  
   271  type Service struct {
   272  	name    string
   273  	methods map[string]*Method
   274  }
   275  
   276  func (s *Service) String() string        { return fmt.Sprintf("<service %q>", s.name) }
   277  func (s *Service) Type() string          { return "openapi.service" }
   278  func (s *Service) Freeze()               {} // immutable?
   279  func (s *Service) Truth() starlark.Bool  { return s.name != "" }
   280  func (s *Service) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable type: %s", s.Type()) }
   281  func (s *Service) Attr(name string) (starlark.Value, error) {
   282  	if m, ok := s.methods[name]; ok {
   283  		return m, nil
   284  	}
   285  	return nil, nil
   286  }
   287  func (s *Service) AttrNames() []string {
   288  	names := make([]string, 0, len(s.methods))
   289  	for name := range s.methods {
   290  		names = append(names, name)
   291  	}
   292  	sort.Strings(names)
   293  	return names
   294  }
   295  
   296  type Method struct {
   297  	c *Client
   298  
   299  	name string
   300  	path string
   301  	op   *spec.Operation
   302  	//params []spec.Parameter
   303  	method string
   304  }
   305  
   306  func (m *Method) String() string        { return fmt.Sprintf("<method %q>", m.name) }
   307  func (m *Method) Type() string          { return "openapi.method" }
   308  func (m *Method) Freeze()               {} // immutable?
   309  func (m *Method) Truth() starlark.Bool  { return m.name != "" }
   310  func (m *Method) Hash() (uint32, error) { return starlark.String(m.path).Hash() }
   311  
   312  var (
   313  	starlarkJSONEncode = starlarkjson.Module.Members["encode"].(*starlark.Builtin)
   314  	starlarkJSONDecode = starlarkjson.Module.Members["decode"].(*starlark.Builtin)
   315  )
   316  
   317  func (m *Method) Name() string { return m.name }
   318  func (m *Method) CallInternal(thread *starlark.Thread, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   319  	ctx := starlarkthread.GetContext(thread)
   320  	hasArgs := len(args) > 0
   321  	//hasKwargs := len(kwargs) > 0
   322  
   323  	if hasArgs {
   324  		return nil, fmt.Errorf("unexpected args")
   325  	}
   326  
   327  	var (
   328  		params        = m.op.Parameters
   329  		vals          = make([]interface{}, 0, len(params))
   330  		pairsRequired []interface{}
   331  		pairsOptional []interface{}
   332  	)
   333  	for i, param := range params {
   334  		kw := param.Name
   335  		switch typ := param.Type; typ {
   336  		case "array":
   337  			vals = append(vals, (*starlark.List)(nil))
   338  		case "string":
   339  			vals = append(vals, "")
   340  		case "integer":
   341  			vals = append(vals, (int)(0))
   342  		case "number":
   343  			vals = append(vals, (float64)(0))
   344  		case "boolean":
   345  			vals = append(vals, (bool)(false))
   346  		case "file":
   347  			// Tuple of (filename, source) where source
   348  			// accepts String, Bytes, Reader.
   349  			// content-type must be form data.
   350  			vals = append(vals, starlark.Value(nil)) // starlark.Tuple(nil))
   351  		default:
   352  			if param.Schema == nil {
   353  				return nil, fmt.Errorf("unknown type: %s", typ)
   354  			}
   355  			// ???
   356  			vals = append(vals, (*starlark.Value)(nil))
   357  		}
   358  		if param.Required {
   359  			pairsRequired = append(pairsRequired, kw, &vals[i])
   360  		} else {
   361  			pairsOptional = append(pairsOptional, kw+"?", &vals[i])
   362  		}
   363  	}
   364  
   365  	pairs := append(pairsRequired, pairsOptional...)
   366  	if err := starlark.UnpackArgs(m.name, args, kwargs, pairs...); err != nil {
   367  		return nil, err
   368  	}
   369  
   370  	chooseType := func(typs []string) string {
   371  		var typ string
   372  		if n := len(typs); n > 0 {
   373  			typ = typs[0]
   374  		} else if n > 1 {
   375  			for _, altTyp := range typs[1:] {
   376  				if altTyp == "application/json" {
   377  					typ = "application/json"
   378  				}
   379  			}
   380  		}
   381  		return typ
   382  	}
   383  
   384  	var (
   385  		urlPath      = m.path
   386  		urlVals      = make(url.Values)
   387  		headers      = make(http.Header)
   388  		body         io.Reader
   389  		formWriter   *multipart.Writer
   390  		consumesType = chooseType(m.op.Consumes)
   391  		producesType = chooseType(m.op.Produces)
   392  	)
   393  
   394  	headers.Set("Content-Type", consumesType)
   395  	headers.Set("Accepts", producesType)
   396  
   397  	for i, param := range params {
   398  		arg := vals[i]
   399  		if arg == nil {
   400  			continue // optional?
   401  		}
   402  
   403  		switch v := param.In; v {
   404  		case "body":
   405  			// create JSON?
   406  			switch typ := consumesType; typ {
   407  			case "application/json":
   408  				v, ok := arg.(starlark.Value)
   409  				if !ok {
   410  					return nil, fmt.Errorf("unknown body arg: %T %v", arg, arg)
   411  				}
   412  				rsp, err := starlark.Call(
   413  					thread, starlarkJSONEncode, starlark.Tuple{v}, nil,
   414  				)
   415  				if err != nil {
   416  					return nil, err
   417  				}
   418  				body = strings.NewReader(
   419  					string(rsp.(starlark.String)),
   420  				)
   421  
   422  			default:
   423  				return nil, fmt.Errorf("unknown consume type: %s", typ)
   424  			}
   425  
   426  		case "path":
   427  			key := "{" + param.Name + "}"
   428  			val := vals[i]
   429  			if i := strings.Index(urlPath, key); i == -1 {
   430  				return nil, fmt.Errorf("missing path variable: %s", key)
   431  			} else {
   432  				urlPath = fmt.Sprintf(
   433  					"%s%v%s", urlPath[:i], val, urlPath[i+len(key):],
   434  				)
   435  			}
   436  
   437  		case "query":
   438  			switch v := arg.(type) {
   439  			case string:
   440  				urlVals.Set(param.Name, v)
   441  			case int:
   442  				urlVals.Set(param.Name, strconv.Itoa(v))
   443  			case bool:
   444  				if v {
   445  					urlVals.Set(param.Name, "true")
   446  				}
   447  			case *starlark.List:
   448  				for i := 0; i < v.Len(); i++ {
   449  					switch v := v.Index(i).(type) {
   450  					case starlark.String:
   451  						urlVals.Set(param.Name, string(v))
   452  					case starlark.Int:
   453  						x, _ := v.Int64()
   454  						urlVals.Set(param.Name, strconv.Itoa(int(x)))
   455  					case starlark.Bool:
   456  						if bool(v) {
   457  							urlVals.Set(param.Name, "true")
   458  						}
   459  					default:
   460  						return nil, fmt.Errorf("invalid param list type: %T %v", v, v)
   461  					}
   462  				}
   463  			default:
   464  				return nil, fmt.Errorf("unknown param type: %T %v", v, v)
   465  			}
   466  
   467  		case "header":
   468  			switch v := arg.(type) {
   469  			case string:
   470  				headers.Add(param.Name, v)
   471  			case int:
   472  				headers.Add(param.Name, strconv.Itoa(v))
   473  			case bool:
   474  				if v {
   475  					headers.Add(param.Name, "true")
   476  				}
   477  			default:
   478  				return nil, fmt.Errorf("unknown header type: %T %v", v, v)
   479  			}
   480  
   481  		case "formData":
   482  			switch consumesType {
   483  			case "multipart/form-data":
   484  				if body == nil {
   485  					buf := new(bytes.Buffer)
   486  					formWriter = multipart.NewWriter(buf)
   487  					// TODO: check this is okay.
   488  					x := crc32.ChecksumIEEE([]byte(m.path))
   489  					if err := formWriter.SetBoundary(
   490  						fmt.Sprintf("%x%x%x", x, x, x),
   491  					); err != nil {
   492  						return nil, err
   493  					}
   494  					body = buf
   495  				}
   496  
   497  				switch param.Type {
   498  				case "file":
   499  					val, ok := arg.(starlark.Tuple)
   500  					if !ok || len(val) != 2 {
   501  						// TODO: better typed errors.
   502  						return nil, fmt.Errorf("expected tuple(filename, source) got %v", arg)
   503  					}
   504  
   505  					filename, ok := starlark.AsString(val[0])
   506  					if !ok {
   507  						return nil, fmt.Errorf("filename must be a string, got %v", val[0])
   508  					}
   509  
   510  					fw, err := formWriter.CreateFormFile(param.Name, filename)
   511  					if err != nil {
   512  						return nil, err
   513  					}
   514  
   515  					var r io.Reader
   516  					switch v := val[1].(type) {
   517  					case starlark.String:
   518  						r = strings.NewReader(string(v))
   519  					case starlark.Bytes:
   520  						r = strings.NewReader(string(v))
   521  					case io.Reader:
   522  						r = v
   523  					default:
   524  						return nil, fmt.Errorf("unknown form type: %T %v", v, v)
   525  					}
   526  					if _, err := io.Copy(fw, r); err != nil {
   527  						return nil, err
   528  					}
   529  				default:
   530  					// TODO: type handling
   531  					s := fmt.Sprintf("%v", arg)
   532  					if err := formWriter.WriteField(param.Name, s); err != nil {
   533  						return nil, err
   534  					}
   535  				}
   536  
   537  			case "application/x-www-form-urlencoded":
   538  				return nil, fmt.Errorf("unimplemented consume type: %s", consumesType)
   539  
   540  			default:
   541  				return nil, fmt.Errorf("unexpected consumes type %v for \"formData\"", consumesType)
   542  			}
   543  
   544  		default:
   545  			return nil, fmt.Errorf("unhandled parameter in: %s", v)
   546  		}
   547  	}
   548  	if formWriter != nil {
   549  		if err := formWriter.Close(); err != nil {
   550  			return nil, err
   551  		}
   552  		headers.Set("Content-Type", formWriter.FormDataContentType())
   553  	}
   554  
   555  	u := m.c.makeURL(urlPath, urlVals)
   556  
   557  	urlStr := u.String()
   558  
   559  	req, err := http.NewRequestWithContext(ctx, m.method, urlStr, body)
   560  	if err != nil {
   561  		return nil, err
   562  	}
   563  	req.Header = headers
   564  
   565  	rsp, err := m.c.do(thread, m.name, &starlarkhttp.Request{
   566  		Request: req,
   567  	})
   568  	if err != nil {
   569  		return nil, err
   570  	}
   571  	defer rsp.Body.Close()
   572  
   573  	rspTyp, rspOk := m.op.Responses.StatusCodeResponses[rsp.StatusCode]
   574  	rspDef := m.op.Responses.Default
   575  
   576  	// Produce struct or array
   577  	switch typ := producesType; typ {
   578  	case "application/json":
   579  		rspBody, err := ioutil.ReadAll(rsp.Body)
   580  		if err != nil {
   581  			return nil, err
   582  		}
   583  
   584  		bodyStr := starlark.String(rspBody)
   585  
   586  		// Load schema
   587  		val, err := starlark.Call(
   588  			thread, starlarkJSONDecode, starlark.Tuple{bodyStr}, nil,
   589  		)
   590  		if err != nil {
   591  			return nil, err
   592  		}
   593  
   594  		if rsp.StatusCode/100 != 2 {
   595  			return nil, fmt.Errorf("%s: %q", rsp.Status, val)
   596  		}
   597  		// Try to return a typed response.
   598  		if rspOk {
   599  			return toStruct(rspTyp.Schema, val)
   600  		}
   601  		if rspDef != nil {
   602  			return toStruct(rspDef.Schema, val)
   603  		}
   604  		// TODO: convert anyway?
   605  		return val, nil
   606  
   607  	default:
   608  		return nil, fmt.Errorf("%s: unknown produces type: %s", rsp.Status, typ)
   609  	}
   610  }
   611  
   612  func errKeyValue(schema *spec.Schema, want string, v starlark.Value) error {
   613  	return fmt.Errorf("invalid type for %s, want %s got %s", schema.ID, want, v.Type())
   614  }
   615  
   616  func typeStr(schema *spec.Schema) string {
   617  	return strings.Join([]string(schema.Type), ",")
   618  }
   619  
   620  // TODO: build typed Dict and typed Lists.
   621  func toStruct(schema *spec.Schema, v starlark.Value) (starlark.Value, error) {
   622  
   623  	switch v := v.(type) {
   624  	case *starlark.Dict:
   625  		if typ := typeStr(schema); typ != "object" {
   626  			return nil, errKeyValue(schema, "dict", v)
   627  		}
   628  		// TODO: typed structs?
   629  		//constructor := starlark.String(schema.ID)
   630  		constructor := starlarkstruct.Default
   631  		kwargs := v.Items()
   632  
   633  		// TODO: validate spec.
   634  		for _, kwarg := range kwargs {
   635  			k, ok := starlark.AsString(kwarg[0])
   636  			if !ok {
   637  				return nil, fmt.Errorf("invalid key %s", k)
   638  			}
   639  			v := kwarg[1]
   640  
   641  			keySchema, ok := schema.Properties[k]
   642  			if !ok {
   643  				return nil, fmt.Errorf("unpexpected key %s", k)
   644  			}
   645  
   646  			x, err := toStruct(&keySchema, v)
   647  			if err != nil {
   648  				return nil, err
   649  			}
   650  			kwarg[1] = x
   651  		}
   652  
   653  		s := starlarkstruct.FromKeywords(constructor, kwargs)
   654  		return s, nil
   655  
   656  	case *starlark.List:
   657  		if typeStr(schema) != "array" {
   658  			return nil, errKeyValue(schema, "list", v)
   659  		}
   660  		if items := schema.Items; items == nil || items.Schema == nil {
   661  			return nil, fmt.Errorf("unepected items schema: %v", items)
   662  		}
   663  		keySchema := schema.Items.Schema
   664  
   665  		// TODO: validate spec.
   666  		for i := 0; i < v.Len(); i++ {
   667  			x, err := toStruct(keySchema, v.Index(i))
   668  			if err != nil {
   669  				return nil, err
   670  			}
   671  			if err := v.SetIndex(i, x); err != nil {
   672  				return nil, err
   673  			}
   674  		}
   675  		return v, nil
   676  
   677  	case starlark.String:
   678  		switch typeStr(schema) {
   679  		case "string", "password":
   680  			return v, nil
   681  		case "byte", "binary":
   682  			data, err := base64.StdEncoding.DecodeString(string(v))
   683  			if err != nil {
   684  				return nil, err
   685  			}
   686  			return starlark.Bytes(string(data)), nil
   687  		case "date":
   688  			t, err := time.Parse("2006-Jan-02", string(v))
   689  			if err != nil {
   690  				return nil, err
   691  			}
   692  			return starlarktime.Time(t), nil
   693  		case "date-time":
   694  			t, err := time.Parse(time.RFC3339, string(v))
   695  			if err != nil {
   696  				return nil, err
   697  			}
   698  			return starlarktime.Time(t), nil
   699  		default:
   700  			return v, nil // TODO: warn?
   701  		}
   702  
   703  	case starlark.Int:
   704  		if typeStr(schema) != "integer" {
   705  			return nil, errKeyValue(schema, "int", v)
   706  		}
   707  		return v, nil
   708  
   709  	case starlark.Float:
   710  		if typeStr(schema) != "number" {
   711  			return nil, errKeyValue(schema, "float", v)
   712  		}
   713  		return v, nil
   714  
   715  	case starlark.Bool:
   716  		if typeStr(schema) != "boolean" {
   717  			return nil, errKeyValue(schema, "bool", v)
   718  		}
   719  		return v, nil
   720  
   721  	default:
   722  		// TODO: validate spec?
   723  		return v, nil
   724  	}
   725  }
   726  
   727  func NewMessage(schema *spec.Schema, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   728  	hasArgs := len(args) > 0
   729  	hasKwargs := len(kwargs) > 0
   730  
   731  	if hasArgs && len(args) > 1 {
   732  		return nil, fmt.Errorf("unexpected number of args")
   733  	}
   734  
   735  	if hasArgs && hasKwargs {
   736  		return nil, fmt.Errorf("unxpected args and kwargs")
   737  	}
   738  
   739  	if hasArgs {
   740  		return toStruct(schema, args[0])
   741  	}
   742  
   743  	return nil, fmt.Errorf("TODO: kwargs")
   744  }