github.com/bhameyie/otto@v0.2.1-0.20160406174117-16052efa52ec/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  }