github.com/dahs81/otto@v0.2.1-0.20160126165905-6400716cf085/appfile/parse.go (about) 1 package appfile 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/hashicorp/go-multierror" 12 "github.com/hashicorp/hcl" 13 "github.com/hashicorp/hcl/hcl/ast" 14 "github.com/mitchellh/mapstructure" 15 ) 16 17 // Parse parses the Appfile from the given io.Reader. 18 // 19 // Due to current internal limitations, the entire contents of the 20 // io.Reader will be copied into memory first before parsing. 21 func Parse(r io.Reader) (*File, error) { 22 // Copy the reader into an in-memory buffer first since HCL requires it. 23 var buf bytes.Buffer 24 if _, err := io.Copy(&buf, r); err != nil { 25 return nil, err 26 } 27 28 // Parse the buffer 29 root, err := hcl.Parse(buf.String()) 30 if err != nil { 31 return nil, fmt.Errorf("error parsing: %s", err) 32 } 33 buf.Reset() 34 35 // Top-level item should be the object list 36 list, ok := root.Node.(*ast.ObjectList) 37 if !ok { 38 return nil, fmt.Errorf("error parsing: file doesn't contain a root object") 39 } 40 41 // Check for invalid keys 42 valid := []string{ 43 "application", 44 "customization", 45 "import", 46 "infrastructure", 47 "project", 48 } 49 if err := checkHCLKeys(list, valid); err != nil { 50 return nil, err 51 } 52 53 var result File 54 55 // Parse the imports 56 if o := list.Filter("import"); len(o.Items) > 0 { 57 if err := parseImport(&result, o); err != nil { 58 return nil, fmt.Errorf("error parsing 'import': %s", err) 59 } 60 } 61 62 // Parse the application 63 if o := list.Filter("application"); len(o.Items) > 0 { 64 if err := parseApplication(&result, o); err != nil { 65 return nil, fmt.Errorf("error parsing 'application': %s", err) 66 } 67 } 68 69 // Parse the project 70 if o := list.Filter("project"); len(o.Items) > 0 { 71 if err := parseProject(&result, o); err != nil { 72 return nil, fmt.Errorf("error parsing 'project': %s", err) 73 } 74 } 75 76 // Parse the infrastructure 77 if o := list.Filter("infrastructure"); len(o.Items) > 0 { 78 if err := parseInfra(&result, o); err != nil { 79 return nil, fmt.Errorf("error parsing 'infrastructure': %s", err) 80 } 81 } 82 83 // Parse the customizations 84 if o := list.Filter("customization"); len(o.Items) > 0 { 85 if err := parseCustomizations(&result, o); err != nil { 86 return nil, fmt.Errorf("error parsing 'customization': %s", err) 87 } 88 } 89 90 return &result, nil 91 } 92 93 // ParseFile parses the given path as an Appfile. 94 func ParseFile(path string) (*File, error) { 95 path, err := filepath.Abs(path) 96 if err != nil { 97 return nil, err 98 } 99 100 f, err := os.Open(path) 101 if err != nil { 102 return nil, err 103 } 104 defer f.Close() 105 106 result, err := Parse(f) 107 if result != nil { 108 result.Path = path 109 if err := result.loadID(); err != nil { 110 return nil, err 111 } 112 } 113 114 return result, err 115 } 116 117 func parseApplication(result *File, list *ast.ObjectList) error { 118 if len(list.Items) > 1 { 119 return fmt.Errorf("only one 'application' block allowed") 120 } 121 122 // Get our one item 123 item := list.Items[0] 124 125 // Check for invalid keys 126 valid := []string{"name", "type", "detect", "dependency"} 127 if err := checkHCLKeys(item.Val, valid); err != nil { 128 return multierror.Prefix(err, "application:") 129 } 130 131 var m map[string]interface{} 132 if err := hcl.DecodeObject(&m, item.Val); err != nil { 133 return err 134 } 135 136 app := Application{Detect: true} 137 result.Application = &app 138 return mapstructure.WeakDecode(m, &app) 139 } 140 141 func parseCustomizations(result *File, list *ast.ObjectList) error { 142 // Go through each object and turn it into an actual result. 143 collection := make([]*Customization, 0, len(list.Items)) 144 for _, item := range list.Items { 145 key := "app" 146 if len(item.Keys) > 0 { 147 key = item.Keys[0].Token.Value().(string) 148 } 149 150 var m map[string]interface{} 151 if err := hcl.DecodeObject(&m, item.Val); err != nil { 152 return err 153 } 154 155 var c Customization 156 c.Type = strings.ToLower(key) 157 c.Config = m 158 159 collection = append(collection, &c) 160 } 161 162 result.Customization = &CustomizationSet{Raw: collection} 163 return nil 164 } 165 166 func parseImport(result *File, list *ast.ObjectList) error { 167 list = list.Children() 168 if len(list.Items) == 0 { 169 return nil 170 } 171 172 // Go through each object and turn it into an actual result. 173 collection := make([]*Import, 0, len(list.Items)) 174 seen := make(map[string]struct{}) 175 for _, item := range list.Items { 176 key := item.Keys[0].Token.Value().(string) 177 178 // Make sure we haven't already found this import 179 if _, ok := seen[key]; ok { 180 return fmt.Errorf("import '%s' defined more than once", key) 181 } 182 seen[key] = struct{}{} 183 184 // Check for invalid keys 185 if err := checkHCLKeys(item.Val, nil); err != nil { 186 return multierror.Prefix(err, fmt.Sprintf( 187 "import '%s':", key)) 188 } 189 190 collection = append(collection, &Import{ 191 Source: key, 192 }) 193 } 194 195 result.Imports = collection 196 return nil 197 } 198 199 func parseInfra(result *File, list *ast.ObjectList) error { 200 list = list.Children() 201 if len(list.Items) == 0 { 202 return nil 203 } 204 205 // Go through each object and turn it into an actual result. 206 collection := make([]*Infrastructure, 0, len(list.Items)) 207 seen := make(map[string]struct{}) 208 for _, item := range list.Items { 209 n := item.Keys[0].Token.Value().(string) 210 211 // Make sure we haven't already found this 212 if _, ok := seen[n]; ok { 213 return fmt.Errorf("infrastructure '%s' defined more than once", n) 214 } 215 seen[n] = struct{}{} 216 217 // Check for invalid keys 218 valid := []string{"name", "type", "flavor", "foundation"} 219 if err := checkHCLKeys(item.Val, valid); err != nil { 220 return multierror.Prefix(err, fmt.Sprintf( 221 "infrastructure '%s':", n)) 222 } 223 224 var listVal *ast.ObjectList 225 if ot, ok := item.Val.(*ast.ObjectType); ok { 226 listVal = ot.List 227 } else { 228 return fmt.Errorf("infrastructure '%s': should be an object", n) 229 } 230 231 var m map[string]interface{} 232 if err := hcl.DecodeObject(&m, item.Val); err != nil { 233 return err 234 } 235 236 var infra Infrastructure 237 if err := mapstructure.WeakDecode(m, &infra); err != nil { 238 return fmt.Errorf( 239 "error parsing infrastructure '%s': %s", n, err) 240 } 241 242 infra.Name = n 243 if infra.Type == "" { 244 infra.Type = infra.Name 245 } 246 247 // Parse the foundations if we have any 248 if o2 := listVal.Filter("foundation"); len(o2.Items) > 0 { 249 if err := parseFoundations(&infra, o2); err != nil { 250 return fmt.Errorf("error parsing 'foundation': %s", err) 251 } 252 } 253 254 collection = append(collection, &infra) 255 } 256 257 result.Infrastructure = collection 258 return nil 259 } 260 261 func parseFoundations(result *Infrastructure, list *ast.ObjectList) error { 262 list = list.Children() 263 if len(list.Items) == 0 { 264 return nil 265 } 266 267 // Go through each object and turn it into an actual result. 268 collection := make([]*Foundation, 0, len(list.Items)) 269 seen := make(map[string]struct{}) 270 for _, item := range list.Items { 271 n := item.Keys[0].Token.Value().(string) 272 273 // Make sure we haven't already found this 274 if _, ok := seen[n]; ok { 275 return fmt.Errorf("foundation '%s' defined more than once", n) 276 } 277 seen[n] = struct{}{} 278 279 var m map[string]interface{} 280 if err := hcl.DecodeObject(&m, item.Val); err != nil { 281 return err 282 } 283 284 var f Foundation 285 f.Name = n 286 f.Config = m 287 288 collection = append(collection, &f) 289 } 290 291 // Set the results 292 result.Foundations = collection 293 return nil 294 } 295 296 func parseProject(result *File, list *ast.ObjectList) error { 297 if len(list.Items) > 1 { 298 return fmt.Errorf("only one 'project' block allowed") 299 } 300 301 // Get our one item 302 item := list.Items[0] 303 304 // Check for invalid keys 305 valid := []string{"name", "infrastructure"} 306 if err := checkHCLKeys(item.Val, valid); err != nil { 307 return multierror.Prefix(err, "project:") 308 } 309 310 var m map[string]interface{} 311 if err := hcl.DecodeObject(&m, item.Val); err != nil { 312 return err 313 } 314 315 // Parse the project 316 var proj Project 317 result.Project = &proj 318 if err := mapstructure.WeakDecode(m, &proj); err != nil { 319 return err 320 } 321 322 return nil 323 } 324 325 func checkHCLKeys(node ast.Node, valid []string) error { 326 var list *ast.ObjectList 327 switch n := node.(type) { 328 case *ast.ObjectList: 329 list = n 330 case *ast.ObjectType: 331 list = n.List 332 default: 333 return fmt.Errorf("cannot check HCL keys of type %T", n) 334 } 335 336 validMap := make(map[string]struct{}, len(valid)) 337 for _, v := range valid { 338 validMap[v] = struct{}{} 339 } 340 341 var result error 342 for _, item := range list.Items { 343 key := item.Keys[0].Token.Value().(string) 344 if _, ok := validMap[key]; !ok { 345 result = multierror.Append(result, fmt.Errorf( 346 "invalid key: %s", key)) 347 } 348 } 349 350 return result 351 }