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 }