Skip to content

Commit

Permalink
feat: trial for potential new template feature
Browse files Browse the repository at this point in the history
cloud.NewTemplate can parse a struct and return a cloud.Template
nested map. The Template String() method will return the text DSL
version of the struct.
  • Loading branch information
masonkatz committed Feb 15, 2024
1 parent 341f6a3 commit 117e2a4
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 20 deletions.
54 changes: 38 additions & 16 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,21 +203,18 @@ func (c *Client) requestWrapper(ctx context.Context, method, path string, in, ou
return nil
}

func (c *Client) request(ctx context.Context, method, path string, in, out interface{}) error {
var r io.Reader = http.NoBody

if in != nil { // encode body
b, err := json.Marshal(in)
if err != nil {
return err
}

r = bytes.NewReader(b)
// Request sends a HTTP request to the Server. The optional request body is read
// from r, and any response body is read and returned. Clients can use this
// low-level method to make arbitrary requests and avoid marshaling and
// unmarshaling JSON.
func (c *Client) Request(ctx context.Context, method, path string, r io.Reader) (*bytes.Buffer, error) {
if r == nil {
r = http.NoBody
}

req, err := http.NewRequestWithContext(ctx, method, c.url(path), r)
if err != nil {
return err
return nil, err
}

req.Header.Set("Authorization", "Bearer "+c.Token)
Expand All @@ -227,27 +224,52 @@ func (c *Client) request(ctx context.Context, method, path string, in, out inter

resp, err := c.Do(req)
if err != nil {
fmt.Printf("do error:%v\n", err)
return err
return nil, err
}

defer resp.Body.Close()

if c.Debug {
b, err := httputil.DumpResponse(resp, true)
if err != nil {
return err
return nil, err
}

fmt.Printf("RESPONSE:\n%s", string(b))
}

if resp.StatusCode < http.StatusOK || resp.StatusCode > 299 {
return c.ErrorParser(resp.StatusCode, resp.Body)
return nil, c.ErrorParser(resp.StatusCode, resp.Body)
}

var buf bytes.Buffer

if _, err = buf.ReadFrom(resp.Body); err != nil {
return nil, err
}

return &buf, nil
}

func (c *Client) request(ctx context.Context, method, path string, in, out interface{}) error {
var r io.Reader = http.NoBody

if in != nil { // encode body
b, err := json.Marshal(in)
if err != nil {
return err
}

r = bytes.NewReader(b)
}

resp, err := c.Request(ctx, method, path, r)
if err != nil {
return err
}

if out != nil { // parse response
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
if err := json.NewDecoder(resp).Decode(out); err != nil {
return err
}
}
Expand Down
4 changes: 2 additions & 2 deletions cloud/instance/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ type Template struct {
HypervOptions string
Imported string
Input string
InstanceGroup []string
InstanceID int
InstanceGroup []string `template:"VMGROUP"`
InstanceID int `template:"VMID"`
Memory int
MemoryCost string
MemoryMax string
Expand Down
7 changes: 7 additions & 0 deletions cloud/instance/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ func ParseTemplate(m map[string]any) (*Template, error) { // nolint:gocognit

for key, value := range m {
switch v := value.(type) {
case []string:
switch key {
case "SECURITY_GROUP_RULE":
copy(dst.SecurityGroupRule, v)
case "VMGROUP":
copy(dst.InstanceGroup, v)
}
case string:
switch key {
case "AUTOMATIC_DS_REQUIREMENTS":
Expand Down
187 changes: 185 additions & 2 deletions cloud/template.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package cloud

import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"unicode"
)

// Template is nested map of string key x value pairs.
Expand Down Expand Up @@ -101,13 +104,24 @@ func pad(s string, length int) string {
func templateString(indent, ljust int, delim, key string, value any) string {
var templates []map[string]any

lhs := fmt.Sprintf("%s%s= ", strings.Repeat("\t", indent), pad(key, ljust))

switch v := value.(type) {
case string:
return fmt.Sprintf("%s%s = %q%s", strings.Repeat("\t", indent), pad(key, ljust), v, delim)
case map[string]any:
templates = []map[string]any{v}
case []map[string]any:
templates = v
case string, fmt.Stringer:
return lhs + fmt.Sprintf("%q", v) + delim
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return lhs + fmt.Sprintf("%v", v) + delim
case bool:
if v {
return lhs + "YES" + delim
}
return lhs + "NO" + delim
default:
return lhs + fmt.Sprintf("\"unknown value type: %v\"", v) + delim
}

var buf strings.Builder
Expand Down Expand Up @@ -144,3 +158,172 @@ func templateString(indent, ljust int, delim, key string, value any) string {

return buf.String()
}

// NewTemplate returns a new Template from the struct t. If t is not a struct
// (or a pointer to one) it panics.
func NewTemplate(t any) Template {
rv := reflect.ValueOf(t)

if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}

if rv.Kind() != reflect.Struct {
panic("NewTemplate: expected struct, got " + rv.Kind().String())
}

return newTemplate(rv.Interface())
}

// newTemplate returns a new map[string]any from the struct t. This exists so we
// can recursively call it and always return a map[string]any. And only deal
// with the Template type alias at the top level. This is done to simplify the
// constructed nested map, which keeps the Template String method "simple".
//
// This is not intended to be general purpose and only needs to handle types the
// are used in the Template structs (which we create).
func newTemplate(t any) map[string]any { //nolint:gocognit
template := make(map[string]any)
rv := reflect.ValueOf(t)
rt := reflect.TypeOf(t)

for i := 0; i < rv.NumField(); i++ {
var options tagOptions

field := rt.Field(i)
fieldName := camelToSnake(field.Name)

if tag := field.Tag.Get("template"); tag != "" {
fieldName, options = parseTag(tag)
}

fv := rv.Field(i)

switch field.Type.Kind() {
case reflect.Struct:
m := newTemplate(rv.Field(i).Interface())
if len(m) > 0 || options.Contains(always) {
template[fieldName] = newTemplate(rv.Field(i).Interface())
}
case reflect.Slice, reflect.Array:
if fv.Len() == 0 && !options.Contains(always) {
continue
}

m := make([]map[string]any, fv.Len())
for i := 0; i < fv.Len(); i++ {
m[i] = newTemplate(fv.Index(i).Interface())
}
template[fieldName] = m
case reflect.Pointer:
if fv.IsNil() { // only handles pointers to structs or simple types
continue
}

fv = fv.Elem()
if fv.Kind() == reflect.Struct {
template[fieldName] = newTemplate(fv.Interface())
continue
}

fallthrough

default:
if fv.IsZero() && !options.Contains(always) {
continue
}

v := simpleValue(fv)

if options.Contains("string") { // not sure about this, might not be needed or might want for other types
template[fieldName] = fmt.Sprint(v)
continue
}

template[fieldName] = v
}
}

return template
}

func simpleValue(v reflect.Value) any {
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}

switch v.Kind() {
case reflect.String:
return v.String()
case reflect.Bool:
return v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint()
case reflect.Float32, reflect.Float64:
return v.Float()
}

return "<nil>"
}

const always = "always" // opposite of omitempty, default is to omit empty fields

// camelToSnake converts a camelCase string to SNAKE_CASE.
func camelToSnake(s string) string {
var buffer bytes.Buffer

for i, char := range s {
if i > 0 && unicode.IsUpper(char) {
var next rune

if i+1 < len(s) {
next = rune(s[i+1])
}
prev := rune(s[i-1])

if (unicode.IsDigit(prev) || unicode.IsLower(prev)) || unicode.IsLower(next) {
buffer.WriteRune('_')
}
}
buffer.WriteRune(unicode.ToUpper(char))
}

return buffer.String()
}

// The following code is from the Go standard library and is licensed under a
// BSD-style license. Specifically, this is from the encoding/json package.

// tagOptions is the string following a comma in a struct field's "json" tag, or
// the empty string. It does not include the leading comma.
type tagOptions string

// parseTag splits a struct field's json tag into its name and comma-separated
// options.
func parseTag(tag string) (string, tagOptions) {
tag, opt, _ := strings.Cut(tag, ",")
return tag, tagOptions(opt)
}

// Contains reports whether a comma-separated list of options contains a
// particular substr flag. substr must be surrounded by a string boundary or
// commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var name string
name, s, _ = strings.Cut(s, ",")
if name == optionName {
return true
}
}
return false
}

0 comments on commit 117e2a4

Please sign in to comment.