github.com/kumasuke120/mockuma@v1.1.9/internal/mckmaps/template.go (about)

     1  package mckmaps
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/kumasuke120/mockuma/internal/myjson"
    10  	"github.com/kumasuke120/mockuma/internal/types"
    11  )
    12  
    13  type template struct {
    14  	content  interface{}
    15  	defaults *vars
    16  	filename string
    17  }
    18  
    19  type templateParser struct {
    20  	json     myjson.Object
    21  	jsonPath *myjson.Path
    22  	Parser
    23  }
    24  
    25  var parsingTemplates []string
    26  
    27  func (p *templateParser) parse() (*template, error) {
    28  	needLoading := p.json == nil
    29  
    30  	if needLoading { // adds the current file
    31  		parsingTemplates = append(parsingTemplates, p.filename)
    32  		err := p.checkCyclicReference()
    33  		if err != nil {
    34  			return nil, err
    35  		}
    36  	}
    37  
    38  	if needLoading { // loads file raw content
    39  		json, err := p.load(true, ppRemoveComment, ppRenderTemplate)
    40  		if err != nil {
    41  			return nil, err
    42  		}
    43  
    44  		switch json.(type) {
    45  		case myjson.Object:
    46  			p.json = json.(myjson.Object)
    47  		default:
    48  			return nil, p.newJSONParseError(p.jsonPath)
    49  		}
    50  	}
    51  
    52  	p.jsonPath = myjson.NewPath("")
    53  	p.jsonPath.SetLast(aType)
    54  	_type, err := p.json.GetString(aType)
    55  	if err != nil || string(_type) != tTemplate {
    56  		return nil, p.newJSONParseError(p.jsonPath)
    57  	}
    58  
    59  	template := new(template)
    60  
    61  	p.jsonPath.SetLast(tTemplate)
    62  	v := p.json.Get(tTemplate)
    63  	content, err := p.parseContent(v)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	template.content = content
    68  
    69  	p.jsonPath.SetLast(tVars)
    70  	var defaults *vars
    71  	if p.json.Has(tVars) {
    72  		vo, err := p.json.GetObject(tVars)
    73  		if err != nil {
    74  			return nil, p.newJSONParseError(p.jsonPath)
    75  		}
    76  		defaults, err = p.parseDefaults(vo)
    77  		if err != nil {
    78  			return nil, err
    79  		}
    80  	} else {
    81  		defaults = &vars{table: map[string]interface{}{}}
    82  	}
    83  	template.defaults = defaults
    84  
    85  	template.filename = p.filename
    86  
    87  	if needLoading { // removes the current file with checking
    88  		if parsingTemplates[len(parsingTemplates)-1] == p.filename {
    89  			parsingTemplates = parsingTemplates[:len(parsingTemplates)-1]
    90  		} else {
    91  			panic("Shouldn't happen")
    92  		}
    93  	}
    94  	return template, nil
    95  }
    96  
    97  func (p *templateParser) parseContent(v interface{}) (interface{}, error) {
    98  	switch v.(type) {
    99  	case myjson.Object:
   100  		return v, nil
   101  	case myjson.Array:
   102  		return v, nil
   103  	case myjson.String:
   104  		return v, nil
   105  	default:
   106  		return nil, p.newJSONParseError(p.jsonPath)
   107  	}
   108  }
   109  
   110  func (p *templateParser) parseDefaults(v myjson.Object) (*vars, error) {
   111  	r, err := parseVars(v)
   112  	if err != nil {
   113  		return nil, p.newJSONParseError(p.jsonPath)
   114  	}
   115  	return r, nil
   116  }
   117  
   118  func (p *templateParser) checkCyclicReference() error {
   119  	found := make(map[string]bool)
   120  	for _, t := range parsingTemplates {
   121  		if _, ok := found[t]; ok {
   122  			return &loadError{
   123  				filename: p.filename,
   124  				err: errors.New("found a cyclic template application : " +
   125  					strings.Join(parsingTemplates, " -> ")),
   126  			}
   127  		} else {
   128  			found[t] = true
   129  		}
   130  	}
   131  	return nil
   132  }
   133  
   134  type renderError struct {
   135  	filename string
   136  	jsonPath *myjson.Path
   137  }
   138  
   139  func (e *renderError) Error() string {
   140  	result := ""
   141  	if e.jsonPath == nil {
   142  		result += "cannot render template"
   143  	} else {
   144  		result += fmt.Sprintf("cannot render the template on json-path \"%v\"", e.jsonPath)
   145  	}
   146  
   147  	if e.filename != "" {
   148  		result += fmt.Sprintf(" in the file '%s'", e.filename)
   149  	}
   150  
   151  	return result
   152  }
   153  
   154  func (t *template) renderAll(varsSlice []*vars) (myjson.Array, error) {
   155  	if len(varsSlice) == 0 {
   156  		return myjson.Array{}, nil
   157  	}
   158  
   159  	result := make(myjson.Array, len(varsSlice))
   160  	for idx, _var := range varsSlice {
   161  		v, err := t.render(nil, t.content, _var)
   162  		if err != nil {
   163  			return nil, err
   164  		}
   165  		result[idx] = v
   166  	}
   167  	return result, nil
   168  }
   169  
   170  func (t *template) render(jsonPath *myjson.Path, v interface{}, varsSlice *vars) (interface{}, error) {
   171  	if jsonPath == nil {
   172  		jsonPath = myjson.NewPath()
   173  	}
   174  
   175  	var result interface{}
   176  	var err error
   177  	switch v.(type) {
   178  	case myjson.Object:
   179  		result, err = t.renderObject(jsonPath, v.(myjson.Object), varsSlice)
   180  	case myjson.Array:
   181  		result, err = t.renderArray(jsonPath, v.(myjson.Array), varsSlice)
   182  	case myjson.String:
   183  		result, err = t.renderString(jsonPath, v.(myjson.String), varsSlice)
   184  	default:
   185  		result, err = v, nil
   186  	}
   187  
   188  	return result, err
   189  }
   190  
   191  func (t *template) renderObject(jsonPath *myjson.Path,
   192  	v myjson.Object, vars *vars) (myjson.Object, error) {
   193  	jsonPath.Append("")
   194  
   195  	result := make(myjson.Object)
   196  	for name, value := range v {
   197  		jsonPath.SetLast(name)
   198  
   199  		rName, err := t.renderPlainString(jsonPath, name, vars)
   200  		if err != nil {
   201  			return nil, err
   202  		}
   203  		rValue, err := t.render(jsonPath, value, vars)
   204  		if err != nil {
   205  			return nil, err
   206  		}
   207  		result[rName] = rValue
   208  	}
   209  
   210  	jsonPath.RemoveLast()
   211  	return result, nil
   212  }
   213  
   214  func (t *template) renderArray(jsonPath *myjson.Path,
   215  	v myjson.Array, vars *vars) (myjson.Array, error) {
   216  	jsonPath.Append(0)
   217  
   218  	result := make(myjson.Array, len(v))
   219  	for idx, value := range v {
   220  		jsonPath.SetLast(idx)
   221  
   222  		rValue, err := t.render(jsonPath, value, vars)
   223  		if err != nil {
   224  			return nil, err
   225  		}
   226  		result[idx] = rValue
   227  	}
   228  
   229  	jsonPath.RemoveLast()
   230  	return result, nil
   231  }
   232  
   233  // states for rendering string
   234  const (
   235  	rsReady = iota
   236  	rsMaybePlaceholder
   237  	rsInPlaceholder
   238  	rsMaybePlaceHolderFormat
   239  	rsInPlaceHolderFormat
   240  )
   241  
   242  // tokens for rendering string
   243  const (
   244  	placeholderPrefix          = '@'
   245  	placeholderLeft            = '{'
   246  	placeholderRight           = '}'
   247  	placeholderFormatSeparator = ':'
   248  )
   249  
   250  func (t *template) renderPlainString(jsonPath *myjson.Path,
   251  	v string, vars *vars) (string, error) {
   252  	r, err := t.renderString(jsonPath, myjson.String(v), vars)
   253  	if err != nil {
   254  		return "", err
   255  	} else {
   256  		switch r.(type) {
   257  		case myjson.String:
   258  			return string(r.(myjson.String)), nil
   259  		default:
   260  			return types.ToString(r), nil
   261  		}
   262  	}
   263  }
   264  
   265  func (t *template) renderString(jsonPath *myjson.Path, v myjson.String, vars *vars) (interface{}, error) {
   266  	s := rsReady
   267  
   268  	runes := []rune(v)
   269  
   270  	var fromBegin, toEnd bool
   271  
   272  	var builder strings.Builder
   273  	var nameBuilder strings.Builder
   274  	var formatBuilder strings.Builder
   275  
   276  	for i := 0; i < len(runes); i++ {
   277  		r := runes[i]
   278  		doWrite := true
   279  		doWriteName := false
   280  		doWriteFormat := false
   281  
   282  		switch s {
   283  		case rsReady:
   284  			if r == placeholderPrefix {
   285  				s = rsMaybePlaceholder
   286  				if i == 0 {
   287  					fromBegin = true
   288  				} else {
   289  					fromBegin = false
   290  				}
   291  				doWrite = false
   292  			}
   293  		case rsMaybePlaceholder:
   294  			if r == placeholderLeft {
   295  				s = rsInPlaceholder
   296  				doWrite = false
   297  			} else {
   298  				s = rsReady
   299  				if r != placeholderPrefix { // replaces "@@" to "@"
   300  					builder.WriteString(string(placeholderPrefix))
   301  				}
   302  				if fromBegin {
   303  					fromBegin = false
   304  				}
   305  			}
   306  		case rsInPlaceholder:
   307  			doWrite = false
   308  			if r == placeholderRight {
   309  				s = rsReady
   310  				if fromBegin && i == len(runes)-1 {
   311  					toEnd = true
   312  				} else {
   313  					varName := nameBuilder.String()
   314  					varFormat := formatBuilder.String()
   315  					if varName == "" {
   316  						return "", &renderError{filename: t.filename, jsonPath: jsonPath}
   317  					}
   318  
   319  					v, err := t.renderTextString(vars, varName, varFormat)
   320  					if err != nil {
   321  						return nil, &renderError{filename: t.filename, jsonPath: jsonPath}
   322  					}
   323  					builder.WriteString(v)
   324  					nameBuilder.Reset()
   325  					formatBuilder.Reset()
   326  				}
   327  			} else if r == placeholderFormatSeparator {
   328  				s = rsMaybePlaceHolderFormat
   329  			} else {
   330  				doWriteName = true
   331  			}
   332  		case rsMaybePlaceHolderFormat:
   333  			doWrite = false
   334  			if r == placeholderRight { // same as empty format, state rolls back
   335  				s = rsInPlaceholder
   336  			} else {
   337  				s = rsInPlaceHolderFormat
   338  			}
   339  			i -= 1 // goes back for other state to process
   340  		case rsInPlaceHolderFormat:
   341  			doWrite = false
   342  			if r == placeholderRight { // end of placeholder
   343  				s = rsInPlaceholder
   344  				i -= 1
   345  			} else {
   346  				doWriteFormat = true
   347  			}
   348  		}
   349  
   350  		if doWrite {
   351  			builder.WriteRune(r)
   352  		}
   353  		if doWriteName {
   354  			nameBuilder.WriteRune(r)
   355  		}
   356  		if doWriteFormat {
   357  			formatBuilder.WriteRune(r)
   358  		}
   359  	}
   360  
   361  	if s != rsReady { // placeholder is not complete
   362  		return nil, &renderError{filename: t.filename, jsonPath: jsonPath}
   363  	}
   364  
   365  	if fromBegin && toEnd { // if the whole string is a placeholder
   366  		varName := nameBuilder.String()
   367  		if varName == "" {
   368  			return nil, &renderError{filename: t.filename, jsonPath: jsonPath}
   369  		}
   370  
   371  		if vVal, vSet := t.getVarValue(vars, varName); vSet {
   372  			return vVal, nil
   373  		} else {
   374  			// undefined var
   375  			return nil, &renderError{filename: t.filename, jsonPath: jsonPath}
   376  		}
   377  	}
   378  
   379  	return myjson.String(builder.String()), nil
   380  }
   381  
   382  var validVarFormat = regexp.MustCompile("^%([-+@0 ])?(\\d+)?\\.?(\\d+)?[tdeEfgsqxX]$")
   383  
   384  func (t *template) renderTextString(vars *vars, varName string, varFormat string) (string, error) {
   385  	if varFormat != "" && !validVarFormat.MatchString(varFormat) {
   386  		return "", errors.New("invalid format for var")
   387  	}
   388  
   389  	vVal, vSet := t.getVarValue(vars, varName)
   390  	if vSet && vVal == nil { // value set but is nil, e.g. { "key": null }
   391  		vVal = myjson.String("")
   392  	}
   393  	switch vVal.(type) {
   394  	case myjson.String:
   395  		if varFormat == "" {
   396  			varFormat = "%s"
   397  		}
   398  		return fmt.Sprintf(varFormat, string(vVal.(myjson.String))), nil
   399  	case myjson.Number:
   400  		if varFormat == "" {
   401  			return fmt.Sprintf("%v", vVal), nil
   402  		} else if varFormat[len(varFormat)-1] == 'd' {
   403  			return fmt.Sprintf(varFormat, int(float64(vVal.(myjson.Number)))), nil
   404  		} else {
   405  			return fmt.Sprintf(varFormat, float64(vVal.(myjson.Number))), nil
   406  		}
   407  	case myjson.Boolean:
   408  		if varFormat == "" {
   409  			varFormat = "%v"
   410  		}
   411  		return fmt.Sprintf(varFormat, bool(vVal.(myjson.Boolean))), nil
   412  	default:
   413  		return "", errors.New("invalid json type for template rendering")
   414  	}
   415  }
   416  
   417  func (t *template) getVarValue(vars *vars, varName string) (vVal interface{}, vSet bool) {
   418  	if vVal, vSet = vars.table[varName]; !vSet {
   419  		// finds in defaults if not found
   420  		vVal, vSet = t.defaults.table[varName]
   421  	}
   422  	return
   423  }