github.com/gocaveman/caveman@v0.0.0-20191211162744-0ddf99dbdf6e/tmpl/yaml-string-data-map.go (about)

     1  package tmpl
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"strings"
    10  
    11  	"github.com/gocaveman/caveman/webutil"
    12  	yaml "gopkg.in/yaml.v2"
    13  )
    14  
    15  // // StringDataMap describes a map of string keys and generic interface values.
    16  // // This interface matches StringDataMap in the pageinfo package, so the meta data
    17  // // is interoperable.  Implementations are not thread-safe.
    18  // type StringDataMap interface {
    19  // 	Value(key string) interface{}
    20  // 	Keys() []string
    21  // 	Set(key string, val interface{}) // Set("key", nil) will delete "key"
    22  // }
    23  
    24  // type check
    25  var _ webutil.StringDataMap = &YAMLStringDataMap{}
    26  
    27  // YAMLStringDataMap implements StringDataMap supporting a subset of YAML and
    28  // facilitates reading and writing while attempting to preserve comments and sequence.
    29  // It is intended for use in the top section of a template file (see FileSystemStore).
    30  type YAMLStringDataMap struct {
    31  	entries         []YAMLMapEntry
    32  	entryMap        map[string]int
    33  	trailingComment string
    34  }
    35  
    36  // Map returns a new map[string]interface{} with the data from this instance.
    37  func (m *YAMLStringDataMap) Map() map[string]interface{} {
    38  	if m == nil {
    39  		return nil
    40  	}
    41  	ret := make(map[string]interface{}, len(m.entries))
    42  	for _, e := range m.entries {
    43  		ret[e.Key] = e.Value
    44  	}
    45  	return ret
    46  }
    47  
    48  func (m *YAMLStringDataMap) rebuildEntryMap() {
    49  	m.entryMap = make(map[string]int, len(m.entries))
    50  	for i, e := range m.entries {
    51  		m.entryMap[e.Key] = i
    52  	}
    53  
    54  }
    55  
    56  func (m *YAMLStringDataMap) Data(key string) interface{} {
    57  	i, ok := m.entryMap[key]
    58  	if !ok {
    59  		return nil
    60  	}
    61  	return m.entries[i].Value
    62  }
    63  
    64  func (m *YAMLStringDataMap) Keys() (ret []string) {
    65  	for _, e := range m.entries {
    66  		ret = append(ret, e.Key)
    67  	}
    68  	return
    69  }
    70  
    71  func (m *YAMLStringDataMap) Set(key string, val interface{}) {
    72  
    73  	i, ok := m.entryMap[key]
    74  	if !ok {
    75  
    76  		if val == nil { // deleting non-existent key, nop
    77  			return
    78  		}
    79  
    80  		// create a new entry and append it
    81  		var e YAMLMapEntry
    82  		e.Key = key
    83  		e.Value = val
    84  		m.entryMap[key] = len(m.entries)
    85  		m.entries = append(m.entries, e)
    86  
    87  		return
    88  	}
    89  
    90  	// update value for existing entry
    91  
    92  	// delete case
    93  	if val == nil {
    94  
    95  		es := m.entries
    96  		es[i].Value = nil
    97  		copy(es[i:], es[i+1:])
    98  		m.entries = es[:len(es)-1]
    99  
   100  		m.rebuildEntryMap()
   101  		return
   102  	}
   103  
   104  	// set case is a simple assignment
   105  	m.entries[i].Value = val
   106  	return
   107  }
   108  
   109  type YAMLMapEntry struct {
   110  	Comment string
   111  	Key     string
   112  	Value   interface{}
   113  }
   114  
   115  // NewYAMLStringDataMap returns a newly initialized YAMLStringDataMap.
   116  func NewYAMLStringDataMap() *YAMLStringDataMap {
   117  	log.Printf("FIXME: YAML is probably a mistake, consider switching to TOML (whitespace sensitivity is a bad thing)")
   118  	return &YAMLStringDataMap{
   119  		entryMap: make(map[string]int),
   120  	}
   121  }
   122  
   123  // BUG(bgp): Keep an eye on this: https://github.com/go-yaml/yaml/pull/219 - would be better
   124  // to use a (more) correctly implemented comment parsing solution.
   125  
   126  // ReadYAMLStringDataMap reads input that is a subset of YAML.
   127  // The data must be a YAML map and comments above each individual map entry
   128  // are preserved, as is the sequence of the keys.
   129  func ReadYAMLStringDataMap(in io.Reader) (*YAMLStringDataMap, error) {
   130  
   131  	ret := NewYAMLStringDataMap()
   132  
   133  	br := bufio.NewReader(in)
   134  
   135  	var commentBuf bytes.Buffer // the current comment we are building
   136  	var yamlBuf bytes.Buffer    // the current yaml map entry we are building
   137  	var inComment = true        // are we currently reading the comment
   138  
   139  	flushEntry := func() error {
   140  		defer func() {
   141  			commentBuf.Truncate(0)
   142  			yamlBuf.Truncate(0)
   143  			inComment = true
   144  		}()
   145  
   146  		ms := yaml.MapSlice{}
   147  		err := yaml.Unmarshal(yamlBuf.Bytes(), &ms)
   148  		if err != nil {
   149  			return err
   150  		}
   151  
   152  		for _, mi := range ms {
   153  
   154  			// ugly, but basically we needed to use MapSlice above to preserve the order,
   155  			// but now we have to marshal and unmarshal again using map[string]interface{},
   156  			// so we don't have these yaml library internal types in our values
   157  			b, err := yaml.Marshal(yaml.MapSlice{mi})
   158  			if err != nil {
   159  				return err
   160  			}
   161  			var mi2 map[string]interface{}
   162  			err = yaml.Unmarshal(b, &mi2)
   163  			if err != nil {
   164  				return err
   165  			}
   166  
   167  			var e YAMLMapEntry
   168  			keyStr, ok := mi.Key.(string)
   169  			if !ok {
   170  				return fmt.Errorf("YAMLStringDataMap does not support key of type %T (val=%v)", mi.Key, mi.Key)
   171  			}
   172  			e.Key = keyStr
   173  			e.Value = mi2[keyStr] // use the value from mi2
   174  			if commentBuf.Len() > 0 {
   175  				e.Comment = commentBuf.String()
   176  				commentBuf.Truncate(0)
   177  			}
   178  			ret.entries = append(ret.entries, e)
   179  		}
   180  
   181  		return nil
   182  	}
   183  
   184  	// read line by line
   185  	for {
   186  		line, err := br.ReadString('\n')
   187  		if err == io.EOF {
   188  			break
   189  		}
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  
   194  		lineTrimmed := strings.TrimSpace(line)
   195  		lineIsComment := lineTrimmed == "" || lineTrimmed[0] == '#'
   196  
   197  		switch {
   198  
   199  		// reading comments, continue
   200  		case inComment && lineIsComment:
   201  			commentBuf.WriteString(line)
   202  
   203  		// reading yaml entry, continue
   204  		case !inComment && !lineIsComment:
   205  			yamlBuf.WriteString(line)
   206  
   207  		// were in comment but now have yaml entry, switch to yaml entry
   208  		case inComment && !lineIsComment:
   209  			inComment = false
   210  			yamlBuf.WriteString(line)
   211  
   212  		// were reading yaml entry but now have comment
   213  		case !inComment && lineIsComment:
   214  			err := flushEntry() // save the entry, clear bufs and set inComment = true
   215  			if err != nil {
   216  				return nil, err
   217  			}
   218  			commentBuf.WriteString(line)
   219  
   220  		}
   221  
   222  	}
   223  
   224  	if yamlBuf.Len() > 0 {
   225  		err := flushEntry()
   226  		if err != nil {
   227  			return nil, err
   228  		}
   229  	} else if commentBuf.Len() > 0 {
   230  		ret.trailingComment = commentBuf.String()
   231  	}
   232  
   233  	ret.rebuildEntryMap()
   234  
   235  	return ret, nil
   236  }
   237  
   238  // WriteYAMLStringDataMap will write a YAMLStringDataMap, preserving sequence and comments
   239  // where preserved from ReadYAMLStringDataMap.
   240  func WriteYAMLStringDataMap(out io.Writer, m *YAMLStringDataMap) error {
   241  
   242  	for _, e := range m.entries {
   243  
   244  		if e.Comment != "" {
   245  			_, err := fmt.Fprint(out, e.Comment)
   246  			if err != nil {
   247  				return err
   248  			}
   249  			if !strings.HasSuffix(e.Comment, "\n") {
   250  				_, err := out.Write([]byte("\n"))
   251  				if err != nil {
   252  					return err
   253  				}
   254  			}
   255  		}
   256  
   257  		ms := yaml.MapSlice{
   258  			yaml.MapItem{
   259  				Key:   e.Key,
   260  				Value: e.Value,
   261  			},
   262  		}
   263  
   264  		b, err := yaml.Marshal(&ms)
   265  		if err != nil {
   266  			return err
   267  		}
   268  
   269  		_, err = out.Write(b)
   270  		if err != nil {
   271  			return err
   272  		}
   273  
   274  		if !bytes.HasSuffix(b, []byte("\n")) {
   275  
   276  			_, err = out.Write([]byte("\n"))
   277  			if err != nil {
   278  				return err
   279  			}
   280  
   281  		}
   282  
   283  	}
   284  
   285  	if len(m.trailingComment) > 0 {
   286  		_, err := out.Write([]byte(m.trailingComment))
   287  		if err != nil {
   288  			return err
   289  		}
   290  	}
   291  
   292  	return nil
   293  }